diff --git a/.coveragerc b/.coveragerc index 0cadadfc5e814..5fc54f1dbea7f 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 @@ -145,7 +150,6 @@ omit = homeassistant/components/clickatell/notify.py homeassistant/components/clicksend/notify.py homeassistant/components/clicksend_tts/notify.py - homeassistant/components/climacell/weather.py homeassistant/components/cmus/media_player.py homeassistant/components/co2signal/* homeassistant/components/coinbase/* @@ -174,6 +178,7 @@ omit = homeassistant/components/deluge/sensor.py homeassistant/components/deluge/switch.py homeassistant/components/denon/media_player.py + homeassistant/components/denonavr/__init__.py homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py @@ -237,6 +242,8 @@ omit = homeassistant/components/emby/media_player.py homeassistant/components/emoncms/sensor.py homeassistant/components/emoncms_history/* + homeassistant/components/emonitor/__init__.py + homeassistant/components/emonitor/sensor.py homeassistant/components/enigma2/media_player.py homeassistant/components/enocean/__init__.py homeassistant/components/enocean/binary_sensor.py @@ -246,12 +253,14 @@ omit = homeassistant/components/enocean/light.py homeassistant/components/enocean/sensor.py homeassistant/components/enocean/switch.py + homeassistant/components/enphase_envoy/__init__.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/entur_public_transport/* homeassistant/components/environment_canada/* homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py + homeassistant/components/epson/__init__.py homeassistant/components/epson/const.py homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py @@ -271,7 +280,13 @@ omit = homeassistant/components/eufy/* homeassistant/components/everlights/light.py homeassistant/components/evohome/* - homeassistant/components/ezviz/* + homeassistant/components/ezviz/__init__.py + homeassistant/components/ezviz/camera.py + homeassistant/components/ezviz/coordinator.py + homeassistant/components/ezviz/const.py + homeassistant/components/ezviz/binary_sensor.py + homeassistant/components/ezviz/sensor.py + homeassistant/components/ezviz/switch.py homeassistant/components/familyhub/camera.py homeassistant/components/faa_delays/__init__.py homeassistant/components/faa_delays/binary_sensor.py @@ -294,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 @@ -319,7 +334,13 @@ omit = homeassistant/components/freebox/router.py 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 @@ -329,13 +350,15 @@ 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 homeassistant/components/garmin_connect/alarm_util.py homeassistant/components/gc100/* homeassistant/components/geniushub/* - homeassistant/components/geizhals/sensor.py homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py @@ -346,9 +369,12 @@ 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 + homeassistant/components/google_travel_time/__init__.py + homeassistant/components/google_travel_time/helpers.py homeassistant/components/google_travel_time/sensor.py homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py @@ -357,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 @@ -421,6 +448,7 @@ omit = homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* + homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py @@ -502,6 +530,10 @@ omit = homeassistant/components/kodi/media_player.py homeassistant/components/kodi/notify.py homeassistant/components/konnected/* + homeassistant/components/kostal_plenticore/__init__.py + homeassistant/components/kostal_plenticore/const.py + homeassistant/components/kostal_plenticore/helper.py + homeassistant/components/kostal_plenticore/sensor.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lametric/* @@ -526,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 @@ -573,6 +604,8 @@ omit = homeassistant/components/melcloud/water_heater.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py + homeassistant/components/met_eireann/__init__.py + homeassistant/components/met_eireann/weather.py homeassistant/components/meteo_france/__init__.py homeassistant/components/meteo_france/const.py homeassistant/components/meteo_france/sensor.py @@ -597,10 +630,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/modbus/sensor.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py @@ -612,6 +641,8 @@ omit = homeassistant/components/msteams/notify.py homeassistant/components/mullvad/__init__.py homeassistant/components/mullvad/binary_sensor.py + homeassistant/components/mutesync/__init__.py + homeassistant/components/mutesync/binary_sensor.py homeassistant/components/nest/const.py homeassistant/components/mvglive/sensor.py homeassistant/components/mychevy/* @@ -634,7 +665,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 @@ -667,6 +698,7 @@ omit = homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nuki/__init__.py homeassistant/components/nuki/const.py + homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/lock.py homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py @@ -680,6 +712,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 @@ -759,6 +792,7 @@ omit = homeassistant/components/poolsense/__init__.py homeassistant/components/poolsense/sensor.py homeassistant/components/poolsense/binary_sensor.py + homeassistant/components/powerwall/__init__.py homeassistant/components/proliphix/climate.py homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/binary_sensor.py @@ -807,9 +841,13 @@ omit = homeassistant/components/rest/switch.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py + homeassistant/components/rituals_perfume_genie/binary_sensor.py + homeassistant/components/rituals_perfume_genie/entity.py + homeassistant/components/rituals_perfume_genie/sensor.py homeassistant/components/rituals_perfume_genie/switch.py homeassistant/components/rituals_perfume_genie/__init__.py homeassistant/components/rocketchat/notify.py + homeassistant/components/roomba/__init__.py homeassistant/components/roomba/binary_sensor.py homeassistant/components/roomba/braava.py homeassistant/components/roomba/irobot_base.py @@ -834,6 +872,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 @@ -841,6 +880,7 @@ omit = homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py homeassistant/components/screenlogic/sensor.py + homeassistant/components/screenlogic/services.py homeassistant/components/screenlogic/switch.py homeassistant/components/scsgate/* homeassistant/components/scsgate/cover.py @@ -876,8 +916,14 @@ 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 homeassistant/components/sma/sensor.py homeassistant/components/smappee/__init__.py homeassistant/components/smappee/api.py @@ -893,7 +939,6 @@ omit = homeassistant/components/snapcast/* homeassistant/components/snmp/* homeassistant/components/sochain/sensor.py - homeassistant/components/socialblade/sensor.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py @@ -902,7 +947,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/* @@ -911,7 +961,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 @@ -935,6 +984,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 @@ -944,6 +997,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 @@ -1004,7 +1061,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 @@ -1097,6 +1153,8 @@ omit = homeassistant/components/waterfurnace/* homeassistant/components/watson_iot/* homeassistant/components/watson_tts/tts.py + homeassistant/components/waze_travel_time/__init__.py + homeassistant/components/waze_travel_time/helpers.py homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* homeassistant/components/whois/sensor.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index aa81d6e4df71c..116afec36eecb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,5 @@ name: Report an issue with Home Assistant Core description: Report an issue with Home Assistant Core. -title: "" -issue_body: true body: - type: markdown attributes: @@ -85,13 +83,10 @@ body: label: Anything in the logs that might be useful for us? description: For example, error message, or stack traces. render: txt - - type: markdown - attributes: - value: | - ## Additional information - - type: markdown + - type: textarea attributes: - value: > + label: Additional information + description: > If you have any additional information for us, use the field below. Please note, you can attach screenshots or screen recordings here, by dragging and dropping files in the field below. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 05726ab79e51a..7c169580cb291 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 [21][43] + for i, register in enumerate(registers): + registers[i] = int.from_bytes( + register.to_bytes(2, byteorder="little"), + byteorder="big", + signed=False, ) - except ConnectionException: - self._available = False - return + if self._swap in [CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE]: + # convert [12][34] ==> [34][12] + registers.reverse() + return registers - if isinstance(result, (ModbusException, ExceptionResponse)): + async def async_update(self, now=None): + """Update the state of the sensor.""" + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval + result = await self._hub.async_pymodbus_call( + self._slave, self._register, self._count, self._register_type + ) + if result is None: self._available = False + self.async_write_ha_state() return - registers = result.registers - if self._reverse_order: - registers.reverse() - + registers = self._swap_registers(result.registers) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) if self._data_type == DATA_TYPE_STRING: self._value = byte_string.decode() @@ -319,24 +270,30 @@ def _update(self): # If unpack() returns a tuple greater than 1, don't try to process the value. # Instead, return the values of unpack(...) separated by commas. if len(val) > 1: - self._value = ",".join(map(str, val)) - else: - val = val[0] - # Apply scale and precision to floats and ints - if isinstance(val, (float, int)): - val = self._scale * val + self._offset + v_result = [] + for entry in val: + v_temp = self._scale * entry + self._offset # We could convert int to float, and the code would still work; however # we lose some precision, and unit tests will fail. Therefore, we do # the conversion only when it's absolutely necessary. - if isinstance(val, int) and self._precision == 0: - self._value = str(val) + if isinstance(v_temp, int) and self._precision == 0: + v_result.append(str(v_temp)) else: - self._value = f"{float(val):.{self._precision}f}" - else: - # Don't process remaining datatypes (bytes and booleans) + v_result.append(f"{float(v_temp):.{self._precision}f}") + self._value = ",".join(map(str, v_result)) + else: + # Apply scale and precision to floats and ints + val = self._scale * val[0] + self._offset + + # We could convert int to float, and the code would still work; however + # we lose some precision, and unit tests will fail. Therefore, we do + # the conversion only when it's absolutely necessary. + if isinstance(val, int) and self._precision == 0: self._value = str(val) + else: + self._value = f"{float(val):.{self._precision}f}" self._available = True - self.schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index ba3113db5e037..a3aa26a1a4129 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -1,30 +1,72 @@ write_coil: + name: Write coil description: Write to a modbus coil. fields: address: + name: Address description: Address of the register to write to. + required: true example: 0 + selector: + number: + min: 1 + max: 255 state: + name: State description: State to write. + required: true example: false + selector: + object: unit: + name: Unit description: Address of the modbus unit. + required: true example: 21 + selector: + number: + min: 1 + max: 255 hub: - description: Optional Modbus hub name. A hub with the name 'default' is used if not specified. + name: Hub + description: Modbus hub name. example: "hub1" + default: "modbus_hub" + selector: + text: write_register: + name: Write register description: Write to a modbus holding register. fields: address: + name: Address description: Address of the holding register to write to. + required: true example: 0 + selector: + number: + min: 1 + max: 255 unit: + name: Unit description: Address of the modbus unit. + required: true example: 21 + selector: + number: + min: 1 + max: 255 value: + name: Value description: Value (single value or array) to write. + required: true example: "0 or [4,0]" + selector: + object: hub: - description: Optional Modbus hub name. A hub with the name 'default' is used if not specified. + name: Hub + description: Modbus hub name. example: "hub1" + default: "modbus_hub" + selector: + text: diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 2985d8b2c0591..ef068d7bd184c 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -1,332 +1,157 @@ """Support for Modbus switches.""" from __future__ import annotations -from abc import ABC, abstractmethod -from datetime import timedelta import logging -from typing import Any -from pymodbus.exceptions import ConnectionException, ModbusException -from pymodbus.pdu import ExceptionResponse -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( CONF_ADDRESS, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_NAME, - CONF_SCAN_INTERVAL, - CONF_SLAVE, CONF_SWITCHES, STATE_ON, ) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.core import HomeAssistant from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType +from .base_platform import BasePlatform from .const import ( CALL_TYPE_COIL, - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_REGISTER_INPUT, - CONF_COILS, - CONF_HUB, + CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_REGISTER, CONF_INPUT_TYPE, - CONF_REGISTER, - CONF_REGISTER_TYPE, - CONF_REGISTERS, CONF_STATE_OFF, CONF_STATE_ON, - CONF_VERIFY_REGISTER, - CONF_VERIFY_STATE, - DEFAULT_HUB, - DEFAULT_SCAN_INTERVAL, + CONF_VERIFY, + CONF_WRITE_TYPE, MODBUS_DOMAIN, ) from .modbus import ModbusHub +PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) -REGISTERS_SCHEMA = vol.Schema( - { - vol.Required(CONF_COMMAND_OFF): cv.positive_int, - vol.Required(CONF_COMMAND_ON): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_REGISTER): cv.positive_int, - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Optional(CONF_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( - [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT] - ), - vol.Optional(CONF_SLAVE): cv.positive_int, - vol.Optional(CONF_STATE_OFF): cv.positive_int, - vol.Optional(CONF_STATE_ON): cv.positive_int, - vol.Optional(CONF_VERIFY_REGISTER): cv.positive_int, - vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean, - } -) - -COILS_SCHEMA = vol.Schema( - { - vol.Required(CALL_TYPE_COIL): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_SLAVE): cv.positive_int, - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - } -) - -PLATFORM_SCHEMA = vol.All( - cv.has_at_least_one_key(CONF_COILS, CONF_REGISTERS), - PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_COILS): [COILS_SCHEMA], - vol.Optional(CONF_REGISTERS): [REGISTERS_SCHEMA], - } - ), -) - - async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Read configuration and create Modbus switches.""" switches = [] - #  check for old config: - if discovery_info is None: - _LOGGER.warning( - "Switch configuration is deprecated, will be removed in a future release" - ) - discovery_info = { - CONF_NAME: "no name", - CONF_SWITCHES: [], - } - if CONF_COILS in config: - discovery_info[CONF_SWITCHES].extend(config[CONF_COILS]) - if CONF_REGISTERS in config: - discovery_info[CONF_SWITCHES].extend(config[CONF_REGISTERS]) - for entry in discovery_info[CONF_SWITCHES]: - if CALL_TYPE_COIL in entry: - entry[CONF_ADDRESS] = entry[CALL_TYPE_COIL] - entry[CONF_INPUT_TYPE] = CALL_TYPE_COIL - del entry[CALL_TYPE_COIL] - if CONF_REGISTER in entry: - entry[CONF_ADDRESS] = entry[CONF_REGISTER] - del entry[CONF_REGISTER] - if CONF_REGISTER_TYPE in entry: - entry[CONF_INPUT_TYPE] = entry[CONF_REGISTER_TYPE] - del entry[CONF_REGISTER_TYPE] - if CONF_SCAN_INTERVAL not in entry: - entry[CONF_SCAN_INTERVAL] = DEFAULT_SCAN_INTERVAL - config = None - for entry in discovery_info[CONF_SWITCHES]: - if CONF_HUB in entry: - # from old config! - discovery_info[CONF_NAME] = entry[CONF_HUB] hub: ModbusHub = hass.data[MODBUS_DOMAIN][discovery_info[CONF_NAME]] - if entry[CONF_INPUT_TYPE] == CALL_TYPE_COIL: - switches.append(ModbusCoilSwitch(hub, entry)) - else: - switches.append(ModbusRegisterSwitch(hub, entry)) + switches.append(ModbusSwitch(hub, entry)) async_add_entities(switches) -class ModbusBaseSwitch(SwitchEntity, RestoreEntity, ABC): +class ModbusSwitch(BasePlatform, SwitchEntity, RestoreEntity): """Base class representing a Modbus switch.""" - def __init__(self, hub: ModbusHub, config: dict[str, Any]): + def __init__(self, hub: ModbusHub, config: dict) -> None: """Initialize the switch.""" - self._hub: ModbusHub = hub - self._name = config[CONF_NAME] - self._slave = config.get(CONF_SLAVE) + config[CONF_INPUT_TYPE] = "" + super().__init__(hub, config) self._is_on = None - self._available = True - self._scan_interval = timedelta(seconds=config[CONF_SCAN_INTERVAL]) + if config[CONF_WRITE_TYPE] == CALL_TYPE_COIL: + self._write_type = CALL_TYPE_WRITE_COIL + else: + self._write_type = CALL_TYPE_WRITE_REGISTER + self._command_on = config[CONF_COMMAND_ON] + self._command_off = config[CONF_COMMAND_OFF] + if CONF_VERIFY in config: + if config[CONF_VERIFY] is None: + config[CONF_VERIFY] = {} + self._verify_active = True + self._verify_address = config[CONF_VERIFY].get( + CONF_ADDRESS, config[CONF_ADDRESS] + ) + self._verify_type = config[CONF_VERIFY].get( + CONF_INPUT_TYPE, config[CONF_WRITE_TYPE] + ) + self._state_on = config[CONF_VERIFY].get(CONF_STATE_ON, self._command_on) + self._state_off = config[CONF_VERIFY].get(CONF_STATE_OFF, self._command_off) + else: + self._verify_active = False async def async_added_to_hass(self): """Handle entity which will be added.""" + await self.async_base_added_to_hass() state = await self.async_get_last_state() if state: self._is_on = state.state == STATE_ON - async_track_time_interval( - self.hass, lambda arg: self._update(), self._scan_interval - ) - - @abstractmethod - def _update(self): - """Update the entity state.""" - @property def is_on(self): """Return true if switch is on.""" return self._is_on - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - - # Handle polling directly in this entity - return False - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - -class ModbusCoilSwitch(ModbusBaseSwitch, SwitchEntity): - """Representation of a Modbus coil switch.""" - - def __init__(self, hub: ModbusHub, config: dict[str, Any]): - """Initialize the coil switch.""" - super().__init__(hub, config) - self._coil = config[CONF_ADDRESS] - - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Set switch on.""" - self._write_coil(self._coil, True) - self._is_on = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Set switch off.""" - self._write_coil(self._coil, False) - self._is_on = False - self.schedule_update_ha_state() - - def _update(self): - """Update the state of the switch.""" - self._is_on = self._read_coil(self._coil) - self.schedule_update_ha_state() - - def _read_coil(self, coil) -> bool: - """Read coil using the Modbus hub slave.""" - try: - result = self._hub.read_coils(self._slave, coil, 1) - except ConnectionException: - self._available = False - return False - - if isinstance(result, (ModbusException, ExceptionResponse)): - self._available = False - return False - - self._available = True - # bits[0] select the lowest bit in result, - # is_on for a binary_sensor is true if the bit is 1 - # The other bits are not considered. - return bool(result.bits[0] & 1) - def _write_coil(self, coil, value): - """Write coil using the Modbus hub slave.""" - try: - self._hub.write_coil(self._slave, coil, value) - except ConnectionException: + result = await self._hub.async_pymodbus_call( + self._slave, self._address, self._command_on, self._write_type + ) + if result is None: self._available = False - return - - self._available = True - - -class ModbusRegisterSwitch(ModbusBaseSwitch, SwitchEntity): - """Representation of a Modbus register switch.""" - - def __init__(self, hub: ModbusHub, config: dict[str, Any]): - """Initialize the register switch.""" - super().__init__(hub, config) - self._register = config[CONF_ADDRESS] - self._command_on = config[CONF_COMMAND_ON] - self._command_off = config[CONF_COMMAND_OFF] - self._state_on = config.get(CONF_STATE_ON, self._command_on) - self._state_off = config.get(CONF_STATE_OFF, self._command_off) - self._verify_state = config[CONF_VERIFY_STATE] - self._verify_register = config.get(CONF_VERIFY_REGISTER, self._register) - self._register_type = config[CONF_INPUT_TYPE] - self._available = True - self._is_on = None - - def turn_on(self, **kwargs): - """Set switch on.""" - # Only holding register is writable - if self._register_type == CALL_TYPE_REGISTER_HOLDING: - self._write_register(self._command_on) - if not self._verify_state: + self.async_write_ha_state() + else: + self._available = True + if self._verify_active: + await self.async_update() + else: self._is_on = True - self.schedule_update_ha_state() + self.async_write_ha_state() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Set switch off.""" - # Only holding register is writable - if self._register_type == CALL_TYPE_REGISTER_HOLDING: - self._write_register(self._command_off) - if not self._verify_state: - self._is_on = False - self.schedule_update_ha_state() - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - def _update(self): - """Update the state of the switch.""" - if not self._verify_state: - return - - value = self._read_register() - if value == self._state_on: - self._is_on = True - elif value == self._state_off: - self._is_on = False - elif value is not None: - _LOGGER.error( - "Unexpected response from hub %s, slave %s register %s, got 0x%2x", - self._hub.name, - self._slave, - self._register, - value, - ) - self.schedule_update_ha_state() - - def _read_register(self) -> int | None: - try: - if self._register_type == CALL_TYPE_REGISTER_INPUT: - result = self._hub.read_input_registers( - self._slave, self._verify_register, 1 - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._verify_register, 1 - ) - except ConnectionException: + result = await self._hub.async_pymodbus_call( + self._slave, self._address, self._command_off, self._write_type + ) + if result is None: self._available = False - return + self.async_write_ha_state() + else: + self._available = True + if self._verify_active: + await self.async_update() + else: + self._is_on = False + self.async_write_ha_state() - if isinstance(result, (ModbusException, ExceptionResponse)): - self._available = False + async def async_update(self, now=None): + """Update the entity state.""" + # remark "now" is a dummy parameter to avoid problems with + # async_track_time_interval + if not self._verify_active: + self._available = True + self.async_write_ha_state() return - self._available = True - - return int(result.registers[0]) - - def _write_register(self, value): - """Write holding register using the Modbus hub slave.""" - try: - self._hub.write_register(self._slave, self._register, value) - except ConnectionException: + result = await self._hub.async_pymodbus_call( + self._slave, self._verify_address, 1, self._verify_type + ) + if result is None: self._available = False + self.async_write_ha_state() return self._available = True + if self._verify_type == CALL_TYPE_COIL: + self._is_on = bool(result.bits[0] & 1) + else: + value = int(result.registers[0]) + if value == self._state_on: + self._is_on = True + elif value == self._state_off: + self._is_on = False + elif value is not None: + _LOGGER.error( + "Unexpected response from hub %s, slave %s register %s, got 0x%2x", + self._hub.name, + self._slave, + self._verify_address, + value, + ) + self.async_write_ha_state() diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index 21e9c94943d25..a3bb7b676f0ea 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -3,5 +3,6 @@ "name": "Modem Caller ID", "documentation": "https://www.home-assistant.io/integrations/modem_callerid", "requirements": ["basicmodem==0.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mold_indicator/manifest.json b/homeassistant/components/mold_indicator/manifest.json index 764faf6e79a85..ce10c8e3692ec 100644 --- a/homeassistant/components/mold_indicator/manifest.json +++ b/homeassistant/components/mold_indicator/manifest.json @@ -3,5 +3,6 @@ "name": "Mold Indicator", "documentation": "https://www.home-assistant.io/integrations/mold_indicator", "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index adc0b05bab769..f543220b5b9eb 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -1,5 +1,4 @@ """The Monoprice 6-Zone Amplifier integration.""" -import asyncio import logging from pymonoprice import get_monoprice @@ -23,11 +22,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Monoprice 6-Zone Amplifier component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Monoprice 6-Zone Amplifier from a config entry.""" port = entry.data[CONF_PORT] @@ -54,25 +48,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): FIRST_RUN: first_run, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index a65fa8d23f36d..1261832c3712c 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -69,7 +69,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Monoprice 6-Zone Amplifier.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index 93cebc9d88546..2001531a396eb 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/monoprice", "requirements": ["pymonoprice==0.3"], "codeowners": ["@etsinko", "@OnFreund"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 4d6d337667e62..8b3de8903a3ec 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): first_run = hass.data[DOMAIN][config_entry.entry_id][FIRST_RUN] async_add_entities(entities, first_run) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() def _call_service(entities, service_call): for entity in entities: diff --git a/homeassistant/components/monoprice/services.yaml b/homeassistant/components/monoprice/services.yaml index a271d704768e8..93275fd2a1dc5 100644 --- a/homeassistant/components/monoprice/services.yaml +++ b/homeassistant/components/monoprice/services.yaml @@ -1,13 +1,15 @@ snapshot: + name: Snapshot description: Take a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be snapshot. Platform dependent. - example: "media_player.living_room" + target: + entity: + integration: monoprice + domain: media_player restore: + name: Restore description: Restore a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be restored. Platform dependent. - example: "media_player.living_room" + target: + entity: + integration: monoprice + domain: media_player diff --git a/homeassistant/components/monoprice/translations/zh-Hant.json b/homeassistant/components/monoprice/translations/zh-Hant.json index b54a678398036..75ed7f1563395 100644 --- a/homeassistant/components/monoprice/translations/zh-Hant.json +++ b/homeassistant/components/monoprice/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 8af5f40630c06..19fb952f59f01 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -3,5 +3,6 @@ "name": "Moon", "documentation": "https://www.home-assistant.io/integrations/moon", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 4b373469cc6df..6213e218d24d0 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -1,5 +1,5 @@ """Support for tracking the moon phases.""" -from astral import Astral +from astral import moon import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -48,7 +48,6 @@ def __init__(self, name): """Initialize the moon sensor.""" self._name = name self._state = None - self._astral = Astral() @property def name(self): @@ -87,4 +86,4 @@ def icon(self): async def async_update(self): """Get the time and updates the states.""" today = dt_util.as_local(dt_util.utcnow()).date() - self._state = self._astral.moon_phase(today) + self._state = moon.phase(today) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 73a27c90140b5..d2400beb4f563 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -1,5 +1,4 @@ """The motion_blinds component.""" -import asyncio from datetime import timedelta import logging from socket import timeout @@ -159,10 +158,7 @@ def stop_motion_multicast(event): sw_version=motion_gateway.protocol, ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -171,13 +167,8 @@ async def async_unload_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry ): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index 9aa62ca2d05c8..796911cef6e1d 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -25,7 +25,6 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Motion Blinds config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the Motion Blinds flow.""" diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 2c4fee5f8aa55..a802ecfb66729 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -116,7 +116,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_ABSOLUTE_POSITION, SET_ABSOLUTE_POSITION_SCHEMA, diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index c144dc99bc5b3..83007cf562c26 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "requirements": ["motionblinds==0.4.10"], - "codeowners": ["@starkillerOG"] + "codeowners": ["@starkillerOG"], + "iot_class": "local_push" } diff --git a/homeassistant/components/motion_blinds/services.yaml b/homeassistant/components/motion_blinds/services.yaml index f46cc94bd43a5..08ee4098e2739 100644 --- a/homeassistant/components/motion_blinds/services.yaml +++ b/homeassistant/components/motion_blinds/services.yaml @@ -1,14 +1,27 @@ # Describes the format for available motion blinds services set_absolute_position: + name: Set absolute position description: "Set the absolute position of the cover." + target: + entity: + integration: motion_blinds + domain: cover fields: - entity_id: - description: Name of the motion blind cover entity to control. - example: "cover.TopDownBottomUp-Bottom-0001" absolute_position: + name: Absolute position description: Absolute position to move to. + required: true example: 70 + selector: + number: + min: 1 + max: 100 width: - description: Optionally specify the width that is covered, only for TDBU Combined entities. + name: Width + description: Specify the width that is covered, only for TDBU Combined entities. example: 30 + selector: + number: + min: 1 + max: 100 diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index d922923d47248..4511b316cd681 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -1,6 +1,5 @@ { "config": { - "flow_title": "Motion Blinds", "step": { "user": { "title": "Motion Blinds", diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json index 01eba9c7ecd61..6c145898598b6 100644 --- a/homeassistant/components/motion_blinds/translations/de.json +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -5,6 +5,9 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "connection_error": "Verbindung fehlgeschlagen" }, + "error": { + "discovery_error": "Motion-Gateway konnte nicht gefunden werden" + }, "flow_title": "Jalousien", "step": { "connect": { diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 0f2f9881ebd09..1c538d7de146e 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "connection_error": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py new file mode 100644 index 0000000000000..f766bb86be22f --- /dev/null +++ b/homeassistant/components/motioneye/__init__.py @@ -0,0 +1,233 @@ +"""The motionEye integration.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, Callable + +from motioneye_client.client import ( + MotionEyeClient, + MotionEyeClientError, + MotionEyeClientInvalidAuthError, +) +from motioneye_client.const import KEY_CAMERAS, KEY_ID, KEY_NAME + +from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_ADMIN_PASSWORD, + CONF_ADMIN_USERNAME, + CONF_CLIENT, + CONF_COORDINATOR, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + MOTIONEYE_MANUFACTURER, + SIGNAL_CAMERA_ADD, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [CAMERA_DOMAIN] + + +def create_motioneye_client( + *args: Any, + **kwargs: Any, +) -> MotionEyeClient: + """Create a MotionEyeClient.""" + return MotionEyeClient(*args, **kwargs) + + +def get_motioneye_device_identifier( + config_entry_id: str, camera_id: int +) -> tuple[str, str]: + """Get the identifiers for a motionEye device.""" + return (DOMAIN, f"{config_entry_id}_{camera_id}") + + +def get_motioneye_entity_unique_id( + config_entry_id: str, camera_id: int, entity_type: str +) -> str: + """Get the unique_id for a motionEye entity.""" + return f"{config_entry_id}_{camera_id}_{entity_type}" + + +def get_camera_from_cameras( + camera_id: int, data: dict[str, Any] | None +) -> dict[str, Any] | None: + """Get an individual camera dict from a multiple cameras data response.""" + for camera in data.get(KEY_CAMERAS, []) if data else []: + if camera.get(KEY_ID) == camera_id: + val: dict[str, Any] = camera + return val + return None + + +def is_acceptable_camera(camera: dict[str, Any] | None) -> bool: + """Determine if a camera dict is acceptable.""" + return bool(camera and KEY_ID in camera and KEY_NAME in camera) + + +@callback +def listen_for_new_cameras( + hass: HomeAssistant, + entry: ConfigEntry, + add_func: Callable, +) -> None: + """Listen for new cameras.""" + + entry.async_on_unload( + async_dispatcher_connect( + hass, + SIGNAL_CAMERA_ADD.format(entry.entry_id), + add_func, + ) + ) + + +@callback +def _add_camera( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client: MotionEyeClient, + entry: ConfigEntry, + camera_id: int, + camera: dict[str, Any], + device_identifier: tuple[str, str], +) -> None: + """Add a motionEye camera to hass.""" + + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={device_identifier}, + manufacturer=MOTIONEYE_MANUFACTURER, + model=MOTIONEYE_MANUFACTURER, + name=camera[KEY_NAME], + ) + + async_dispatcher_send( + hass, + SIGNAL_CAMERA_ADD.format(entry.entry_id), + camera, + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up motionEye from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + client = create_motioneye_client( + entry.data[CONF_URL], + admin_username=entry.data.get(CONF_ADMIN_USERNAME), + admin_password=entry.data.get(CONF_ADMIN_PASSWORD), + surveillance_username=entry.data.get(CONF_SURVEILLANCE_USERNAME), + surveillance_password=entry.data.get(CONF_SURVEILLANCE_PASSWORD), + ) + + try: + await client.async_client_login() + except MotionEyeClientInvalidAuthError as exc: + await client.async_client_close() + raise ConfigEntryAuthFailed from exc + except MotionEyeClientError as exc: + await client.async_client_close() + raise ConfigEntryNotReady from exc + + @callback + async def async_update_data() -> dict[str, Any] | None: + try: + return await client.async_get_cameras() + except MotionEyeClientError as exc: + raise UpdateFailed("Error communicating with API") from exc + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + hass.data[DOMAIN][entry.entry_id] = { + CONF_CLIENT: client, + CONF_COORDINATOR: coordinator, + } + + current_cameras: set[tuple[str, str]] = set() + device_registry = await dr.async_get_registry(hass) + + @callback + def _async_process_motioneye_cameras() -> None: + """Process motionEye camera additions and removals.""" + inbound_camera: set[tuple[str, str]] = set() + if coordinator.data is None or KEY_CAMERAS not in coordinator.data: + return + + for camera in coordinator.data[KEY_CAMERAS]: + if not is_acceptable_camera(camera): + return + camera_id = camera[KEY_ID] + device_identifier = get_motioneye_device_identifier( + entry.entry_id, camera_id + ) + inbound_camera.add(device_identifier) + + if device_identifier in current_cameras: + continue + current_cameras.add(device_identifier) + _add_camera( + hass, + device_registry, + client, + entry, + camera_id, + camera, + device_identifier, + ) + + # Ensure every device associated with this config entry is still in the list of + # motionEye cameras, otherwise remove the device (and thus entities). + for device_entry in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ): + for identifier in device_entry.identifiers: + if identifier in inbound_camera: + break + else: + device_registry.async_remove_device(device_entry.id) + + async def setup_then_listen() -> None: + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, platform) + for platform in PLATFORMS + ] + ) + entry.async_on_unload( + coordinator.async_add_listener(_async_process_motioneye_cameras) + ) + await coordinator.async_refresh() + + hass.async_create_task(setup_then_listen()) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + config_data = hass.data[DOMAIN].pop(entry.entry_id) + await config_data[CONF_CLIENT].async_client_close() + + return unload_ok diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py new file mode 100644 index 0000000000000..e3cad73dfc587 --- /dev/null +++ b/homeassistant/components/motioneye/camera.py @@ -0,0 +1,209 @@ +"""The motionEye integration.""" +from __future__ import annotations + +import logging +from typing import Any, Dict, Optional + +import aiohttp +from motioneye_client.client import MotionEyeClient +from motioneye_client.const import ( + DEFAULT_SURVEILLANCE_USERNAME, + KEY_ID, + KEY_MOTION_DETECTION, + KEY_NAME, + KEY_STREAMING_AUTH_MODE, +) + +from homeassistant.components.mjpeg.camera import ( + CONF_MJPEG_URL, + CONF_STILL_IMAGE_URL, + CONF_VERIFY_SSL, + MjpegCamera, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import ( + get_camera_from_cameras, + get_motioneye_device_identifier, + get_motioneye_entity_unique_id, + is_acceptable_camera, + listen_for_new_cameras, +) +from .const import ( + CONF_CLIENT, + CONF_COORDINATOR, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DOMAIN, + MOTIONEYE_MANUFACTURER, + TYPE_MOTIONEYE_MJPEG_CAMERA, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["camera"] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up motionEye from a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + + @callback + def camera_add(camera: dict[str, Any]) -> None: + """Add a new motionEye camera.""" + async_add_entities( + [ + MotionEyeMjpegCamera( + entry.entry_id, + entry.data.get( + CONF_SURVEILLANCE_USERNAME, DEFAULT_SURVEILLANCE_USERNAME + ), + entry.data.get(CONF_SURVEILLANCE_PASSWORD, ""), + camera, + entry_data[CONF_CLIENT], + entry_data[CONF_COORDINATOR], + ) + ] + ) + + listen_for_new_cameras(hass, entry, camera_add) + + +class MotionEyeMjpegCamera(MjpegCamera, CoordinatorEntity[Optional[Dict[str, Any]]]): + """motionEye mjpeg camera.""" + + def __init__( + self, + config_entry_id: str, + username: str, + password: str, + camera: dict[str, Any], + client: MotionEyeClient, + coordinator: DataUpdateCoordinator[dict[str, Any] | None], + ) -> None: + """Initialize a MJPEG camera.""" + self._surveillance_username = username + self._surveillance_password = password + self._client = client + self._camera_id = camera[KEY_ID] + self._device_identifier = get_motioneye_device_identifier( + config_entry_id, self._camera_id + ) + self._unique_id = get_motioneye_entity_unique_id( + config_entry_id, self._camera_id, TYPE_MOTIONEYE_MJPEG_CAMERA + ) + self._motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False) + self._available = self._is_acceptable_streaming_camera(camera) + + # motionEye cameras are always streaming or unavailable. + self.is_streaming = True + + MjpegCamera.__init__( + self, + { + CONF_VERIFY_SSL: False, + **self._get_mjpeg_camera_properties_for_camera(camera), + }, + ) + CoordinatorEntity.__init__(self, coordinator) + + @callback + def _get_mjpeg_camera_properties_for_camera( + self, camera: dict[str, Any] + ) -> dict[str, Any]: + """Convert a motionEye camera to MjpegCamera internal properties.""" + auth = None + if camera.get(KEY_STREAMING_AUTH_MODE) in [ + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, + ]: + auth = camera[KEY_STREAMING_AUTH_MODE] + + return { + CONF_NAME: camera[KEY_NAME], + CONF_USERNAME: self._surveillance_username if auth is not None else None, + CONF_PASSWORD: self._surveillance_password if auth is not None else None, + CONF_MJPEG_URL: self._client.get_camera_stream_url(camera) or "", + CONF_STILL_IMAGE_URL: self._client.get_camera_snapshot_url(camera), + CONF_AUTHENTICATION: auth, + } + + @callback + def _set_mjpeg_camera_state_for_camera(self, camera: dict[str, Any]) -> None: + """Set the internal state to match the given camera.""" + + # Sets the state of the underlying (inherited) MjpegCamera based on the updated + # MotionEye camera dictionary. + properties = self._get_mjpeg_camera_properties_for_camera(camera) + self._name = properties[CONF_NAME] + self._username = properties[CONF_USERNAME] + self._password = properties[CONF_PASSWORD] + self._mjpeg_url = properties[CONF_MJPEG_URL] + self._still_image_url = properties[CONF_STILL_IMAGE_URL] + self._authentication = properties[CONF_AUTHENTICATION] + + if self._authentication == HTTP_BASIC_AUTHENTICATION: + self._auth = aiohttp.BasicAuth(self._username, password=self._password) + + @property + def unique_id(self) -> str: + """Return a unique id for this instance.""" + return self._unique_id + + @classmethod + def _is_acceptable_streaming_camera(cls, camera: dict[str, Any] | None) -> bool: + """Determine if a camera is streaming/usable.""" + return is_acceptable_camera(camera) and MotionEyeClient.is_camera_streaming( + camera + ) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._available + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + available = False + if self.coordinator.last_update_success: + camera = get_camera_from_cameras(self._camera_id, self.coordinator.data) + if self._is_acceptable_streaming_camera(camera): + assert camera + self._set_mjpeg_camera_state_for_camera(camera) + self._motion_detection_enabled = camera.get(KEY_MOTION_DETECTION, False) + available = True + self._available = available + super()._handle_coordinator_update() + + @property + def brand(self) -> str: + """Return the camera brand.""" + return MOTIONEYE_MANUFACTURER + + @property + def motion_detection_enabled(self) -> bool: + """Return the camera motion detection status.""" + return self._motion_detection_enabled + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + return {"identifiers": {self._device_identifier}} diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py new file mode 100644 index 0000000000000..a8562189d1f40 --- /dev/null +++ b/homeassistant/components/motioneye/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow for motionEye integration.""" +from __future__ import annotations + +import logging +from typing import Any, Dict, cast + +from motioneye_client.client import ( + MotionEyeClientConnectionError, + MotionEyeClientInvalidAuthError, + MotionEyeClientRequestError, +) +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow +from homeassistant.const import CONF_SOURCE, CONF_URL +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from . import create_motioneye_client +from .const import ( + CONF_ADMIN_PASSWORD, + CONF_ADMIN_USERNAME, + CONF_SURVEILLANCE_PASSWORD, + CONF_SURVEILLANCE_USERNAME, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for motionEye.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + + def _get_form( + user_input: dict[str, Any], errors: dict[str, str] | None = None + ) -> FlowResult: + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_URL, default=user_input.get(CONF_URL, "") + ): str, + vol.Optional( + CONF_ADMIN_USERNAME, + default=user_input.get(CONF_ADMIN_USERNAME), + ): str, + vol.Optional( + CONF_ADMIN_PASSWORD, + default=user_input.get(CONF_ADMIN_PASSWORD), + ): str, + vol.Optional( + CONF_SURVEILLANCE_USERNAME, + default=user_input.get(CONF_SURVEILLANCE_USERNAME), + ): str, + vol.Optional( + CONF_SURVEILLANCE_PASSWORD, + default=user_input.get(CONF_SURVEILLANCE_PASSWORD), + ): str, + } + ), + errors=errors, + ) + + reauth_entry = None + if self.context.get("entry_id"): + reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + + if user_input is None: + return _get_form( + cast(Dict[str, Any], reauth_entry.data) if reauth_entry else {} + ) + + try: + # Cannot use cv.url validation in the schema itself, so + # apply extra validation here. + cv.url(user_input[CONF_URL]) + except vol.Invalid: + return _get_form(user_input, {"base": "invalid_url"}) + + client = create_motioneye_client( + user_input[CONF_URL], + admin_username=user_input.get(CONF_ADMIN_USERNAME), + admin_password=user_input.get(CONF_ADMIN_PASSWORD), + surveillance_username=user_input.get(CONF_SURVEILLANCE_USERNAME), + surveillance_password=user_input.get(CONF_SURVEILLANCE_PASSWORD), + ) + + errors = {} + try: + await client.async_client_login() + except MotionEyeClientConnectionError: + errors["base"] = "cannot_connect" + except MotionEyeClientInvalidAuthError: + errors["base"] = "invalid_auth" + except MotionEyeClientRequestError: + errors["base"] = "unknown" + finally: + await client.async_client_close() + + if errors: + return _get_form(user_input, errors) + + if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and reauth_entry is not None: + self.hass.config_entries.async_update_entry(reauth_entry, data=user_input) + # Need to manually reload, as the listener won't have been + # installed because the initial load did not succeed (the reauth + # flow will not be initiated if the load succeeds). + await self.hass.config_entries.async_reload(reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + # Search for duplicates: there isn't a useful unique_id, but + # at least prevent entries with the same motionEye URL. + self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) + + return self.async_create_entry( + title=f"{user_input[CONF_URL]}", + data=user_input, + ) + + async def async_step_reauth( + self, + config_data: dict[str, Any] | None = None, + ) -> FlowResult: + """Handle a reauthentication flow.""" + return await self.async_step_user(config_data) diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py new file mode 100644 index 0000000000000..fbd0d9b4d2e9a --- /dev/null +++ b/homeassistant/components/motioneye/const.py @@ -0,0 +1,19 @@ +"""Constants for the motionEye integration.""" +from datetime import timedelta + +DOMAIN = "motioneye" + +CONF_CLIENT = "client" +CONF_COORDINATOR = "coordinator" +CONF_ADMIN_PASSWORD = "admin_password" +CONF_ADMIN_USERNAME = "admin_username" +CONF_SURVEILLANCE_USERNAME = "surveillance_username" +CONF_SURVEILLANCE_PASSWORD = "surveillance_password" +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +MOTIONEYE_MANUFACTURER = "motionEye" + +SIGNAL_CAMERA_ADD = f"{DOMAIN}_camera_add_signal." "{}" +SIGNAL_CAMERA_REMOVE = f"{DOMAIN}_camera_remove_signal." "{}" + +TYPE_MOTIONEYE_MJPEG_CAMERA = "motioneye_mjpeg_camera" diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json new file mode 100644 index 0000000000000..43cb231c30cdd --- /dev/null +++ b/homeassistant/components/motioneye/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "motioneye", + "name": "motionEye", + "documentation": "https://www.home-assistant.io/integrations/motioneye", + "config_flow": true, + "requirements": [ + "motioneye-client==0.3.6" + ], + "codeowners": [ + "@dermotduffy" + ], + "iot_class": "local_polling" +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/strings.json b/homeassistant/components/motioneye/strings.json new file mode 100644 index 0000000000000..d365ba272ea43 --- /dev/null +++ b/homeassistant/components/motioneye/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "admin_username": "Admin [%key:common::config_flow::data::username%]", + "admin_password": "Admin [%key:common::config_flow::data::password%]", + "surveillance_username": "Surveillance [%key:common::config_flow::data::username%]", + "surveillance_password": "Surveillance [%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%]", + "invalid_url": "Invalid URL" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/motioneye/translations/ca.json b/homeassistant/components/motioneye/translations/ca.json new file mode 100644 index 0000000000000..65ce7e48781c2 --- /dev/null +++ b/homeassistant/components/motioneye/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_url": "URL inv\u00e0lid", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "admin_password": "Contrasenya d'administrador", + "admin_username": "Nom d'usuari d'usuari", + "surveillance_password": "Contrasenya de vigilant", + "surveillance_username": "Nom d'usuari de vigilant", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/cs.json b/homeassistant/components/motioneye/translations/cs.json new file mode 100644 index 0000000000000..311a1d4d96565 --- /dev/null +++ b/homeassistant/components/motioneye/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "invalid_url": "Neplatn\u00e1 URL adresa", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/de.json b/homeassistant/components/motioneye/translations/de.json new file mode 100644 index 0000000000000..94b86f04b2d33 --- /dev/null +++ b/homeassistant/components/motioneye/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_url": "Ung\u00fcltige URL", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Passwort", + "admin_username": "Admin Benutzername", + "surveillance_password": "Surveillance Passwort", + "surveillance_username": "Surveillance Benutzername", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/en.json b/homeassistant/components/motioneye/translations/en.json new file mode 100644 index 0000000000000..dd4f337e9f974 --- /dev/null +++ b/homeassistant/components/motioneye/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_url": "Invalid URL", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Password", + "admin_username": "Admin Username", + "surveillance_password": "Surveillance Password", + "surveillance_username": "Surveillance Username", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json new file mode 100644 index 0000000000000..4f749d5c6d8a9 --- /dev/null +++ b/homeassistant/components/motioneye/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_url": "URL no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "admin_password": "Contrase\u00f1a administrador", + "admin_username": "Usuario administrador", + "surveillance_password": "Contrase\u00f1a vigilancia", + "surveillance_username": "Usuario vigilancia", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/et.json b/homeassistant/components/motioneye/translations/et.json new file mode 100644 index 0000000000000..c3e44c5297490 --- /dev/null +++ b/homeassistant/components/motioneye/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendumine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_url": "Sobimatu URL", + "unknown": "Tundmatu viga" + }, + "step": { + "user": { + "data": { + "admin_password": "Haldaja salas\u00f5na", + "admin_username": "Haldaja kasutajanimi", + "surveillance_password": "J\u00e4relvalve salas\u00f5na", + "surveillance_username": "J\u00e4relvalve kasutajanimi", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/fr.json b/homeassistant/components/motioneye/translations/fr.json new file mode 100644 index 0000000000000..a520c05dba29a --- /dev/null +++ b/homeassistant/components/motioneye/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_url": "URL invalide" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Mot de passe", + "admin_username": "Admin Nom d'utilisateur", + "surveillance_password": "Surveillance Mot de passe", + "surveillance_username": "Surveillance Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/it.json b/homeassistant/components/motioneye/translations/it.json new file mode 100644 index 0000000000000..af07fac1a943b --- /dev/null +++ b/homeassistant/components/motioneye/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_url": "URL non valido", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "admin_password": "Amministratore Password", + "admin_username": "Amministratore Nome utente", + "surveillance_password": "Sorveglianza Password", + "surveillance_username": "Sorveglianza Nome utente", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/nl.json b/homeassistant/components/motioneye/translations/nl.json new file mode 100644 index 0000000000000..07d8dc71a103d --- /dev/null +++ b/homeassistant/components/motioneye/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_url": "Ongeldige URL", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Wachtwoord", + "admin_username": "Admin Gebruikersnaam", + "surveillance_password": "Surveillance Wachtwoord", + "surveillance_username": "Surveillance Gebruikersnaam", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/no.json b/homeassistant/components/motioneye/translations/no.json new file mode 100644 index 0000000000000..5b7f6538bb89a --- /dev/null +++ b/homeassistant/components/motioneye/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_url": "Ugyldig URL-adresse", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin Passord", + "admin_username": "Administrator Brukernavn", + "surveillance_password": "Overv\u00e5king Passord", + "surveillance_username": "Overv\u00e5king Brukernavn", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/pl.json b/homeassistant/components/motioneye/translations/pl.json new file mode 100644 index 0000000000000..dca40bdcd3d19 --- /dev/null +++ b/homeassistant/components/motioneye/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_url": "Nieprawid\u0142owy adres URL", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "admin_password": "Has\u0142o admina", + "admin_username": "Nazwa u\u017cytkownika admina", + "surveillance_password": "Has\u0142o podgl\u0105du", + "surveillance_username": "[%key::common::config_flow::data::username%] podgl\u0105du", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/ru.json b/homeassistant/components/motioneye/translations/ru.json new file mode 100644 index 0000000000000..a983ddcae0f6b --- /dev/null +++ b/homeassistant/components/motioneye/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "admin_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "admin_username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "surveillance_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f", + "surveillance_username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u044f", + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/zh-Hant.json b/homeassistant/components/motioneye/translations/zh-Hant.json new file mode 100644 index 0000000000000..aa05784e53da0 --- /dev/null +++ b/homeassistant/components/motioneye/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_url": "\u7db2\u5740\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin \u5bc6\u78bc", + "admin_username": "Admin \u4f7f\u7528\u8005\u540d\u7a31", + "surveillance_password": "Surveillance \u5bc6\u78bc", + "surveillance_username": "Surveillance \u4f7f\u7528\u8005\u540d\u7a31", + "url": "\u7db2\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mpchc/manifest.json b/homeassistant/components/mpchc/manifest.json index 2ff6793151856..a1a9e769be617 100644 --- a/homeassistant/components/mpchc/manifest.json +++ b/homeassistant/components/mpchc/manifest.json @@ -2,5 +2,6 @@ "domain": "mpchc", "name": "Media Player Classic Home Cinema (MPC-HC)", "documentation": "https://www.home-assistant.io/integrations/mpchc", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index a11b9fedd801c..39b4e45196b76 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -3,5 +3,6 @@ "name": "Music Player Daemon (MPD)", "documentation": "https://www.home-assistant.io/integrations/mpd", "requirements": ["python-mpd2==3.0.4"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ce2d413e1b69c..16379aa79235e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -31,11 +31,18 @@ EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback +from homeassistant.core import ( + CoreState, + Event, + HassJob, + HomeAssistant, + ServiceCall, + callback, +) from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType +from homeassistant.helpers.typing import ConfigType, ServiceDataType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe @@ -245,7 +252,7 @@ def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType: @bind_hass -def publish(hass: HomeAssistantType, topic, payload, qos=None, retain=None) -> None: +def publish(hass: HomeAssistant, topic, payload, qos=None, retain=None) -> None: """Publish message to an MQTT topic.""" hass.add_job(async_publish, hass, topic, payload, qos, retain) @@ -253,7 +260,7 @@ def publish(hass: HomeAssistantType, topic, payload, qos=None, retain=None) -> N @callback @bind_hass def async_publish( - hass: HomeAssistantType, topic: Any, payload, qos=None, retain=None + hass: HomeAssistant, topic: Any, payload, qos=None, retain=None ) -> None: """Publish message to an MQTT topic.""" data = _build_publish_data(topic, qos, retain) @@ -263,7 +270,7 @@ def async_publish( @bind_hass def publish_template( - hass: HomeAssistantType, topic, payload_template, qos=None, retain=None + hass: HomeAssistant, topic, payload_template, qos=None, retain=None ) -> None: """Publish message to an MQTT topic.""" hass.add_job(async_publish_template, hass, topic, payload_template, qos, retain) @@ -271,7 +278,7 @@ def publish_template( @bind_hass def async_publish_template( - hass: HomeAssistantType, topic, payload_template, qos=None, retain=None + hass: HomeAssistant, topic, payload_template, qos=None, retain=None ) -> None: """Publish message to an MQTT topic using a template payload.""" data = _build_publish_data(topic, qos, retain) @@ -308,7 +315,7 @@ def wrapper(msg: Any) -> None: @bind_hass async def async_subscribe( - hass: HomeAssistantType, + hass: HomeAssistant, topic: str, msg_callback: MessageCallbackType, qos: int = DEFAULT_QOS, @@ -353,7 +360,7 @@ async def async_subscribe( @bind_hass def subscribe( - hass: HomeAssistantType, + hass: HomeAssistant, topic: str, msg_callback: MessageCallbackType, qos: int = DEFAULT_QOS, @@ -372,7 +379,7 @@ def remove(): async def _async_setup_discovery( - hass: HomeAssistantType, conf: ConfigType, config_entry + hass: HomeAssistant, conf: ConfigType, config_entry ) -> bool: """Try to start the discovery of MQTT devices. @@ -385,7 +392,7 @@ async def _async_setup_discovery( return success -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the MQTT protocol service.""" conf: ConfigType | None = config.get(DOMAIN) @@ -542,7 +549,7 @@ class MQTT: def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, config_entry, conf, ) -> None: diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 78d0434e4128b..a16d721ba7c59 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -25,6 +25,8 @@ "chrg_t": "charging_topic", "chrg_tpl": "charging_template", "clrm": "color_mode", + "clrm_stat_t": "color_mode_state_topic", + "clrm_val_tpl": "color_mode_value_template", "clr_temp_cmd_t": "color_temp_command_topic", "clr_temp_stat_t": "color_temp_state_topic", "clr_temp_tpl": "color_temp_template", @@ -75,6 +77,8 @@ "json_attr": "json_attributes", "json_attr_t": "json_attributes_topic", "json_attr_tpl": "json_attributes_template", + "max": "max", + "min": "min", "max_mirs": "max_mireds", "min_mirs": "min_mireds", "max_temp": "max_temp", @@ -119,6 +123,8 @@ "pl_osc_off": "payload_oscillation_off", "pl_osc_on": "payload_oscillation_on", "pl_paus": "payload_pause", + "pl_rst_pct": "payload_reset_percentage", + "pl_rst_pr_mode": "payload_reset_preset_mode", "pl_stop": "payload_stop", "pl_strt": "payload_start", "pl_stpa": "payload_start_pause", @@ -142,6 +148,14 @@ "rgb_cmd_t": "rgb_command_topic", "rgb_stat_t": "rgb_state_topic", "rgb_val_tpl": "rgb_value_template", + "rgbw_cmd_tpl": "rgbw_command_template", + "rgbw_cmd_t": "rgbw_command_topic", + "rgbw_stat_t": "rgbw_state_topic", + "rgbw_val_tpl": "rgbw_value_template", + "rgbww_cmd_tpl": "rgbww_command_template", + "rgbww_cmd_t": "rgbww_command_topic", + "rgbww_stat_t": "rgbww_state_topic", + "rgbww_val_tpl": "rgbww_value_template", "send_cmd_t": "send_command_topic", "send_if_off": "send_if_off", "set_fan_spd_t": "set_fan_speed_topic", @@ -156,6 +170,7 @@ "spd_val_tpl": "speed_value_template", "spds": "speeds", "src_type": "source_type", + "stat_cla": "state_class", "stat_clsd": "state_closed", "stat_closing": "state_closing", "stat_off": "state_off", @@ -168,6 +183,7 @@ "stat_t": "state_topic", "stat_tpl": "state_template", "stat_val_tpl": "state_value_template", + "step": "step", "stype": "subtype", "sup_feat": "supported_features", "sup_clrm": "supported_color_modes", diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 0f10e91e41ca9..1e7ccf5bb4c42 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -26,10 +26,10 @@ STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -87,7 +87,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT alarm control panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index fbd5e7535c519..e24abc2702877 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -18,12 +18,12 @@ CONF_PAYLOAD_ON, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription @@ -59,7 +59,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT binary sensor through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 0a1a35b2ddd4a..0a9f37ac9ea30 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -6,10 +6,10 @@ from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.const import CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import CONF_QOS, DOMAIN, PLATFORMS, subscription from .. import mqtt @@ -28,7 +28,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT camera through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 8ab7a9ca3cfb6..da0ed485b7236 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -46,10 +46,10 @@ PRECISION_WHOLE, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_QOS, @@ -251,7 +251,7 @@ async def async_setup_platform( - hass: HomeAssistantType, async_add_entities, config: ConfigType, discovery_info=None + hass: HomeAssistant, async_add_entities, config: ConfigType, discovery_info=None ): """Set up MQTT climate device through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -359,7 +359,7 @@ def _setup_from_config(self, config): tpl.hass = self.hass self._command_templates = command_templates - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} qos = self._config[CONF_QOS] diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 5e5b8c54cf249..c6af0cc08b5ba 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -32,12 +32,10 @@ from .util import MQTT_WILL_BIRTH_SCHEMA -@config_entries.HANDLERS.register(DOMAIN) -class FlowHandler(config_entries.ConfigFlow): +class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH _hassio_discovery = None @@ -67,6 +65,7 @@ async def async_step_broker(self, user_input=None): ) if can_connect: + user_input[CONF_DISCOVERY] = DEFAULT_DISCOVERY return self.async_create_entry( title=user_input[CONF_BROKER], data=user_input ) @@ -78,7 +77,6 @@ async def async_step_broker(self, user_input=None): fields[vol.Required(CONF_PORT, default=1883)] = vol.Coerce(int) fields[vol.Optional(CONF_USERNAME)] = str fields[vol.Optional(CONF_PASSWORD)] = str - fields[vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY)] = bool return self.async_show_form( step_id="broker", data_schema=vol.Schema(fields), errors=errors @@ -128,7 +126,7 @@ async def async_step_hassio_confirm(self, user_input=None): CONF_USERNAME: data.get(CONF_USERNAME), CONF_PASSWORD: data.get(CONF_PASSWORD), CONF_PROTOCOL: data.get(CONF_PROTOCOL), - CONF_DISCOVERY: user_input[CONF_DISCOVERY], + CONF_DISCOVERY: DEFAULT_DISCOVERY, }, ) @@ -137,9 +135,6 @@ async def async_step_hassio_confirm(self, user_input=None): return self.async_show_form( step_id="hassio_confirm", description_placeholders={"addon": self._hassio_discovery["addon"]}, - data_schema=vol.Schema( - {vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): bool} - ), errors=errors, ) @@ -158,7 +153,7 @@ async def async_step_init(self, user_input=None): return await self.async_step_broker() async def async_step_broker(self, user_input=None): - """Manage the MQTT options.""" + """Manage the MQTT broker configuration.""" errors = {} current_config = self.config_entry.data yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) @@ -201,6 +196,7 @@ async def async_step_broker(self, user_input=None): step_id="broker", data_schema=vol.Schema(fields), errors=errors, + last_step=False, ) async def async_step_options(self, user_input=None): @@ -321,6 +317,7 @@ async def async_step_options(self, user_input=None): step_id="options", data_schema=vol.Schema(fields), errors=errors, + last_step=True, ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 010f751dad47c..a8de06ff1ca87 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -1,5 +1,6 @@ """Support for MQTT cover devices.""" import functools +from json import JSONDecodeError, loads as json_loads import logging import voluptuous as vol @@ -30,10 +31,10 @@ STATE_OPENING, STATE_UNKNOWN, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -70,7 +71,6 @@ CONF_STATE_OPENING = "state_opening" CONF_STATE_STOPPED = "state_stopped" CONF_TILT_CLOSED_POSITION = "tilt_closed_value" -CONF_TILT_INVERT_STATE = "tilt_invert_state" CONF_TILT_MAX = "tilt_max" CONF_TILT_MIN = "tilt_min" CONF_TILT_OPEN_POSITION = "tilt_opened_value" @@ -89,7 +89,6 @@ DEFAULT_RETAIN = False DEFAULT_STATE_STOPPED = "stopped" DEFAULT_TILT_CLOSED_POSITION = 0 -DEFAULT_TILT_INVERT_STATE = False DEFAULT_TILT_MAX = 100 DEFAULT_TILT_MIN = 0 DEFAULT_TILT_OPEN_POSITION = 100 @@ -111,25 +110,34 @@ def validate_options(value): """ if CONF_SET_POSITION_TOPIC in value and CONF_GET_POSITION_TOPIC not in value: raise vol.Invalid( - "'set_position_topic' must be set together with 'position_topic'." + f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) - if ( - CONF_GET_POSITION_TOPIC in value - and CONF_STATE_TOPIC not in value - and CONF_VALUE_TEMPLATE in value - ): - _LOGGER.warning( - "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'" + # if templates are set make sure the topic for the template is also set + + if CONF_VALUE_TEMPLATE in value and CONF_STATE_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." ) - if CONF_TILT_INVERT_STATE in value: - _LOGGER.warning( - "'tilt_invert_state' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please invert tilt using 'tilt_min' & 'tilt_max'" + if CONF_GET_POSITION_TEMPLATE in value and CONF_GET_POSITION_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." + ) + + if CONF_SET_POSITION_TEMPLATE in value and CONF_SET_POSITION_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." + ) + + if CONF_TILT_COMMAND_TEMPLATE in value and CONF_TILT_COMMAND_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." + ) + + if CONF_TILT_STATUS_TEMPLATE in value and CONF_TILT_STATUS_TOPIC not in value: + raise vol.Invalid( + f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." ) return value @@ -163,7 +171,6 @@ def validate_options(value): CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION ): int, vol.Optional(CONF_TILT_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TILT_INVERT_STATE): cv.boolean, vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int, vol.Optional(CONF_TILT_MIN, default=DEFAULT_TILT_MIN): int, vol.Optional( @@ -184,7 +191,7 @@ def validate_options(value): async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT cover through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -252,7 +259,7 @@ def _setup_from_config(self, config): if tilt_status_template is not None: tilt_status_template.hass = self.hass - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -261,41 +268,40 @@ async def _subscribe_topics(self): def tilt_message_received(msg): """Handle tilt updates.""" payload = msg.payload - tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) - if tilt_status_template is not None: - payload = tilt_status_template.async_render_with_possible_json_value( - payload + template = self._config.get(CONF_TILT_STATUS_TEMPLATE) + if template is not None: + variables = { + "entity_id": self.entity_id, + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + payload = template.async_render_with_possible_json_value( + payload, variables=variables ) - if not payload.isnumeric(): - _LOGGER.warning("Payload '%s' is not numeric", payload) - elif ( - self._config[CONF_TILT_MIN] - <= int(payload) - <= self._config[CONF_TILT_MAX] - or self._config[CONF_TILT_MAX] - <= int(payload) - <= self._config[CONF_TILT_MIN] - ): - level = self.find_percentage_in_range(float(payload)) - self._tilt_value = level - self.async_write_ha_state() - else: - _LOGGER.warning( - "Payload '%s' is out of range, must be between '%s' and '%s' inclusive", - payload, - self._config[CONF_TILT_MIN], - self._config[CONF_TILT_MAX], - ) + if not payload: + _LOGGER.debug("Ignoring empty tilt message from '%s'", msg.topic) + return + + self.tilt_payload_received(payload) @callback @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload - value_template = self._config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: - payload = value_template.async_render_with_possible_json_value(payload) + template = self._config.get(CONF_VALUE_TEMPLATE) + if template is not None: + variables = {"entity_id": self.entity_id} + payload = template.async_render_with_possible_json_value( + payload, variables=variables + ) + + if not payload: + _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) + return if payload == self._config[CONF_STATE_STOPPED]: if self._config.get(CONF_GET_POSITION_TOPIC) is not None: @@ -332,29 +338,58 @@ def position_message_received(msg): payload = msg.payload template = self._config.get(CONF_GET_POSITION_TEMPLATE) - - # To be removed in 2021.6: - # allow using `value_template` as position template if no `state_topic` - if template is None and self._config.get(CONF_STATE_TOPIC) is None: - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: - payload = template.async_render_with_possible_json_value(payload) + variables = { + "entity_id": self.entity_id, + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + payload = template.async_render_with_possible_json_value( + payload, variables=variables + ) - if payload.isnumeric(): + if not payload: + _LOGGER.debug( + "Ignoring empty position message from '%s'", msg.topic + ) + return + + try: + payload = json_loads(payload) + except JSONDecodeError: + pass + + if isinstance(payload, dict): + if "position" not in payload: + _LOGGER.warning( + "Template (position_template) returned JSON without position attribute" + ) + return + if "tilt_position" in payload: + if not self._config.get(CONF_TILT_STATE_OPTIMISTIC): + # reset forced set tilt optimistic + self._tilt_optimistic = False + self.tilt_payload_received(payload["tilt_position"]) + payload = payload["position"] + + try: percentage_payload = self.find_percentage_in_range( float(payload), COVER_PAYLOAD ) - self._position = percentage_payload - if self._config.get(CONF_STATE_TOPIC) is None: - self._state = ( - STATE_CLOSED - if percentage_payload == DEFAULT_POSITION_CLOSED - else STATE_OPEN - ) - else: + except ValueError: _LOGGER.warning("Payload '%s' is not numeric", payload) return + + self._position = percentage_payload + if self._config.get(CONF_STATE_TOPIC) is None: + self._state = ( + STATE_CLOSED + if percentage_payload == DEFAULT_POSITION_CLOSED + else STATE_OPEN + ) + self.async_write_ha_state() if self._config.get(CONF_GET_POSITION_TOPIC): @@ -379,6 +414,7 @@ def position_message_received(msg): self._optimistic = True if self._config.get(CONF_TILT_STATUS_TOPIC) is None: + # Force into optimistic tilt mode. self._tilt_optimistic = True else: self._tilt_value = STATE_UNKNOWN @@ -538,12 +574,21 @@ async def async_close_cover_tilt(self, **kwargs): async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - set_tilt_template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) + template = self._config.get(CONF_TILT_COMMAND_TEMPLATE) tilt = kwargs[ATTR_TILT_POSITION] percentage_tilt = tilt tilt = self.find_in_range_from_percent(tilt) - if set_tilt_template is not None: - tilt = set_tilt_template.async_render(parse_result=False, **kwargs) + # Handover the tilt after calculated from percent would make it more consistent with receiving templates + if template is not None: + variables = { + "tilt_position": percentage_tilt, + "entity_id": self.entity_id, + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + tilt = template.async_render(parse_result=False, variables=variables) mqtt.async_publish( self.hass, @@ -553,17 +598,26 @@ async def async_set_cover_tilt_position(self, **kwargs): self._config[CONF_RETAIN], ) if self._tilt_optimistic: + _LOGGER.debug("Set tilt value optimistic") self._tilt_value = percentage_tilt self.async_write_ha_state() async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE) + template = self._config.get(CONF_SET_POSITION_TEMPLATE) position = kwargs[ATTR_POSITION] percentage_position = position position = self.find_in_range_from_percent(position, COVER_PAYLOAD) - if set_position_template is not None: - position = set_position_template.async_render(parse_result=False, **kwargs) + if template is not None: + variables = { + "position": percentage_position, + "entity_id": self.entity_id, + "position_open": self._config[CONF_POSITION_OPEN], + "position_closed": self._config[CONF_POSITION_CLOSED], + "tilt_min": self._config[CONF_TILT_MIN], + "tilt_max": self._config[CONF_TILT_MAX], + } + position = template.async_render(parse_result=False, variables=variables) mqtt.async_publish( self.hass, @@ -611,8 +665,7 @@ def find_percentage_in_range(self, position, range_type=TILT_PAYLOAD): max_percent = 100 min_percent = 0 position_percentage = min(max(position_percentage, min_percent), max_percent) - if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE): - return 100 - position_percentage + return position_percentage def find_in_range_from_percent(self, percentage, range_type=TILT_PAYLOAD): @@ -635,6 +688,30 @@ def find_in_range_from_percent(self, percentage, range_type=TILT_PAYLOAD): position = round(current_range * (percentage / 100.0)) position += offset - if range_type == TILT_PAYLOAD and self._config.get(CONF_TILT_INVERT_STATE): - position = max_range - position + offset return position + + def tilt_payload_received(self, _payload): + """Set the tilt value.""" + + try: + payload = int(round(float(_payload))) + except ValueError: + _LOGGER.warning("Payload '%s' is not numeric", _payload) + return + + if ( + self._config[CONF_TILT_MIN] <= int(payload) <= self._config[CONF_TILT_MAX] + or self._config[CONF_TILT_MAX] + <= int(payload) + <= self._config[CONF_TILT_MIN] + ): + level = self.find_percentage_in_range(payload) + self._tilt_value = level + self.async_write_ha_state() + else: + _LOGGER.warning( + "Payload '%s' is out of range, must be between '%s' and '%s' inclusive", + payload, + self._config[CONF_TILT_MIN], + self._config[CONF_TILT_MAX], + ) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 52aeb20e3aa4b..d00d65c24514d 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -3,7 +3,7 @@ from functools import wraps from typing import Any -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC from .models import MessageCallbackType @@ -12,7 +12,7 @@ STORED_MESSAGES = 10 -def log_messages(hass: HomeAssistantType, entity_id: str) -> MessageCallbackType: +def log_messages(hass: HomeAssistant, entity_id: str) -> MessageCallbackType: """Wrap an MQTT message callback to support message logging.""" def _log_message(msg): @@ -159,7 +159,7 @@ async def info_for_device(hass, device_id): ) for trigger in mqtt_debug_info["triggers"].values(): - if trigger["device_id"] != device_id: + if trigger["device_id"] != device_id or trigger["discovery_data"] is None: continue discovery_data = { diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 1e058162bc3fb..038e6e9152347 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -24,7 +24,7 @@ async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import CONF_PAYLOAD, CONF_QOS, DOMAIN, debug_info, trigger as mqtt_trigger from .. import mqtt @@ -120,7 +120,7 @@ class Trigger: device_id: str = attr.ib() discovery_data: dict = attr.ib() - hass: HomeAssistantType = attr.ib() + hass: HomeAssistant = attr.ib() payload: str = attr.ib() qos: int = attr.ib() remove_signal: Callable[[], None] = attr.ib() diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 347166fdb82ce..3a5a3cb5f87fb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -8,11 +8,11 @@ import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -79,8 +79,8 @@ class MQTTConfig(dict): """Dummy class to allow adding attributes.""" -async def async_start( - hass: HomeAssistantType, discovery_topic, config_entry=None +async def async_start( # noqa: C901 + hass: HomeAssistant, discovery_topic, config_entry=None ) -> bool: """Start MQTT Discovery.""" mqtt_integrations = {} @@ -295,7 +295,7 @@ async def async_integration_message_received(integration, msg): return True -async def async_stop(hass: HomeAssistantType) -> bool: +async def async_stop(hass: HomeAssistant) -> bool: """Stop MQTT Discovery.""" if DISCOVERY_UNSUBSCRIBE in hass.data: for unsub in hass.data[DISCOVERY_UNSUBSCRIBE]: diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 24c4c805dfda2..5cd924551f7f5 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -28,10 +28,10 @@ CONF_PAYLOAD_ON, CONF_STATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util.percentage import ( int_states_in_range, ordered_list_item_to_percentage, @@ -59,6 +59,7 @@ CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template" CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template" +CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage" CONF_SPEED_RANGE_MIN = "speed_range_min" CONF_SPEED_RANGE_MAX = "speed_range_max" CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic" @@ -66,6 +67,7 @@ CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" CONF_PRESET_MODES_LIST = "preset_modes" +CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode" CONF_SPEED_STATE_TOPIC = "speed_state_topic" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" CONF_SPEED_VALUE_TEMPLATE = "speed_value_template" @@ -84,6 +86,7 @@ DEFAULT_NAME = "MQTT Fan" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_PAYLOAD_RESET = "None" DEFAULT_OPTIMISTIC = False DEFAULT_SPEED_RANGE_MIN = 1 DEFAULT_SPEED_RANGE_MAX = 100 @@ -113,6 +116,13 @@ def valid_speed_range_configuration(config): return config +def valid_preset_mode_configuration(config): + """Validate that the preset mode reset payload is not one of the preset modes.""" + if config.get(CONF_PAYLOAD_RESET_PRESET_MODE) in config.get(CONF_PRESET_MODES_LIST): + raise ValueError("preset_modes must not contain payload_reset_preset_mode") + return config + + PLATFORM_SCHEMA = vol.All( # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, CONF_SPEED_LIST and # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, @@ -153,6 +163,12 @@ def valid_speed_range_configuration(config): vol.Optional( CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX ): cv.positive_int, + vol.Optional( + CONF_PAYLOAD_RESET_PERCENTAGE, default=DEFAULT_PAYLOAD_RESET + ): cv.string, + vol.Optional( + CONF_PAYLOAD_RESET_PRESET_MODE, default=DEFAULT_PAYLOAD_RESET + ): cv.string, vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string, @@ -177,11 +193,12 @@ def valid_speed_range_configuration(config): ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), valid_fan_speed_configuration, valid_speed_range_configuration, + valid_preset_mode_configuration, ) async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT fan through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -281,6 +298,8 @@ def _setup_from_config(self, config): "SPEED_MEDIUM": config[CONF_PAYLOAD_MEDIUM_SPEED], "SPEED_HIGH": config[CONF_PAYLOAD_HIGH_SPEED], "SPEED_OFF": config[CONF_PAYLOAD_OFF_SPEED], + "PERCENTAGE_RESET": config[CONF_PAYLOAD_RESET_PERCENTAGE], + "PRESET_MODE_RESET": config[CONF_PAYLOAD_RESET_PRESET_MODE], } # The use of legacy speeds is deprecated in the schema, support will be removed after a quarter (2021.7) self._feature_legacy_speeds = not self._topic[CONF_SPEED_COMMAND_TOPIC] is None @@ -335,7 +354,7 @@ def _setup_from_config(self, config): tpl.hass = self.hass tpl_dict[key] = tpl.async_render_with_possible_json_value - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -344,6 +363,9 @@ async def _subscribe_topics(self): def state_received(msg): """Handle new received MQTT message.""" payload = self._value_templates[CONF_STATE](msg.payload) + if not payload: + _LOGGER.debug("Ignoring empty state from '%s'", msg.topic) + return if payload == self._payload["STATE_ON"]: self._state = True elif payload == self._payload["STATE_OFF"]: @@ -361,23 +383,35 @@ def state_received(msg): @log_messages(self.hass, self.entity_id) def percentage_received(msg): """Handle new received MQTT message for the percentage.""" - numeric_val_str = self._value_templates[ATTR_PERCENTAGE](msg.payload) + rendered_percentage_payload = self._value_templates[ATTR_PERCENTAGE]( + msg.payload + ) + if not rendered_percentage_payload: + _LOGGER.debug("Ignoring empty speed from '%s'", msg.topic) + return + if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]: + self._percentage = None + self._speed = None + self.async_write_ha_state() + return try: percentage = ranged_value_to_percentage( - self._speed_range, int(numeric_val_str) + self._speed_range, int(rendered_percentage_payload) ) except ValueError: _LOGGER.warning( - "'%s' received on topic %s is not a valid speed within the speed range", + "'%s' received on topic %s. '%s' is not a valid speed within the speed range", msg.payload, msg.topic, + rendered_percentage_payload, ) return if percentage < 0 or percentage > 100: _LOGGER.warning( - "'%s' received on topic %s is not a valid speed within the speed range", + "'%s' received on topic %s. '%s' is not a valid speed within the speed range", msg.payload, msg.topic, + rendered_percentage_payload, ) return self._percentage = percentage @@ -396,11 +430,19 @@ def percentage_received(msg): def preset_mode_received(msg): """Handle new received MQTT message for preset mode.""" preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload) + if preset_mode == self._payload["PRESET_MODE_RESET"]: + self._preset_mode = None + self.async_write_ha_state() + return + if not preset_mode: + _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) + return if preset_mode not in self.preset_modes: _LOGGER.warning( - "'%s' received on topic %s is not a valid preset mode", + "'%s' received on topic %s. '%s' is not a valid preset mode", msg.payload, msg.topic, + preset_mode, ) return @@ -436,9 +478,10 @@ def speed_received(msg): self._speed = speed else: _LOGGER.warning( - "'%s' received on topic %s is not a valid speed", + "'%s' received on topic %s. '%s' is not a valid speed", msg.payload, msg.topic, + speed, ) return @@ -464,6 +507,9 @@ def speed_received(msg): def oscillation_received(msg): """Handle new received MQTT message for the oscillation.""" payload = self._value_templates[ATTR_OSCILLATING](msg.payload) + if not payload: + _LOGGER.debug("Ignoring empty oscillation from '%s'", msg.topic) + return if payload == self._payload["OSCILLATE_ON_PAYLOAD"]: self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: @@ -642,7 +688,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: if self._optimistic_preset_mode: self._preset_mode = preset_mode - self.async_write_ha_state() + self.async_write_ha_state() # async_set_speed is deprecated, support will be removed after a quarter (2021.7) async def async_set_speed(self, speed: str) -> None: @@ -675,7 +721,7 @@ async def async_set_speed(self, speed: str) -> None: if self._optimistic_speed and speed_payload: self._speed = speed - self.async_write_ha_state() + self.async_write_ha_state() async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation. diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 9c4b0f3a3e3a3..3e347363428ba 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -5,10 +5,24 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_WHITE_VALUE, + ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_UNKNOWN, + COLOR_MODE_XY, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -41,6 +55,8 @@ CONF_BRIGHTNESS_SCALE = "brightness_scale" CONF_BRIGHTNESS_STATE_TOPIC = "brightness_state_topic" CONF_BRIGHTNESS_VALUE_TEMPLATE = "brightness_value_template" +CONF_COLOR_MODE_STATE_TOPIC = "color_mode_state_topic" +CONF_COLOR_MODE_VALUE_TEMPLATE = "color_mode_value_template" CONF_COLOR_TEMP_COMMAND_TEMPLATE = "color_temp_command_template" CONF_COLOR_TEMP_COMMAND_TOPIC = "color_temp_command_topic" CONF_COLOR_TEMP_STATE_TOPIC = "color_temp_state_topic" @@ -58,6 +74,14 @@ CONF_RGB_COMMAND_TOPIC = "rgb_command_topic" CONF_RGB_STATE_TOPIC = "rgb_state_topic" CONF_RGB_VALUE_TEMPLATE = "rgb_value_template" +CONF_RGBW_COMMAND_TEMPLATE = "rgbw_command_template" +CONF_RGBW_COMMAND_TOPIC = "rgbw_command_topic" +CONF_RGBW_STATE_TOPIC = "rgbw_state_topic" +CONF_RGBW_VALUE_TEMPLATE = "rgbw_value_template" +CONF_RGBWW_COMMAND_TEMPLATE = "rgbww_command_template" +CONF_RGBWW_COMMAND_TOPIC = "rgbww_command_topic" +CONF_RGBWW_STATE_TOPIC = "rgbww_state_topic" +CONF_RGBWW_VALUE_TEMPLATE = "rgbww_value_template" CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_XY_COMMAND_TOPIC = "xy_command_topic" CONF_XY_STATE_TOPIC = "xy_state_topic" @@ -78,13 +102,21 @@ VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"] -COMMAND_TEMPLATE_KEYS = [CONF_COLOR_TEMP_COMMAND_TEMPLATE, CONF_RGB_COMMAND_TEMPLATE] +COMMAND_TEMPLATE_KEYS = [ + CONF_COLOR_TEMP_COMMAND_TEMPLATE, + CONF_RGB_COMMAND_TEMPLATE, + CONF_RGBW_COMMAND_TEMPLATE, + CONF_RGBWW_COMMAND_TEMPLATE, +] VALUE_TEMPLATE_KEYS = [ CONF_BRIGHTNESS_VALUE_TEMPLATE, + CONF_COLOR_MODE_VALUE_TEMPLATE, CONF_COLOR_TEMP_VALUE_TEMPLATE, CONF_EFFECT_VALUE_TEMPLATE, CONF_HS_VALUE_TEMPLATE, CONF_RGB_VALUE_TEMPLATE, + CONF_RGBW_VALUE_TEMPLATE, + CONF_RGBWW_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE, CONF_WHITE_VALUE_TEMPLATE, CONF_XY_VALUE_TEMPLATE, @@ -99,6 +131,8 @@ ): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_COLOR_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COLOR_TEMP_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -123,6 +157,14 @@ vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_RGBW_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_RGBW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RGBW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBW_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_RGBWW_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_RGBWW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( @@ -155,25 +197,35 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT light.""" - self._state = False self._brightness = None - self._hs = None + self._color_mode = None self._color_temp = None self._effect = None + self._hs_color = None + self._legacy_mode = False + self._rgb_color = None + self._rgbw_color = None + self._rgbww_color = None + self._state = False + self._supported_color_modes = None self._white_value = None + self._xy_color = None self._topic = None self._payload = None self._command_templates = None self._value_templates = None self._optimistic = False - self._optimistic_rgb = False self._optimistic_brightness = False + self._optimistic_color_mode = False self._optimistic_color_temp = False self._optimistic_effect = False - self._optimistic_hs = False + self._optimistic_hs_color = False + self._optimistic_rgb_color = False + self._optimistic_rgbw_color = False + self._optimistic_rgbww_color = False self._optimistic_white_value = False - self._optimistic_xy = False + self._optimistic_xy_color = False MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -189,6 +241,7 @@ def _setup_from_config(self, config): for key in ( CONF_BRIGHTNESS_COMMAND_TOPIC, CONF_BRIGHTNESS_STATE_TOPIC, + CONF_COLOR_MODE_STATE_TOPIC, CONF_COLOR_TEMP_COMMAND_TOPIC, CONF_COLOR_TEMP_STATE_TOPIC, CONF_COMMAND_TOPIC, @@ -198,6 +251,10 @@ def _setup_from_config(self, config): CONF_HS_STATE_TOPIC, CONF_RGB_COMMAND_TOPIC, CONF_RGB_STATE_TOPIC, + CONF_RGBW_COMMAND_TOPIC, + CONF_RGBW_STATE_TOPIC, + CONF_RGBWW_COMMAND_TOPIC, + CONF_RGBWW_STATE_TOPIC, CONF_STATE_TOPIC, CONF_WHITE_VALUE_COMMAND_TOPIC, CONF_WHITE_VALUE_STATE_TOPIC, @@ -227,8 +284,15 @@ def _setup_from_config(self, config): self._command_templates = command_templates optimistic = config[CONF_OPTIMISTIC] + self._optimistic_color_mode = ( + optimistic or topic[CONF_COLOR_MODE_STATE_TOPIC] is None + ) self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None - self._optimistic_rgb = optimistic or topic[CONF_RGB_STATE_TOPIC] is None + self._optimistic_rgb_color = optimistic or topic[CONF_RGB_STATE_TOPIC] is None + self._optimistic_rgbw_color = optimistic or topic[CONF_RGBW_STATE_TOPIC] is None + self._optimistic_rgbww_color = ( + optimistic or topic[CONF_RGBWW_STATE_TOPIC] is None + ) self._optimistic_brightness = ( optimistic or ( @@ -244,18 +308,71 @@ def _setup_from_config(self, config): optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None ) self._optimistic_effect = optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None - self._optimistic_hs = optimistic or topic[CONF_HS_STATE_TOPIC] is None + self._optimistic_hs_color = optimistic or topic[CONF_HS_STATE_TOPIC] is None self._optimistic_white_value = ( optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None ) - self._optimistic_xy = optimistic or topic[CONF_XY_STATE_TOPIC] is None + self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None + self._supported_color_modes = set() + if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + self._color_mode = COLOR_MODE_COLOR_TEMP + if topic[CONF_HS_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_HS) + self._color_mode = COLOR_MODE_HS + if topic[CONF_RGB_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_RGB) + self._color_mode = COLOR_MODE_RGB + if topic[CONF_RGBW_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_RGBW) + self._color_mode = COLOR_MODE_RGBW + if topic[CONF_RGBWW_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_RGBWW) + self._color_mode = COLOR_MODE_RGBWW + if topic[CONF_XY_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_XY) + self._color_mode = COLOR_MODE_XY + if len(self._supported_color_modes) > 1: + self._color_mode = COLOR_MODE_UNKNOWN + + if not self._supported_color_modes: + if topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + self._color_mode = COLOR_MODE_BRIGHTNESS + self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + else: + self._color_mode = COLOR_MODE_ONOFF + self._supported_color_modes.add(COLOR_MODE_ONOFF) - async def _subscribe_topics(self): + if topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: + self._legacy_mode = True + + def _is_optimistic(self, attribute): + """Return True if the attribute is optimistically updated.""" + return getattr(self, f"_optimistic_{attribute}") + + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} last_state = await self.async_get_last_state() + def add_topic(topic, msg_callback): + """Add a topic.""" + if self._topic[topic] is not None: + topics[topic] = { + "topic": self._topic[topic], + "msg_callback": msg_callback, + "qos": self._config[CONF_QOS], + } + + def restore_state(attribute, condition_attribute=None): + """Restore a state attribute.""" + if condition_attribute is None: + condition_attribute = attribute + optimistic = self._is_optimistic(condition_attribute) + if optimistic and last_state and last_state.attributes.get(attribute): + setattr(self, f"_{attribute}", last_state.attributes[attribute]) + @callback @log_messages(self.hass, self.entity_id) def state_received(msg): @@ -298,47 +415,97 @@ def brightness_received(msg): self._brightness = percent_bright * 255 self.async_write_ha_state() - if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None: - topics[CONF_BRIGHTNESS_STATE_TOPIC] = { - "topic": self._topic[CONF_BRIGHTNESS_STATE_TOPIC], - "msg_callback": brightness_received, - "qos": self._config[CONF_QOS], - } - elif ( - self._optimistic_brightness - and last_state - and last_state.attributes.get(ATTR_BRIGHTNESS) - ): - self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) + restore_state(ATTR_BRIGHTNESS) + + def _rgbx_received(msg, template, color_mode, convert_color): + """Handle new MQTT messages for RGBW and RGBWW.""" + payload = self._value_templates[template](msg.payload, None) + if not payload: + _LOGGER.debug( + "Ignoring empty %s message from '%s'", color_mode, msg.topic + ) + return None + color = tuple(int(val) for val in payload.split(",")) + if self._optimistic_color_mode: + self._color_mode = color_mode + if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: + rgb = convert_color(*color) + percent_bright = float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 + self._brightness = percent_bright * 255 + return color @callback @log_messages(self.hass, self.entity_id) def rgb_received(msg): """Handle new MQTT messages for RGB.""" - payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload, None) + rgb = _rgbx_received( + msg, CONF_RGB_VALUE_TEMPLATE, COLOR_MODE_RGB, lambda *x: x + ) + if not rgb: + return + if self._legacy_mode: + self._hs_color = color_util.color_RGB_to_hs(*rgb) + else: + self._rgb_color = rgb + self.async_write_ha_state() + + add_topic(CONF_RGB_STATE_TOPIC, rgb_received) + restore_state(ATTR_RGB_COLOR) + restore_state(ATTR_HS_COLOR, ATTR_RGB_COLOR) + + @callback + @log_messages(self.hass, self.entity_id) + def rgbw_received(msg): + """Handle new MQTT messages for RGBW.""" + rgbw = _rgbx_received( + msg, + CONF_RGBW_VALUE_TEMPLATE, + COLOR_MODE_RGBW, + color_util.color_rgbw_to_rgb, + ) + if not rgbw: + return + self._rgbw_color = rgbw + self.async_write_ha_state() + + add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) + restore_state(ATTR_RGBW_COLOR) + + @callback + @log_messages(self.hass, self.entity_id) + def rgbww_received(msg): + """Handle new MQTT messages for RGBWW.""" + rgbww = _rgbx_received( + msg, + CONF_RGBWW_VALUE_TEMPLATE, + COLOR_MODE_RGBWW, + color_util.color_rgbww_to_rgb, + ) + if not rgbww: + return + self._rgbww_color = rgbww + self.async_write_ha_state() + + add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) + restore_state(ATTR_RGBWW_COLOR) + + @callback + @log_messages(self.hass, self.entity_id) + def color_mode_received(msg): + """Handle new MQTT messages for color mode.""" + payload = self._value_templates[CONF_COLOR_MODE_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: - _LOGGER.debug("Ignoring empty rgb message from '%s'", msg.topic) + _LOGGER.debug("Ignoring empty color mode message from '%s'", msg.topic) return - rgb = [int(val) for val in payload.split(",")] - self._hs = color_util.color_RGB_to_hs(*rgb) - if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: - percent_bright = float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 - self._brightness = percent_bright * 255 + self._color_mode = payload self.async_write_ha_state() - if self._topic[CONF_RGB_STATE_TOPIC] is not None: - topics[CONF_RGB_STATE_TOPIC] = { - "topic": self._topic[CONF_RGB_STATE_TOPIC], - "msg_callback": rgb_received, - "qos": self._config[CONF_QOS], - } - if ( - self._optimistic_rgb - and last_state - and last_state.attributes.get(ATTR_HS_COLOR) - ): - self._hs = last_state.attributes.get(ATTR_HS_COLOR) + add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) + restore_state(ATTR_COLOR_MODE) @callback @log_messages(self.hass, self.entity_id) @@ -351,21 +518,13 @@ def color_temp_received(msg): _LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic) return + if self._optimistic_color_mode: + self._color_mode = COLOR_MODE_COLOR_TEMP self._color_temp = int(payload) self.async_write_ha_state() - if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None: - topics[CONF_COLOR_TEMP_STATE_TOPIC] = { - "topic": self._topic[CONF_COLOR_TEMP_STATE_TOPIC], - "msg_callback": color_temp_received, - "qos": self._config[CONF_QOS], - } - if ( - self._optimistic_color_temp - and last_state - and last_state.attributes.get(ATTR_COLOR_TEMP) - ): - self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) + restore_state(ATTR_COLOR_TEMP) @callback @log_messages(self.hass, self.entity_id) @@ -381,18 +540,8 @@ def effect_received(msg): self._effect = payload self.async_write_ha_state() - if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: - topics[CONF_EFFECT_STATE_TOPIC] = { - "topic": self._topic[CONF_EFFECT_STATE_TOPIC], - "msg_callback": effect_received, - "qos": self._config[CONF_QOS], - } - if ( - self._optimistic_effect - and last_state - and last_state.attributes.get(ATTR_EFFECT) - ): - self._effect = last_state.attributes.get(ATTR_EFFECT) + add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) + restore_state(ATTR_EFFECT) @callback @log_messages(self.hass, self.entity_id) @@ -402,26 +551,17 @@ def hs_received(msg): if not payload: _LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic) return - try: - hs_color = [float(val) for val in payload.split(",", 2)] - self._hs = hs_color + hs_color = tuple(float(val) for val in payload.split(",", 2)) + if self._optimistic_color_mode: + self._color_mode = COLOR_MODE_HS + self._hs_color = hs_color self.async_write_ha_state() except ValueError: _LOGGER.debug("Failed to parse hs state update: '%s'", payload) - if self._topic[CONF_HS_STATE_TOPIC] is not None: - topics[CONF_HS_STATE_TOPIC] = { - "topic": self._topic[CONF_HS_STATE_TOPIC], - "msg_callback": hs_received, - "qos": self._config[CONF_QOS], - } - if ( - self._optimistic_hs - and last_state - and last_state.attributes.get(ATTR_HS_COLOR) - ): - self._hs = last_state.attributes.get(ATTR_HS_COLOR) + add_topic(CONF_HS_STATE_TOPIC, hs_received) + restore_state(ATTR_HS_COLOR) @callback @log_messages(self.hass, self.entity_id) @@ -439,18 +579,8 @@ def white_value_received(msg): self._white_value = percent_white * 255 self.async_write_ha_state() - if self._topic[CONF_WHITE_VALUE_STATE_TOPIC] is not None: - topics[CONF_WHITE_VALUE_STATE_TOPIC] = { - "topic": self._topic[CONF_WHITE_VALUE_STATE_TOPIC], - "msg_callback": white_value_received, - "qos": self._config[CONF_QOS], - } - elif ( - self._optimistic_white_value - and last_state - and last_state.attributes.get(ATTR_WHITE_VALUE) - ): - self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) + add_topic(CONF_WHITE_VALUE_STATE_TOPIC, white_value_received) + restore_state(ATTR_WHITE_VALUE) @callback @log_messages(self.hass, self.entity_id) @@ -461,22 +591,18 @@ def xy_received(msg): _LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic) return - xy_color = [float(val) for val in payload.split(",")] - self._hs = color_util.color_xy_to_hs(*xy_color) + xy_color = tuple(float(val) for val in payload.split(",")) + if self._optimistic_color_mode: + self._color_mode = COLOR_MODE_XY + if self._legacy_mode: + self._hs_color = color_util.color_xy_to_hs(*xy_color) + else: + self._xy_color = xy_color self.async_write_ha_state() - if self._topic[CONF_XY_STATE_TOPIC] is not None: - topics[CONF_XY_STATE_TOPIC] = { - "topic": self._topic[CONF_XY_STATE_TOPIC], - "msg_callback": xy_received, - "qos": self._config[CONF_QOS], - } - if ( - self._optimistic_xy - and last_state - and last_state.attributes.get(ATTR_HS_COLOR) - ): - self._hs = last_state.attributes.get(ATTR_HS_COLOR) + add_topic(CONF_XY_STATE_TOPIC, xy_received) + restore_state(ATTR_XY_COLOR) + restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) self._sub_state = await subscription.async_subscribe_topics( self.hass, self._sub_state, topics @@ -490,16 +616,51 @@ def brightness(self): brightness = min(round(brightness), 255) return brightness + @property + def color_mode(self): + """Return current color mode.""" + if self._legacy_mode: + return None + return self._color_mode + @property def hs_color(self): """Return the hs color value.""" + if not self._legacy_mode: + return self._hs_color + + # Legacy mode, gate color_temp with white_value == 0 if self._white_value: return None - return self._hs + return self._hs_color + + @property + def rgb_color(self): + """Return the rgb color value.""" + return self._rgb_color + + @property + def rgbw_color(self): + """Return the rgbw color value.""" + return self._rgbw_color + + @property + def rgbww_color(self): + """Return the rgbww color value.""" + return self._rgbww_color + + @property + def xy_color(self): + """Return the xy color value.""" + return self._xy_color @property def color_temp(self): """Return the color temperature in mired.""" + if not self._legacy_mode: + return self._color_temp + + # Legacy mode, gate color_temp with white_value > 0 supports_color = ( self._topic[CONF_RGB_COMMAND_TOPIC] or self._topic[CONF_HS_COMMAND_TOPIC] @@ -548,10 +709,24 @@ def effect(self): """Return the current effect.""" return self._effect + @property + def supported_color_modes(self): + """Flag supported color modes.""" + if self._legacy_mode: + return None + return self._supported_color_modes + @property def supported_features(self): """Flag supported features.""" supported_features = 0 + supported_features |= ( + self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None and SUPPORT_EFFECT + ) + if not self._legacy_mode: + return supported_features + + # Legacy mode supported_features |= self._topic[CONF_RGB_COMMAND_TOPIC] is not None and ( SUPPORT_COLOR | SUPPORT_BRIGHTNESS ) @@ -563,9 +738,6 @@ def supported_features(self): self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and SUPPORT_COLOR_TEMP ) - supported_features |= ( - self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None and SUPPORT_EFFECT - ) supported_features |= ( self._topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR ) @@ -579,7 +751,7 @@ def supported_features(self): return supported_features - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. @@ -587,14 +759,56 @@ async def async_turn_on(self, **kwargs): should_update = False on_command_type = self._config[CONF_ON_COMMAND_TYPE] - if on_command_type == "first": + def publish(topic, payload): + """Publish an MQTT message.""" mqtt.async_publish( self.hass, - self._topic[CONF_COMMAND_TOPIC], - self._payload["on"], + self._topic[topic], + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], ) + + def scale_rgbx(color, brightness=None): + """Scale RGBx for brightness.""" + if brightness is None: + # If there's a brightness topic set, we don't want to scale the RGBx + # values given using the brightness. + if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255 + ) + return tuple(int(channel * brightness / 255) for channel in color) + + def render_rgbx(color, template, color_mode): + """Render RGBx payload.""" + tpl = self._command_templates[template] + if tpl: + keys = ["red", "green", "blue"] + if color_mode == COLOR_MODE_RGBW: + keys.append("white") + elif color_mode == COLOR_MODE_RGBWW: + keys.extend(["cold_white", "warm_white"]) + rgb_color_str = tpl(zip(keys, color)) + else: + rgb_color_str = ",".join(str(channel) for channel in color) + return rgb_color_str + + def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): + """Optimistically update a state attribute.""" + if condition_attribute is None: + condition_attribute = attribute + if not self._is_optimistic(condition_attribute): + return False + if color_mode and self._optimistic_color_mode: + self._color_mode = color_mode + setattr(self, f"_{attribute}", value) + return True + + if on_command_type == "first": + publish(CONF_COMMAND_TOPIC, self._payload["on"]) should_update = True # If brightness is being used instead of an on command, make sure @@ -603,68 +817,73 @@ async def async_turn_on(self, **kwargs): elif on_command_type == "brightness" and ATTR_BRIGHTNESS not in kwargs: kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 - if ATTR_HS_COLOR in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None: - - hs_color = kwargs[ATTR_HS_COLOR] - - # If there's a brightness topic set, we don't want to scale the RGB - # values given using the brightness. - if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: - brightness = 255 - else: - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255 - ) - rgb = color_util.color_hsv_to_RGB( - hs_color[0], hs_color[1], brightness / 255 * 100 - ) - tpl = self._command_templates[CONF_RGB_COMMAND_TEMPLATE] - if tpl: - rgb_color_str = tpl({"red": rgb[0], "green": rgb[1], "blue": rgb[2]}) - else: - rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" - - mqtt.async_publish( - self.hass, - self._topic[CONF_RGB_COMMAND_TOPIC], - rgb_color_str, - self._config[CONF_QOS], - self._config[CONF_RETAIN], + hs_color = kwargs.get(ATTR_HS_COLOR) + if ( + hs_color + and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and self._legacy_mode + ): + # Legacy mode: Convert HS to RGB + rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100)) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + should_update |= set_optimistic( + ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_RGB_COLOR ) - if self._optimistic_rgb: - self._hs = kwargs[ATTR_HS_COLOR] - should_update = True + if hs_color and self._topic[CONF_HS_COMMAND_TOPIC] is not None: + publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") + should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, COLOR_MODE_HS) - if ATTR_HS_COLOR in kwargs and self._topic[CONF_HS_COMMAND_TOPIC] is not None: - - hs_color = kwargs[ATTR_HS_COLOR] - mqtt.async_publish( - self.hass, - self._topic[CONF_HS_COMMAND_TOPIC], - f"{hs_color[0]},{hs_color[1]}", - self._config[CONF_QOS], - self._config[CONF_RETAIN], + if ( + hs_color + and self._topic[CONF_XY_COMMAND_TOPIC] is not None + and self._legacy_mode + ): + # Legacy mode: Convert HS to XY + xy_color = color_util.color_hs_to_xy(*hs_color) + publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") + should_update |= set_optimistic( + ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_XY_COLOR ) - if self._optimistic_hs: - self._hs = kwargs[ATTR_HS_COLOR] - should_update = True + if ( + (rgb := kwargs.get(ATTR_RGB_COLOR)) + and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + scaled = scale_rgbx(rgb) + rgb_s = render_rgbx(scaled, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + should_update |= set_optimistic(ATTR_RGB_COLOR, rgb, COLOR_MODE_RGB) - if ATTR_HS_COLOR in kwargs and self._topic[CONF_XY_COMMAND_TOPIC] is not None: + if ( + (rgbw := kwargs.get(ATTR_RGBW_COLOR)) + and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + scaled = scale_rgbx(rgbw) + rgbw_s = render_rgbx(scaled, CONF_RGBW_COMMAND_TEMPLATE, COLOR_MODE_RGBW) + publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) + should_update |= set_optimistic(ATTR_RGBW_COLOR, rgbw, COLOR_MODE_RGBW) - xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) - mqtt.async_publish( - self.hass, - self._topic[CONF_XY_COMMAND_TOPIC], - f"{xy_color[0]},{xy_color[1]}", - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + if ( + (rgbww := kwargs.get(ATTR_RGBWW_COLOR)) + and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + scaled = scale_rgbx(rgbww) + rgbww_s = render_rgbx(scaled, CONF_RGBWW_COMMAND_TEMPLATE, COLOR_MODE_RGBWW) + publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) + should_update |= set_optimistic(ATTR_RGBWW_COLOR, rgbww, COLOR_MODE_RGBWW) - if self._optimistic_xy: - self._hs = kwargs[ATTR_HS_COLOR] - should_update = True + if ( + (xy_color := kwargs.get(ATTR_XY_COLOR)) + and self._topic[CONF_XY_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") + should_update |= set_optimistic(ATTR_XY_COLOR, xy_color, COLOR_MODE_XY) if ( ATTR_BRIGHTNESS in kwargs @@ -677,80 +896,77 @@ async def async_turn_on(self, **kwargs): ) # Make sure the brightness is not rounded down to 0 device_brightness = max(device_brightness, 1) - mqtt.async_publish( - self.hass, - self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC], - device_brightness, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) - - if self._optimistic_brightness: - self._brightness = kwargs[ATTR_BRIGHTNESS] - should_update = True + publish(CONF_BRIGHTNESS_COMMAND_TOPIC, device_brightness) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR not in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and self._legacy_mode + ): + # Legacy mode + hs_color = self._hs_color if self._hs_color is not None else (0, 0) + brightness = kwargs[ATTR_BRIGHTNESS] + rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100), brightness) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) + elif ( + ATTR_BRIGHTNESS in kwargs + and ATTR_RGB_COLOR not in kwargs + and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and not self._legacy_mode ): - hs_color = self._hs if self._hs is not None else (0, 0) - rgb = color_util.color_hsv_to_RGB( - hs_color[0], hs_color[1], kwargs[ATTR_BRIGHTNESS] / 255 * 100 + rgb_color = self._rgb_color if self._rgb_color is not None else (255,) * 3 + rgb = scale_rgbx(rgb_color, kwargs[ATTR_BRIGHTNESS]) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) + elif ( + ATTR_BRIGHTNESS in kwargs + and ATTR_RGBW_COLOR not in kwargs + and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + rgbw_color = ( + self._rgbw_color if self._rgbw_color is not None else (255,) * 4 ) - tpl = self._command_templates[CONF_RGB_COMMAND_TEMPLATE] - if tpl: - rgb_color_str = tpl({"red": rgb[0], "green": rgb[1], "blue": rgb[2]}) - else: - rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" - - mqtt.async_publish( - self.hass, - self._topic[CONF_RGB_COMMAND_TOPIC], - rgb_color_str, - self._config[CONF_QOS], - self._config[CONF_RETAIN], + rgbw = scale_rgbx(rgbw_color, kwargs[ATTR_BRIGHTNESS]) + rgbw_s = render_rgbx(rgbw, CONF_RGBW_COMMAND_TEMPLATE, COLOR_MODE_RGBW) + publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) + elif ( + ATTR_BRIGHTNESS in kwargs + and ATTR_RGBWW_COLOR not in kwargs + and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + rgbww_color = ( + self._rgbww_color if self._rgbww_color is not None else (255,) * 5 ) - - if self._optimistic_brightness: - self._brightness = kwargs[ATTR_BRIGHTNESS] - should_update = True - + rgbww = scale_rgbx(rgbww_color, kwargs[ATTR_BRIGHTNESS]) + rgbww_s = render_rgbx(rgbww, CONF_RGBWW_COMMAND_TEMPLATE, COLOR_MODE_RGBWW) + publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) if ( ATTR_COLOR_TEMP in kwargs and self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None ): color_temp = int(kwargs[ATTR_COLOR_TEMP]) tpl = self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE] - if tpl: color_temp = tpl({"value": color_temp}) - mqtt.async_publish( - self.hass, - self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC], - color_temp, - self._config[CONF_QOS], - self._config[CONF_RETAIN], + publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) + should_update |= set_optimistic( + ATTR_COLOR_TEMP, kwargs[ATTR_COLOR_TEMP], COLOR_MODE_COLOR_TEMP ) - if self._optimistic_color_temp: - self._color_temp = kwargs[ATTR_COLOR_TEMP] - should_update = True - if ATTR_EFFECT in kwargs and self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: effect = kwargs[ATTR_EFFECT] if effect in self._config.get(CONF_EFFECT_LIST): - mqtt.async_publish( - self.hass, - self._topic[CONF_EFFECT_COMMAND_TOPIC], - effect, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) - - if self._optimistic_effect: - self._effect = kwargs[ATTR_EFFECT] - should_update = True + publish(CONF_EFFECT_COMMAND_TOPIC, effect) + should_update |= set_optimistic(ATTR_EFFECT, effect) if ( ATTR_WHITE_VALUE in kwargs @@ -759,26 +975,11 @@ async def async_turn_on(self, **kwargs): percent_white = float(kwargs[ATTR_WHITE_VALUE]) / 255 white_scale = self._config[CONF_WHITE_VALUE_SCALE] device_white_value = min(round(percent_white * white_scale), white_scale) - mqtt.async_publish( - self.hass, - self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC], - device_white_value, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) - - if self._optimistic_white_value: - self._white_value = kwargs[ATTR_WHITE_VALUE] - should_update = True + publish(CONF_WHITE_VALUE_COMMAND_TOPIC, device_white_value) + should_update |= set_optimistic(ATTR_WHITE_VALUE, kwargs[ATTR_WHITE_VALUE]) if on_command_type == "last": - mqtt.async_publish( - self.hass, - self._topic[CONF_COMMAND_TOPIC], - self._payload["on"], - self._config[CONF_QOS], - self._config[CONF_RETAIN], - ) + publish(CONF_COMMAND_TOPIC, self._payload["on"]) should_update = True if self._optimistic: diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 9940d646a35bc..5143b92622aa4 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -487,7 +487,7 @@ def _scale_rgbxx(self, rgbxx, kwargs): def _supports_color_mode(self, color_mode): return self.supported_color_modes and color_mode in self.supported_color_modes - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 7c0266265db5c..c5eee7006d6f6 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -142,7 +142,7 @@ def _setup_from_config(self, config): or self._templates[CONF_STATE_TEMPLATE] is None ) - async def _subscribe_topics(self): + async def _subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" for tpl in self._templates.values(): if tpl is not None: diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index cdfa51015480a..24d58b148fa23 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -6,10 +6,10 @@ from homeassistant.components import lock from homeassistant.components.lock import LockEntity from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -50,7 +50,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT lock panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 9de3b07184487..c5d9ad21ed614 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": ["paho-mqtt==1.5.1"], "dependencies": ["http"], - "codeowners": ["@emontnemery"] + "codeowners": ["@emontnemery"], + "iot_class": "local_push" } diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index e7839f8e483bd..95409924fa418 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -5,13 +5,18 @@ import voluptuous as vol from homeassistant.components import number -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import ( + DEFAULT_MAX_VALUE, + DEFAULT_MIN_VALUE, + DEFAULT_STEP, + NumberEntity, +) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -28,19 +33,40 @@ _LOGGER = logging.getLogger(__name__) +CONF_MIN = "min" +CONF_MAX = "max" +CONF_STEP = "step" + DEFAULT_NAME = "MQTT Number" DEFAULT_OPTIMISTIC = False -PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - } -).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +def validate_config(config): + """Validate that the configuration is valid, throws if it isn't.""" + if config.get(CONF_MIN) >= config.get(CONF_MAX): + raise vol.Invalid(f"'{CONF_MAX}'' must be > '{CONF_MIN}'") + + return config + + +PLATFORM_SCHEMA = vol.All( + mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), + vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( + vol.Coerce(float), vol.Range(min=1e-3) + ), + }, + ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + validate_config, +) async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT number through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -67,6 +93,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): def __init__(self, config, config_entry, discovery_data): """Initialize the MQTT Number.""" + self._config = config self._sub_state = None self._current_number = None @@ -89,12 +116,28 @@ def message_received(msg): """Handle new MQTT messages.""" try: if msg.payload.decode("utf-8").isnumeric(): - self._current_number = int(msg.payload) + num_value = int(msg.payload) else: - self._current_number = float(msg.payload) - self.async_write_ha_state() + num_value = float(msg.payload) except ValueError: - _LOGGER.warning("We received <%s> which is not a Number", msg.payload) + _LOGGER.warning( + "Payload '%s' is not a Number", + msg.payload.decode("utf-8", errors="ignore"), + ) + return + + if num_value < self.min_value or num_value > self.max_value: + _LOGGER.error( + "Invalid value for %s: %s (range %s - %s)", + self.entity_id, + num_value, + self.min_value, + self.max_value, + ) + return + + self._current_number = num_value + self.async_write_ha_state() if self._config.get(CONF_STATE_TOPIC) is None: # Force into optimistic mode. @@ -118,6 +161,21 @@ def message_received(msg): if last_state: self._current_number = last_state.state + @property + def min_value(self) -> float: + """Return the minimum value.""" + return self._config[CONF_MIN] + + @property + def max_value(self) -> float: + """Return the maximum value.""" + return self._config[CONF_MAX] + + @property + def step(self) -> float: + """Return the increment/decrement step.""" + return self._config[CONF_STEP] + @property def value(self): """Return the current value.""" diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index c6d9140af616a..1d84d1cecae73 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -6,9 +6,10 @@ from homeassistant.components import scene from homeassistant.components.scene import Scene from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN, PLATFORMS from .. import mqtt @@ -35,7 +36,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT scene through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 65c9e0550e0ba..145af55daa8d5 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -7,7 +7,11 @@ import voluptuous as vol from homeassistant.components import sensor -from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA, + STATE_CLASSES_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, @@ -15,11 +19,11 @@ CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription @@ -33,6 +37,7 @@ ) CONF_EXPIRE_AFTER = "expire_after" +CONF_STATE_CLASS = "state_class" DEFAULT_NAME = "MQTT Sensor" DEFAULT_FORCE_UPDATE = False @@ -42,13 +47,14 @@ vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT sensors through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -125,8 +131,11 @@ def message_received(msg): template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: + variables = {"entity_id": self.entity_id} payload = template.async_render_with_possible_json_value( - payload, self._state + payload, + self._state, + variables=variables, ) self._state = payload self.async_write_ha_state() @@ -170,6 +179,11 @@ def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) + @property + def state_class(self) -> str | None: + """Return the state class of the sensor.""" + return self._config.get(CONF_STATE_CLASS) + @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 2edbc86eb8c77..9de9075f19db8 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -51,6 +51,7 @@ "options": { "step": { "broker": { + "title": "Broker options", "description": "Please enter the connection information of your MQTT broker.", "data": { "broker": "Broker", @@ -60,7 +61,8 @@ } }, "options": { - "description": "Please select MQTT options.", + "title": "MQTT options", + "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.", "data": { "discovery": "Enable discovery", "birth_enable": "Enable birth message", diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index e6c99c09fd5a4..6c711600b2ca9 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -5,7 +5,7 @@ import attr -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass from . import debug_info @@ -18,7 +18,7 @@ class EntitySubscription: """Class to hold data about an active entity topic subscription.""" - hass: HomeAssistantType = attr.ib() + hass: HomeAssistant = attr.ib() topic: str = attr.ib() message_callback: MessageCallbackType = attr.ib() unsubscribe_callback: Callable[[], None] | None = attr.ib() @@ -63,7 +63,7 @@ def _should_resubscribe(self, other): @bind_hass async def async_subscribe_topics( - hass: HomeAssistantType, + hass: HomeAssistant, new_state: dict[str, EntitySubscription] | None, topics: dict[str, Any], ): @@ -106,6 +106,6 @@ async def async_subscribe_topics( @bind_hass -async def async_unsubscribe_topics(hass: HomeAssistantType, sub_state: dict): +async def async_unsubscribe_topics(hass: HomeAssistant, sub_state: dict): """Unsubscribe from all MQTT topics managed by async_subscribe_topics.""" return await async_subscribe_topics(hass, sub_state, {}) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 2b272b0f9be98..d07f639f41da2 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -13,11 +13,11 @@ CONF_VALUE_TEMPLATE, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from . import ( CONF_COMMAND_TOPIC, @@ -52,7 +52,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT switch through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 23b7cd5dfa9f1..108da3b126306 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -21,8 +21,8 @@ "data": { "discovery": "Habilitar descobriment autom\u00e0tic" }, - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de Hass.io {addon}?", - "title": "Broker MQTT via complement de Hass.io" + "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement {addon}?", + "title": "Broker MQTT via complement de Home Assistant" } } }, @@ -50,7 +50,7 @@ }, "options": { "error": { - "bad_birth": "Topic missatge de naixement inv\u00e0lid.", + "bad_birth": "Topic del missatge de naixement inv\u00e0lid.", "bad_will": "Topic missatge d'\u00faltima voluntat inv\u00e0lid.", "cannot_connect": "Ha fallat la connexi\u00f3" }, @@ -62,23 +62,25 @@ "port": "Port", "username": "Nom d'usuari" }, - "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT." + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT.", + "title": "Opcions del broker" }, "options": { "data": { "birth_enable": "Activa el missatge de naixement", - "birth_payload": "Dades (payload) missatge de naixement", - "birth_qos": "QoS missatge de naixement", - "birth_retain": "Retenci\u00f3 missatge de naixement", - "birth_topic": "Topic missatge de naixement", + "birth_payload": "Dades (payload) del missatge de naixement", + "birth_qos": "QoS del missatge de naixement", + "birth_retain": "Retenci\u00f3 del missatge de naixement", + "birth_topic": "Topic del missatge de naixement", "discovery": "Activar descobriment", "will_enable": "Activa el missatge d'\u00faltima voluntat", - "will_payload": "Dades (payload) missatge d'\u00faltima voluntat", - "will_qos": "QoS missatge d'\u00faltima voluntat", - "will_retain": "Retenci\u00f3 missatge d'\u00faltima voluntat", - "will_topic": "Topic missatge d'\u00faltima voluntat" + "will_payload": "Dades (payload) del missatge d'\u00faltima voluntat", + "will_qos": "QoS del missatge d'\u00faltima voluntat", + "will_retain": "Retenci\u00f3 del missatge d'\u00faltima voluntat", + "will_topic": "Topic del missatge d'\u00faltima voluntat" }, - "description": "Selecciona les opcions MQTT." + "description": "Descobriment - Si est\u00e0 activat (recomanat), Home Assistant descobrir\u00e0 autom\u00e0ticament dispositius i entitats que publiquin la seva configuraci\u00f3 al broker MQTT. Si est\u00e0 desactivat, les configuracions s'han de fer manualment.\nMissatge de naixement - S'enviar\u00e0 cada vegada que Home Assistant \u00e9s connecti al broker MQTT.\nMissatge d'\u00faltima voluntat - S'enviar\u00e0 cada vegada que Home Assistant perdi la connexi\u00f3 amb el broker, tant si \u00e9s una desconnexi\u00f3 neta (per exemple si s'apaga Home Assistant) com si \u00e9s una desconnexi\u00f3 dolenta (per exemple si Home Assistant falla o perd la connexi\u00f3 a Internet).", + "title": "Opcions d'MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 4b57249eb382e..ffd2d1b36a1ca 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -50,6 +50,8 @@ }, "options": { "error": { + "bad_birth": "Ung\u00fcltiges Birth Thema.", + "bad_will": "Ung\u00fcltiges will Thema.", "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { @@ -64,10 +66,12 @@ }, "options": { "data": { + "birth_payload": "Nutzdaten der Birth Nachricht", "discovery": "Erkennung aktivieren", "will_enable": "Letzten Willen aktivieren" }, - "description": "Bitte die MQTT-Einstellungen ausw\u00e4hlen." + "description": "Erkennung - Wenn die Erkennung aktiviert ist (empfohlen), erkennt Home Assistant automatisch Ger\u00e4te und Entit\u00e4ten, die ihre Konfiguration auf dem MQTT-Broker ver\u00f6ffentlichen. Wenn die Erkennung deaktiviert ist, muss die gesamte Konfiguration manuell vorgenommen werden.\nGeburtsnachricht - Die Geburtsnachricht wird jedes Mal gesendet, wenn sich Home Assistant (erneut) mit dem MQTT-Broker verbindet.\nWill-Nachricht - Die Will-Nachricht wird jedes Mal gesendet, wenn Home Assistant die Verbindung zum Broker verliert, sowohl im Falle einer sauberen (z. B. Herunterfahren von Home Assistant) als auch im Falle einer unsauberen (z. B. Absturz von Home Assistant oder Verlust der Netzwerkverbindung) Verbindungstrennung.", + "title": "MQTT-Optionen" } } } diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 362e51b440537..775b4d21c9b6c 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -21,8 +21,8 @@ "data": { "discovery": "Enable discovery" }, - "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the Hass.io add-on {addon}?", - "title": "MQTT Broker via Hass.io add-on" + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the add-on {addon}?", + "title": "MQTT Broker via Home Assistant add-on" } } }, @@ -62,7 +62,8 @@ "port": "Port", "username": "Username" }, - "description": "Please enter the connection information of your MQTT broker." + "description": "Please enter the connection information of your MQTT broker.", + "title": "Broker options" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Will message retain", "will_topic": "Will message topic" }, - "description": "Please select MQTT options." + "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.", + "title": "MQTT options" } } } diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 70107efa26912..2cabe392308f8 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -62,7 +62,8 @@ "port": "Puerto", "username": "Usuario" }, - "description": "Por favor, introduce la informaci\u00f3n de tu agente MQTT." + "description": "Por favor, introduce la informaci\u00f3n de tu agente MQTT.", + "title": "Opciones para el Broker" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Retendr\u00e1 el mensaje", "will_topic": "Enviar\u00e1 un mensaje al tema" }, - "description": "Por favor, selecciona las opciones para MQTT." + "description": "Por favor, selecciona las opciones para MQTT.", + "title": "Opciones para MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index f28b1f4f94ed5..3b7a0c87f574e 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -21,8 +21,8 @@ "data": { "discovery": "Luba automaatne avastamine" }, - "description": "Kas soovid seadistada Home Assistanti \u00fchenduse loomiseks Hass.io lisandmooduli {addon} pakutava MQTT vahendajaga?", - "title": "MQTT vahendaja Hass.io lisandmooduli abil" + "description": "Kas soovid seadistada Home Assistanti \u00fchenduse loomiseks lisandmooduli {addon} pakutava MQTT vahendajaga?", + "title": "MQTT vahendaja Home Assistanti lisandmooduli abil" } } }, @@ -62,7 +62,8 @@ "port": "Port", "username": "Kasutajanimi" }, - "description": "Sisesta oma MQTT vahendaja \u00fchenduse teave." + "description": "Sisesta oma MQTT vahendaja \u00fchenduse teave.", + "title": "MQTT maakleri valikud" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "L\u00f5petamisteate j\u00e4\u00e4dvustamine", "will_topic": "L\u00f5petamisteade" }, - "description": "Vali MQTT s\u00e4tted." + "description": "Avastamine - kui avastamine on lubatud (soovitatav) avastab Home Assistant automaatselt seadmed ja \u00fcksused, kes avaldavad oma konfiguratsiooni MQTT maakleris. Kui avastamine on keelatud, tuleb kogu seadistamine teha k\u00e4sitsi.\n S\u00fcnnis\u00f5num - s\u00fcnnis\u00f5num saadetakse iga kord kui Home Assistant (uuesti) MQTT maakleriga \u00fchendust v\u00f5tab.\n Tahte s\u00f5num - tahte s\u00f5num saadetakse iga kord kui Home Assistant kaotab \u00fchenduse maakleriga, nii korralisel (nt Home Assistant sulgub) kui ka erakorralisel (nt Home Assistant krahhi v\u00f5i v\u00f5rgu\u00fchenduse kaotamisel) \u00fchenduse kadumisel.", + "title": "MQTT valikud" } } } diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index 6ee3788725d86..af13e69ab4a7f 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -62,7 +62,8 @@ "port": "Port", "username": "Username" }, - "description": "Veuillez entrer les informations de connexion de votre broker MQTT." + "description": "Veuillez entrer les informations de connexion de votre broker MQTT.", + "title": "Options de courtier" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Retenir le message de testament", "will_topic": "Topic du message de testament" }, - "description": "Veuillez s\u00e9lectionner les options MQTT." + "description": "D\u00e9couverte - Si la d\u00e9couverte est activ\u00e9e (recommand\u00e9e), Home Assistant d\u00e9couvrira automatiquement les appareils et les entit\u00e9s qui publient leur configuration sur le courtier MQTT. Si la d\u00e9couverte est d\u00e9sactiv\u00e9e, toute la configuration doit \u00eatre effectu\u00e9e manuellement.\n Message de naissance - Le message de naissance sera envoy\u00e9 chaque fois que Home Assistant (re) se connecte au courtier MQTT.\n Will message - Le message will sera envoy\u00e9 chaque fois que Home Assistant perd sa connexion avec le courtier, \u00e0 la fois en cas de nettoyage (par exemple, arr\u00eat de Home Assistant) et en cas de salet\u00e9 (par exemple, Home Assistant se bloque ou perd sa connexion r\u00e9seau) d\u00e9connecter.", + "title": "Options MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 845d0efabc787..9636e0ea4462f 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -21,8 +21,8 @@ "data": { "discovery": "Attiva l'individuazione" }, - "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo di Hass.io: {addon}?", - "title": "Broker MQTT tramite il componente aggiuntivo di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo: {addon}?", + "title": "Broker MQTT tramite il componente aggiuntivo di Home Assistant" } } }, @@ -62,7 +62,8 @@ "port": "Porta", "username": "Nome utente" }, - "description": "Inserisci le informazioni di connessione del tuo broker MQTT." + "description": "Inserisci le informazioni di connessione del tuo broker MQTT.", + "title": "Opzioni del broker" }, "options": { "data": { @@ -72,13 +73,14 @@ "birth_retain": "Persistenza del messaggio birth", "birth_topic": "Argomento del messaggio birth", "discovery": "Attiva l'individuazione", - "will_enable": "Abilita messaggio di ultima volont\u00e0 e testamento", - "will_payload": "Payload del messaggio will", - "will_qos": "QoS del messaggio will", - "will_retain": "Persistenza del messaggio will", - "will_topic": "Argomento del messaggio will" + "will_enable": "Abilita il messaggio testamento", + "will_payload": "Payload del messaggio testamento", + "will_qos": "QoS del messaggio testamento", + "will_retain": "Persistenza del messaggio testamento", + "will_topic": "Argomento del messaggio testamento" }, - "description": "Selezionare le opzioni MQTT." + "description": "Rilevamento: se il rilevamento \u00e8 abilitato (consigliato), Home Assistant rilever\u00e0 automaticamente i dispositivi e le entit\u00e0 che pubblicano la loro configurazione sul broker MQTT. Se il rilevamento \u00e8 disabilitato, tutta la configurazione deve essere eseguita manualmente.\nMessaggio di nascita: il messaggio di nascita verr\u00e0 inviato ogni volta che Home Assistant si (ri)collega al broker MQTT.\nMessaggio testamento: Il messaggio testamento verr\u00e0 inviato ogni volta che Home Assistant perde la connessione al broker, sia in caso di buona (es. arresto di Home Assistant) sia in caso di cattiva (es. Home Assistant in crash o perdita della connessione di rete) disconnessione.", + "title": "Opzioni MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index e7631c5805d37..dccd49b2ef3eb 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -21,8 +21,8 @@ "data": { "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654" }, - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hass.io \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4" + "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Home Assistant \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4" } } }, diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index cac483b1bf050..e9c2469a06165 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -21,8 +21,8 @@ "data": { "discovery": "Detectie inschakelen" }, - "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de Supervisor add-on {addon} ?", - "title": "MQTT Broker via Supervisor add-on" + "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de add-on {addon}?", + "title": "MQTT Broker via Home Assistant add-on" } } }, @@ -62,7 +62,8 @@ "port": "Poort", "username": "Gebruikersnaam" }, - "description": "Voer de verbindingsgegevens van uw MQTT-broker in." + "description": "Voer de verbindingsgegevens van uw MQTT-broker in.", + "title": "Broker opties" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Will message behouden", "will_topic": "Will message topic" }, - "description": "Selecteer MQTT-opties." + "description": "Detectie - Als detectie is ingeschakeld (aanbevolen), zal Home Assistant automatisch apparaten en entiteiten detecteren die hun configuratie publiceren op de MQTT-broker. Als detectie is uitgeschakeld, moet alle configuratie handmatig worden uitgevoerd.\n Birth message - Het birth message wordt elke keer dat Home Assistant (opnieuw) verbinding maakt met de MQTT-broker, verzonden.\n Will message - Het will message wordt telkens verzonden wanneer Home Assistant de verbinding met de broker verliest, zowel in het geval van een schone (bijv. Home Assistant wordt uitgeschakeld) als in geval van een onjuiste (bijv. Home Assistant crasht of verliest de netwerkverbinding) verbroken verbinding.", + "title": "MQTT-opties" } } } diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 586c62dac6a36..fee6505862ae8 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -21,8 +21,8 @@ "data": { "discovery": "Aktiver oppdagelse" }, - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til MQTT-megleren levert av Hass.io-tillegget {addon} ?", - "title": "MQTT Megler via Hass.io-tillegg" + "description": "Vil du konfigurere Home Assistant for \u00e5 koble til MQTT-megleren levert av tillegget {addon} ?", + "title": "MQTT Broker via Home Assistant-tillegg" } } }, @@ -62,7 +62,8 @@ "port": "Port", "username": "Brukernavn" }, - "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler." + "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", + "title": "Megleralternativer" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Testament melding behold", "will_topic": "Testament melding emne" }, - "description": "Vennligst velg MQTT-alternativer." + "description": "Discovery - Hvis oppdagelse er aktivert (anbefales), vil Home Assistant automatisk oppdage enheter og enheter som publiserer konfigurasjonen p\u00e5 MQTT-megleren. Hvis s\u00f8k er deaktivert, m\u00e5 all konfigurasjon utf\u00f8res manuelt.\nF\u00f8dselsmelding - F\u00f8dselsmeldingen vil bli sendt hver gang Home Assistant (re) kobles til MQTT megleren.\nWill message - Will-meldingen vil bli sendt hver gang Home Assistant mister forbindelsen til megleren, b\u00e5de i tilfelle en ren (f.eks. at Home Assistant avsluttes) og i tilfelle en uren (f.eks. hjemmeassistent krasjer eller mister nettverkstilkoblingen) koble fra.", + "title": "MQTT-alternativer" } } } diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 287f0165d9655..2103cc2c441be 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -21,8 +21,8 @@ "data": { "discovery": "W\u0142\u0105cz wykrywanie" }, - "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?", - "title": "Po\u015brednik MQTT przez dodatek Hass.io" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek {addon}?", + "title": "Po\u015brednik MQTT przez dodatek Home Assistant" } } }, @@ -62,7 +62,8 @@ "port": "Port", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT" + "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT", + "title": "Opcje brokera" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Flaga \"retain\" wiadomo\u015bci \"will\"", "will_topic": "Temat wiadomo\u015bci \"will\"" }, - "description": "Opcje MQTT" + "description": "Wykrywanie - je\u015bli wykrywanie jest w\u0142\u0105czone (zalecane), Home Assistant automatycznie wykryje urz\u0105dzenia i encje, kt\u00f3re publikuj\u0105 swoj\u0105 konfiguracj\u0119 w brokerze MQTT. Je\u015bli wykrywanie jest wy\u0142\u0105czone, ca\u0142\u0105 konfiguracj\u0119 nale\u017cy wykona\u0107 r\u0119cznie.\nWiadomo\u015b\u0107 Birth - wiadomo\u015b\u0107 Birth zostanie wys\u0142ana za ka\u017cdym razem, gdy Home Assistant (ponownie) po\u0142\u0105czy si\u0119 z brokerem MQTT.\nWiadomo\u015b\u0107 Will - wiadomo\u015b\u0107 Will b\u0119dzie wysy\u0142ana za ka\u017cdym razem, gdy Home Assistant utraci po\u0142\u0105czenie z brokerem, zar\u00f3wno w przypadku czystego (np. wy\u0142\u0105czenie Home Assistanta), jak i w przypadku nieczystego (np. zawieszenie Home Assistanta lub utrata po\u0142\u0105czenia sieciowego) roz\u0142\u0105czenia.", + "title": "Opcje MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 4357a0902c6ec..321a1e5e56c02 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -21,7 +21,7 @@ "data": { "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" }, - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" } } @@ -62,7 +62,8 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0411\u0440\u043e\u043a\u0435\u0440\u0430" }, "options": { "data": { @@ -72,13 +73,14 @@ "birth_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "birth_topic": "\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)", "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", - "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_topic": "\u0422\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MQTT." + "description": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u2014 \u0435\u0441\u043b\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f), Home Assistant \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0443\u0431\u043b\u0438\u043a\u0443\u044e\u0442 \u0441\u0432\u043e\u044e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043d\u0430 \u0431\u0440\u043e\u043a\u0435\u0440\u0435 MQTT. \u0415\u0441\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e, \u0432\u0441\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c\u0441\u044f \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437, \u043a\u043e\u0433\u0434\u0430 Home Assistant \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.\n\u0422\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u2014 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u043a\u0430\u043a \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043e\u0442 \u0431\u0440\u043e\u043a\u0435\u0440\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Home Assistant), \u0442\u0430\u043a \u0438 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0441\u0431\u043e\u0435 Home Assistant \u0438\u043b\u0438 \u043f\u043e\u0442\u0435\u0440\u0435 \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f).", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b MQTT" } } } diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 807de2e2c0913..a8dc6d4ce9e18 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -21,8 +21,8 @@ "data": { "discovery": "\u958b\u555f\u641c\u5c0b" }, - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u5143\u4ef6 {addon} \u4e4b MQTT broker\uff1f", - "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u5143\u4ef6 MQTT Broker" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 MQTT broker\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", + "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 MQTT Broker" } } }, @@ -62,7 +62,8 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002", + "title": "Broker \u9078\u9805" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Will \u8a0a\u606f Retain", "will_topic": "Will \u8a0a\u606f\u4e3b\u984c" }, - "description": "\u8acb\u9078\u64c7 MQTT \u9078\u9805\u3002" + "description": "Discovery - \u5047\u5982\u63a2\u7d22\uff08Discovery\uff09\u529f\u80fd\u958b\u555f\uff08\u5efa\u8b70\uff09\uff0cHome Assistant \u5c07\u6703\u81ea\u52d5\u767c\u73fe\u88dd\u7f6e\u8207\u5be6\u9ad4\u3001\u4e26\u767c\u5e03\u5176\u8a2d\u5b9a\u81f3 MQTT Broker\u3002\u5047\u5982\u63a2\u7d22\u95dc\u9589\u7684\u8a71\uff0c\u6240\u6709\u8a2d\u5b9a\u5fc5\u9808\u624b\u52d5\u9032\u884c\u3002\nBirth \u8a0a\u606f - Birth \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u9023\u7dda\u81f3 MQTT Broker \u6642\u50b3\u9001\u3002\nWill \u8a0a\u606f - Will \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u81ea Broker \u65b7\u7dda\u6642\u50b3\u9001\u3001\u540c\u6642\u5305\u542b\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u95dc\u6a5f\uff09\u53ca\u975e\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u7576\u6a5f\u6216\u65b7\u7dda\uff09\u72c0\u6cc1\u3002", + "title": "MQTT \u9078\u9805" } } } diff --git a/homeassistant/components/mqtt_eventstream/manifest.json b/homeassistant/components/mqtt_eventstream/manifest.json index 87eb6bee31e2b..ec1fa9d2a5c64 100644 --- a/homeassistant/components/mqtt_eventstream/manifest.json +++ b/homeassistant/components/mqtt_eventstream/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT Eventstream", "documentation": "https://www.home-assistant.io/integrations/mqtt_eventstream", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 8f64636b81742..2d14001b61b09 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -5,7 +5,9 @@ import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.components.mqtt import CONF_QOS from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -29,7 +31,7 @@ extra=vol.ALLOW_EXTRA, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} ) diff --git a/homeassistant/components/mqtt_json/manifest.json b/homeassistant/components/mqtt_json/manifest.json index 353ca20d5d77b..8a603f3539c50 100644 --- a/homeassistant/components/mqtt_json/manifest.json +++ b/homeassistant/components/mqtt_json/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT JSON", "documentation": "https://www.home-assistant.io/integrations/mqtt_json", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mqtt_room/manifest.json b/homeassistant/components/mqtt_room/manifest.json index 814435ea8355c..5a5197550ad2c 100644 --- a/homeassistant/components/mqtt_room/manifest.json +++ b/homeassistant/components/mqtt_room/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT Room Presence", "documentation": "https://www.home-assistant.io/integrations/mqtt_room", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index e446ab8ba7a09..b40d550abf600 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -120,7 +120,7 @@ def message_received(msg): if ( device.get(ATTR_ROOM) == self._state or device.get(ATTR_DISTANCE) < self._distance - or timediff.seconds >= self._timeout + or timediff.total_seconds() >= self._timeout ): update_state(**device) diff --git a/homeassistant/components/mqtt_statestream/manifest.json b/homeassistant/components/mqtt_statestream/manifest.json index eb8556d8d9ffb..dec6d4d09d2b9 100644 --- a/homeassistant/components/mqtt_statestream/manifest.json +++ b/homeassistant/components/mqtt_statestream/manifest.json @@ -3,5 +3,6 @@ "name": "MQTT Statestream", "documentation": "https://www.home-assistant.io/integrations/mqtt_statestream", "dependencies": ["mqtt"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/msteams/manifest.json b/homeassistant/components/msteams/manifest.json index 184e50915a55d..3024bfb310ba3 100644 --- a/homeassistant/components/msteams/manifest.json +++ b/homeassistant/components/msteams/manifest.json @@ -3,5 +3,6 @@ "name": "Microsoft Teams", "documentation": "https://www.home-assistant.io/integrations/msteams", "requirements": ["pymsteams==0.1.12"], - "codeowners": ["@peroyvind"] + "codeowners": ["@peroyvind"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 541c6075cc3bf..d89c947a4f383 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -1,5 +1,4 @@ """The Mullvad VPN integration.""" -import asyncio from datetime import timedelta import logging @@ -15,11 +14,6 @@ PLATFORMS = ["binary_sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Mullvad VPN integration.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: dict): """Set up Mullvad VPN integration.""" @@ -39,25 +33,14 @@ async def async_get_mullvad_api_data(): hass.data[DOMAIN] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN] diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py index 50f67d10e257b..1b330d4f6a3a8 100644 --- a/homeassistant/components/mullvad/config_flow.py +++ b/homeassistant/components/mullvad/config_flow.py @@ -14,12 +14,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Mullvad VPN.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() errors = {} if user_input is not None: diff --git a/homeassistant/components/mullvad/manifest.json b/homeassistant/components/mullvad/manifest.json index 1a440240d7e1c..6a9bf2017ab9f 100644 --- a/homeassistant/components/mullvad/manifest.json +++ b/homeassistant/components/mullvad/manifest.json @@ -3,10 +3,7 @@ "name": "Mullvad VPN", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mullvad", - "requirements": [ - "mullvad-api==1.0.0" - ], - "codeowners": [ - "@meichthys" - ] + "requirements": ["mullvad-api==1.0.0"], + "codeowners": ["@meichthys"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mullvad/translations/bg.json b/homeassistant/components/mullvad/translations/bg.json index a84e1c3bfdf35..5d274ec2b73c7 100644 --- a/homeassistant/components/mullvad/translations/bg.json +++ b/homeassistant/components/mullvad/translations/bg.json @@ -2,14 +2,6 @@ "config": { "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" - }, - "step": { - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/ca.json b/homeassistant/components/mullvad/translations/ca.json index f81781cbc0fa4..0dc1f0bd4a9ec 100644 --- a/homeassistant/components/mullvad/translations/ca.json +++ b/homeassistant/components/mullvad/translations/ca.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "step": { "user": { - "data": { - "host": "Amfitri\u00f3", - "password": "Contrasenya", - "username": "Nom d'usuari" - }, "description": "Vols configurar la integraci\u00f3 Mullvad VPN?" } } diff --git a/homeassistant/components/mullvad/translations/cs.json b/homeassistant/components/mullvad/translations/cs.json index 0f02cd974c207..0887542d78417 100644 --- a/homeassistant/components/mullvad/translations/cs.json +++ b/homeassistant/components/mullvad/translations/cs.json @@ -5,17 +5,7 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "user": { - "data": { - "host": "Hostitel", - "password": "Heslo", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/de.json b/homeassistant/components/mullvad/translations/de.json index 6014a9155c81b..be3beba93fd5b 100644 --- a/homeassistant/components/mullvad/translations/de.json +++ b/homeassistant/components/mullvad/translations/de.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Passwort", - "username": "Benutzername" - }, "description": "Mullvad VPN Integration einrichten?" } } diff --git a/homeassistant/components/mullvad/translations/el.json b/homeassistant/components/mullvad/translations/el.json index 6f19f0039edda..06559d04b01b6 100644 --- a/homeassistant/components/mullvad/translations/el.json +++ b/homeassistant/components/mullvad/translations/el.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_auth": "\u0386\u03ba\u03c5\u03c1\u03b7 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "user": { - "data": { - "host": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" - }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Mullvad VPN;" } } diff --git a/homeassistant/components/mullvad/translations/en.json b/homeassistant/components/mullvad/translations/en.json index fcfa89ef0829e..45664554aed74 100644 --- a/homeassistant/components/mullvad/translations/en.json +++ b/homeassistant/components/mullvad/translations/en.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Password", - "username": "Username" - }, "description": "Set up the Mullvad VPN integration?" } } diff --git a/homeassistant/components/mullvad/translations/es.json b/homeassistant/components/mullvad/translations/es.json index 579726b061e52..f7ad856ea9105 100644 --- a/homeassistant/components/mullvad/translations/es.json +++ b/homeassistant/components/mullvad/translations/es.json @@ -5,15 +5,10 @@ }, "error": { "cannot_connect": "Fallo al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, "step": { "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Usuario" - }, "description": "\u00bfConfigurar la integraci\u00f3n VPN de Mullvad?" } } diff --git a/homeassistant/components/mullvad/translations/et.json b/homeassistant/components/mullvad/translations/et.json index 671d18a2cd344..66e74822d18fd 100644 --- a/homeassistant/components/mullvad/translations/et.json +++ b/homeassistant/components/mullvad/translations/et.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\u00dchendumine nurjus", - "invalid_auth": "Tuvastamise viga", "unknown": "Ootamatu t\u00f5rge" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Salas\u00f5na", - "username": "Kasutajanimi" - }, "description": "Kas seadistada Mullvad VPN sidumine?" } } diff --git a/homeassistant/components/mullvad/translations/fr.json b/homeassistant/components/mullvad/translations/fr.json index 1a8b10de809cf..542412da986ab 100644 --- a/homeassistant/components/mullvad/translations/fr.json +++ b/homeassistant/components/mullvad/translations/fr.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, "step": { "user": { - "data": { - "host": "H\u00f4te", - "password": "Mot de passe", - "username": "Nom d'utilisateur" - }, "description": "Configurez l'int\u00e9gration VPN Mullvad?" } } diff --git a/homeassistant/components/mullvad/translations/he.json b/homeassistant/components/mullvad/translations/he.json index 7f60f15d598ac..1551f5e6bb03d 100644 --- a/homeassistant/components/mullvad/translations/he.json +++ b/homeassistant/components/mullvad/translations/he.json @@ -5,17 +5,7 @@ }, "error": { "cannot_connect": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05ea\u05e7\u05d9\u05df", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "user": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/hu.json b/homeassistant/components/mullvad/translations/hu.json index 0abcc301f0c85..e92d5c4bdea94 100644 --- a/homeassistant/components/mullvad/translations/hu.json +++ b/homeassistant/components/mullvad/translations/hu.json @@ -5,17 +5,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "step": { - "user": { - "data": { - "host": "Hoszt", - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/id.json b/homeassistant/components/mullvad/translations/id.json index a5409549f192d..1bab139542270 100644 --- a/homeassistant/components/mullvad/translations/id.json +++ b/homeassistant/components/mullvad/translations/id.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Gagal terhubung", - "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Kata Sandi", - "username": "Nama Pengguna" - }, "description": "Siapkan integrasi VPN Mullvad?" } } diff --git a/homeassistant/components/mullvad/translations/it.json b/homeassistant/components/mullvad/translations/it.json index 47cd8290f215a..7b1941a5ee8d0 100644 --- a/homeassistant/components/mullvad/translations/it.json +++ b/homeassistant/components/mullvad/translations/it.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Password", - "username": "Nome utente" - }, "description": "Configurare l'integrazione VPN Mullvad?" } } diff --git a/homeassistant/components/mullvad/translations/ko.json b/homeassistant/components/mullvad/translations/ko.json index fd9134b977c4f..09f9ce4771ca4 100644 --- a/homeassistant/components/mullvad/translations/ko.json +++ b/homeassistant/components/mullvad/translations/ko.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { "user": { - "data": { - "host": "\ud638\uc2a4\ud2b8", - "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, "description": "Mullvad VPN \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } diff --git a/homeassistant/components/mullvad/translations/nl.json b/homeassistant/components/mullvad/translations/nl.json index aa4d80ac71dd6..e056a50a0912c 100644 --- a/homeassistant/components/mullvad/translations/nl.json +++ b/homeassistant/components/mullvad/translations/nl.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { "user": { - "data": { - "host": "Host", - "password": "Wachtwoord", - "username": "Gebruikersnaam" - }, "description": "De Mullvad VPN-integratie instellen?" } } diff --git a/homeassistant/components/mullvad/translations/no.json b/homeassistant/components/mullvad/translations/no.json index d33f26404452b..232c581b0b119 100644 --- a/homeassistant/components/mullvad/translations/no.json +++ b/homeassistant/components/mullvad/translations/no.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "step": { "user": { - "data": { - "host": "Vert", - "password": "Passord", - "username": "Brukernavn" - }, "description": "Sette opp Mullvad VPN-integrasjon?" } } diff --git a/homeassistant/components/mullvad/translations/pl.json b/homeassistant/components/mullvad/translations/pl.json index f5aca4e092c9a..249d2f1ff24bc 100644 --- a/homeassistant/components/mullvad/translations/pl.json +++ b/homeassistant/components/mullvad/translations/pl.json @@ -5,16 +5,10 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { - "data": { - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" - }, "description": "Skonfigurowa\u0107 integracj\u0119 Mullvad VPN?" } } diff --git a/homeassistant/components/mullvad/translations/pt.json b/homeassistant/components/mullvad/translations/pt.json index 561c8d77287ef..657ce03e544fe 100644 --- a/homeassistant/components/mullvad/translations/pt.json +++ b/homeassistant/components/mullvad/translations/pt.json @@ -5,17 +5,7 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "host": "Servidor", - "password": "Palavra-passe", - "username": "Nome de Utilizador" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/ru.json b/homeassistant/components/mullvad/translations/ru.json index af2ecd321f00d..cf22d6e4f492a 100644 --- a/homeassistant/components/mullvad/translations/ru.json +++ b/homeassistant/components/mullvad/translations/ru.json @@ -5,16 +5,10 @@ }, "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": { - "host": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Mullvad VPN." } } diff --git a/homeassistant/components/mullvad/translations/sv.json b/homeassistant/components/mullvad/translations/sv.json new file mode 100644 index 0000000000000..c5ad71d784df8 --- /dev/null +++ b/homeassistant/components/mullvad/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/tr.json b/homeassistant/components/mullvad/translations/tr.json deleted file mode 100644 index 0f3ddabfc4f44..0000000000000 --- a/homeassistant/components/mullvad/translations/tr.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/zh-Hans.json b/homeassistant/components/mullvad/translations/zh-Hans.json index acb02a7d0f630..ae40024a84147 100644 --- a/homeassistant/components/mullvad/translations/zh-Hans.json +++ b/homeassistant/components/mullvad/translations/zh-Hans.json @@ -5,16 +5,7 @@ }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "invalid_auth": "\u9a8c\u8bc1\u5931\u8d25", "unknown": "\u9884\u671f\u5916\u7684\u9519\u8bef" - }, - "step": { - "user": { - "data": { - "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mullvad/translations/zh-Hant.json b/homeassistant/components/mullvad/translations/zh-Hant.json index d78c36b72d77c..dedfa2febbc91 100644 --- a/homeassistant/components/mullvad/translations/zh-Hant.json +++ b/homeassistant/components/mullvad/translations/zh-Hant.json @@ -1,20 +1,14 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "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": { - "host": "\u4e3b\u6a5f\u7aef", - "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, "description": "\u8a2d\u5b9a Mullvad VPN \u6574\u5408\uff1f" } } diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py new file mode 100644 index 0000000000000..7bee5ff5a9bee --- /dev/null +++ b/homeassistant/components/mutesync/__init__.py @@ -0,0 +1,54 @@ +"""The mütesync integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import async_timeout +import mutesync + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import update_coordinator + +from .const import DOMAIN + +PLATFORMS = ["binary_sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up mütesync from a config entry.""" + client = mutesync.PyMutesync( + entry.data["token"], + entry.data["host"], + hass.helpers.aiohttp_client.async_get_clientsession(), + ) + + async def update_data(): + """Update the data.""" + async with async_timeout.timeout(2.5): + return await client.get_state() + + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = update_coordinator.DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name=DOMAIN, + update_interval=timedelta(seconds=5), + update_method=update_data, + ) + await coordinator.async_config_entry_first_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/mutesync/binary_sensor.py b/homeassistant/components/mutesync/binary_sensor.py new file mode 100644 index 0000000000000..a2f87bf9017b0 --- /dev/null +++ b/homeassistant/components/mutesync/binary_sensor.py @@ -0,0 +1,53 @@ +"""mütesync binary sensor entities.""" +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers import update_coordinator + +from .const import DOMAIN + +SENSORS = { + "in_meeting": "In Meeting", + "muted": "Muted", +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the mütesync button.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [MuteStatus(coordinator, sensor_type) for sensor_type in SENSORS], True + ) + + +class MuteStatus(update_coordinator.CoordinatorEntity, BinarySensorEntity): + """Mütesync binary sensors.""" + + def __init__(self, coordinator, sensor_type): + """Initialize our sensor.""" + super().__init__(coordinator) + self._sensor_type = sensor_type + + @property + def name(self): + """Return the name of the sensor.""" + return SENSORS[self._sensor_type] + + @property + def unique_id(self): + """Return the unique ID of the sensor.""" + return f"{self.coordinator.data['user-id']}-{self._sensor_type}" + + @property + def is_on(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._sensor_type] + + @property + def device_info(self): + """Return the device info of the sensor.""" + return { + "identifiers": {(DOMAIN, self.coordinator.data["user-id"])}, + "name": "mutesync", + "manufacturer": "mütesync", + "model": "mutesync app", + "entry_type": "service", + } diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py new file mode 100644 index 0000000000000..e4964d552b05a --- /dev/null +++ b/homeassistant/components/mutesync/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for mütesync integration.""" +from __future__ import annotations + +import asyncio +from typing import Any + +import aiohttp +import async_timeout +import mutesync +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema({"host": str}) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + session = hass.helpers.aiohttp_client.async_get_clientsession() + try: + async with async_timeout.timeout(5): + token = await mutesync.authenticate(session, data["host"]) + except aiohttp.ClientResponseError as error: + if error.status == 403: + raise InvalidAuth from error + raise CannotConnect from error + except (aiohttp.ClientError, asyncio.TimeoutError) as error: + raise CannotConnect from error + + return token + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for mütesync.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + token = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input["host"], + data={"token": token, "host": user_input["host"]}, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/mutesync/const.py b/homeassistant/components/mutesync/const.py new file mode 100644 index 0000000000000..fcf05584f4285 --- /dev/null +++ b/homeassistant/components/mutesync/const.py @@ -0,0 +1,3 @@ +"""Constants for the mütesync integration.""" + +DOMAIN = "mutesync" diff --git a/homeassistant/components/mutesync/manifest.json b/homeassistant/components/mutesync/manifest.json new file mode 100644 index 0000000000000..74e6d89d9f854 --- /dev/null +++ b/homeassistant/components/mutesync/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "mutesync", + "name": "mutesync", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/mutesync", + "requirements": ["mutesync==0.0.1"], + "iot_class": "local_polling", + "codeowners": [ + "@currentoor" + ] +} diff --git a/homeassistant/components/mutesync/strings.json b/homeassistant/components/mutesync/strings.json new file mode 100644 index 0000000000000..9b18620acf88f --- /dev/null +++ b/homeassistant/components/mutesync/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "Enable authentication in mütesync Preferences > Authentication", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/mutesync/translations/ca.json b/homeassistant/components/mutesync/translations/ca.json new file mode 100644 index 0000000000000..c97e9814abb89 --- /dev/null +++ b/homeassistant/components/mutesync/translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Activa l'autenticaci\u00f3 a Prefer\u00e8ncies de m\u00fctesync > Autenticaci\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/cs.json b/homeassistant/components/mutesync/translations/cs.json new file mode 100644 index 0000000000000..246a84fa62f18 --- /dev/null +++ b/homeassistant/components/mutesync/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/de.json b/homeassistant/components/mutesync/translations/de.json new file mode 100644 index 0000000000000..613cac29b1c67 --- /dev/null +++ b/homeassistant/components/mutesync/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Aktivieredie Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/en.json b/homeassistant/components/mutesync/translations/en.json new file mode 100644 index 0000000000000..0152f03bc2ad5 --- /dev/null +++ b/homeassistant/components/mutesync/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Enable authentication in m\u00fctesync Preferences > Authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/es.json b/homeassistant/components/mutesync/translations/es.json new file mode 100644 index 0000000000000..fb32193010eab --- /dev/null +++ b/homeassistant/components/mutesync/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Activar la autenticaci\u00f3n en las Preferencias de m\u00fctesync > Autenticaci\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/et.json b/homeassistant/components/mutesync/translations/et.json new file mode 100644 index 0000000000000..5f4e2c8739e03 --- /dev/null +++ b/homeassistant/components/mutesync/translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Luba tuvastamine jaotises m\u00fctesync Preferences > Authentication", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/fr.json b/homeassistant/components/mutesync/translations/fr.json new file mode 100644 index 0000000000000..7a292eeeeaef9 --- /dev/null +++ b/homeassistant/components/mutesync/translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Erreur de connexion", + "invalid_auth": "Activer l'authentification dans Pr\u00e9f\u00e9rences > Authentification de m\u00fctesync", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/it.json b/homeassistant/components/mutesync/translations/it.json new file mode 100644 index 0000000000000..c1d52c2be2697 --- /dev/null +++ b/homeassistant/components/mutesync/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Abilita l'autenticazione in m\u00fctesync Preferenze > Autenticazione", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/nl.json b/homeassistant/components/mutesync/translations/nl.json new file mode 100644 index 0000000000000..1b3dc36f659ff --- /dev/null +++ b/homeassistant/components/mutesync/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Activeer authenticatie in m\u00fctesync Voorkeuren > Authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/no.json b/homeassistant/components/mutesync/translations/no.json new file mode 100644 index 0000000000000..14e4738567ebc --- /dev/null +++ b/homeassistant/components/mutesync/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Aktiver autentisering i m\u00fctesync-innstillinger > Autentisering", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/pl.json b/homeassistant/components/mutesync/translations/pl.json new file mode 100644 index 0000000000000..dbc143f850405 --- /dev/null +++ b/homeassistant/components/mutesync/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "W\u0142\u0105cz uwierzytelnianie w Preferencjach m\u00fctesync > Uwierzytelnianie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/ru.json b/homeassistant/components/mutesync/translations/ru.json new file mode 100644 index 0000000000000..99164a766b6b2 --- /dev/null +++ b/homeassistant/components/mutesync/translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e m\u00fctesync \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mutesync/translations/zh-Hant.json b/homeassistant/components/mutesync/translations/zh-Hant.json new file mode 100644 index 0000000000000..c274757f78f1f --- /dev/null +++ b/homeassistant/components/mutesync/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u65bc m\u00fctesync \u7cfb\u7d71\u504f\u597d\u8a2d\u5b9a > \u8a8d\u8b49\u4e2d\u958b\u555f\u8a8d\u8b49", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mvglive/manifest.json b/homeassistant/components/mvglive/manifest.json index e676cb0438cf8..90c4b5a9ec08d 100644 --- a/homeassistant/components/mvglive/manifest.json +++ b/homeassistant/components/mvglive/manifest.json @@ -3,5 +3,6 @@ "name": "MVG", "documentation": "https://www.home-assistant.io/integrations/mvglive", "requirements": ["PyMVGLive==1.1.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/my/manifest.json b/homeassistant/components/my/manifest.json index 3b9e253f35327..8c88b092e1c7d 100644 --- a/homeassistant/components/my/manifest.json +++ b/homeassistant/components/my/manifest.json @@ -3,5 +3,6 @@ "name": "My Home Assistant", "documentation": "https://www.home-assistant.io/integrations/my", "dependencies": ["frontend"], - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/mychevy/__init__.py b/homeassistant/components/mychevy/__init__.py index 2b8bd65dfe8a0..5ea5b142657b7 100644 --- a/homeassistant/components/mychevy/__init__.py +++ b/homeassistant/components/mychevy/__init__.py @@ -145,11 +145,11 @@ def run(self): _LOGGER.info("Starting mychevy loop") self.update() self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) - time.sleep(MIN_TIME_BETWEEN_UPDATES.seconds) + time.sleep(MIN_TIME_BETWEEN_UPDATES.total_seconds()) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error updating mychevy data. " "This probably means the OnStar link is down again" ) self.hass.helpers.dispatcher.dispatcher_send(ERROR_TOPIC) - time.sleep(ERROR_SLEEP_TIME.seconds) + time.sleep(ERROR_SLEEP_TIME.total_seconds()) diff --git a/homeassistant/components/mychevy/manifest.json b/homeassistant/components/mychevy/manifest.json index 5c34290f425ad..e726d49bb6482 100644 --- a/homeassistant/components/mychevy/manifest.json +++ b/homeassistant/components/mychevy/manifest.json @@ -3,5 +3,6 @@ "name": "myChevrolet", "documentation": "https://www.home-assistant.io/integrations/mychevy", "requirements": ["mychevy==2.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/mycroft/manifest.json b/homeassistant/components/mycroft/manifest.json index 33fafacaa8884..21fc51fa9eeed 100644 --- a/homeassistant/components/mycroft/manifest.json +++ b/homeassistant/components/mycroft/manifest.json @@ -3,5 +3,6 @@ "name": "Mycroft", "documentation": "https://www.home-assistant.io/integrations/mycroft", "requirements": ["mycroftapi==2.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index b25751d7270db..a299968712aa3 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -1,5 +1,4 @@ """The MyQ integration.""" -import asyncio from datetime import timedelta import logging @@ -9,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -18,25 +17,17 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the MyQ component.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up MyQ from a config entry.""" + hass.data.setdefault(DOMAIN, {}) websession = aiohttp_client.async_get_clientsession(hass) conf = entry.data try: myq = await pymyq.login(conf[CONF_USERNAME], conf[CONF_PASSWORD], websession) except InvalidCredentialsError as err: - _LOGGER.error("There was an error while logging in: %s", err) - return False + raise ConfigEntryAuthFailed from err except MyQError as err: raise ConfigEntryNotReady from err @@ -45,6 +36,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_update_data(): try: return await myq.update_device_info() + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed from err except MyQError as err: raise UpdateFailed(str(err)) from err @@ -58,24 +51,14 @@ async def async_update_data(): hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index e3832458b9bd2..b1b3680343d06 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for device in myq.gateways.values(): entities.append(MyQBinarySensorEntity(coordinator, device)) - async_add_entities(entities, True) + async_add_entities(entities) class MyQBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 17c98195a4ed8..78a751a18b1e9 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -5,7 +5,7 @@ from pymyq.errors import InvalidCredentialsError, MyQError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client @@ -18,73 +18,81 @@ ) -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect. - - Data has the keys from DATA_SCHEMA with values provided by the user. - """ - - websession = aiohttp_client.async_get_clientsession(hass) - - try: - await pymyq.login(data[CONF_USERNAME], data[CONF_PASSWORD], websession) - except InvalidCredentialsError as err: - raise InvalidAuth from err - except MyQError as err: - raise CannotConnect from err - - return {"title": data[CONF_USERNAME]} - - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for MyQ.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Start a myq config flow.""" + self._reauth_unique_id = None + + async def _async_validate_input(self, username, password): + """Validate the user input allows us to connect.""" + websession = aiohttp_client.async_get_clientsession(self.hass) + try: + await pymyq.login(username, password, websession) + except InvalidCredentialsError: + return {CONF_PASSWORD: "invalid_auth"} + except MyQError: + return {"base": "cannot_connect"} + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return {"base": "unknown"} + + return None async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: - try: - info = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - if "base" not in errors: + errors = await self._async_validate_input( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) + if not errors: await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() - return self.async_create_entry(title=info["title"], data=user_input) + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, homekit_info): - """Handle HomeKit discovery.""" - if self._async_current_entries(): - # We can see myq on the network to tell them to configure - # it, but since the device will not give up the account it is - # bound to and there can be multiple myq gateways on a single - # account, we avoid showing the device as discovered once - # they already have one configured as they can always - # add a new one via "+" - return self.async_abort(reason="already_configured") - properties = { - key.lower(): value for (key, value) in homekit_info["properties"].items() - } - await self.async_set_unique_id(properties["id"]) - return await self.async_step_user() - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" + async def async_step_reauth(self, user_input=None): + """Handle reauth.""" + self._reauth_unique_id = self.context["unique_id"] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Handle reauth input.""" + errors = {} + existing_entry = await self.async_set_unique_id(self._reauth_unique_id) + if user_input is not None: + errors = await self._async_validate_input( + existing_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD] + ) + if not errors: + self.hass.config_entries.async_update_entry( + existing_entry, + data={ + **existing_entry.data, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + description_placeholders={ + CONF_USERNAME: existing_entry.data[CONF_USERNAME] + }, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index e26a969e724bd..3d587635f2d41 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator = data[MYQ_COORDINATOR] async_add_entities( - [MyQDevice(coordinator, device) for device in myq.covers.values()], True + [MyQDevice(coordinator, device) for device in myq.covers.values()] ) @@ -158,9 +158,3 @@ def device_info(self): if self._device.parent_device_id: device_info["via_device"] = (DOMAIN, self._device.parent_device_id) return device_info - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 2098480af523b..a93501c941f38 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -6,6 +6,8 @@ "codeowners": ["@bdraco"], "config_flow": true, "homekit": { - "models": ["819LMB"] - } + "models": ["819LMB", "MYQ"] + }, + "iot_class": "cloud_polling", + "dhcp": [{ "macaddress": "645299*" }] } diff --git a/homeassistant/components/myq/strings.json b/homeassistant/components/myq/strings.json index 19717907b0ffa..e8a0baa85ffca 100644 --- a/homeassistant/components/myq/strings.json +++ b/homeassistant/components/myq/strings.json @@ -7,7 +7,14 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } - } + }, + "reauth_confirm": { + "description": "The password for {username} is no longer valid.", + "title": "Reauthenticate your MyQ Account", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -15,6 +22,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } diff --git a/homeassistant/components/myq/translations/ca.json b/homeassistant/components/myq/translations/ca.json index 2b6549586a40b..1c61ce60154c9 100644 --- a/homeassistant/components/myq/translations/ca.json +++ b/homeassistant/components/myq/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya de {username} ja no \u00e9s v\u00e0lida.", + "title": "Torna a autenticar el compte MyQ" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/myq/translations/cs.json b/homeassistant/components/myq/translations/cs.json index 7c0c1ae503c35..c13753adccddb 100644 --- a/homeassistant/components/myq/translations/cs.json +++ b/homeassistant/components/myq/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Slu\u017eba je ji\u017e nastavena" + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", @@ -9,6 +10,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/myq/translations/de.json b/homeassistant/components/myq/translations/de.json index fafa38c781764..5d8b5acdc7996 100644 --- a/homeassistant/components/myq/translations/de.json +++ b/homeassistant/components/myq/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Das Passwort f\u00fcr {username} ist nicht mehr g\u00fcltig.", + "title": "Authentifizieren Sie Ihr MyQ-Konto erneut" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/myq/translations/en.json b/homeassistant/components/myq/translations/en.json index 9dad2d10cad48..5dc6d811c8704 100644 --- a/homeassistant/components/myq/translations/en.json +++ b/homeassistant/components/myq/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is no longer valid.", + "title": "Reauthenticate your MyQ Account" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index a405883fd3c68..08d2ccbee733a 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "MyQ ya est\u00e1 configurado" + "already_configured": "MyQ ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", @@ -9,6 +10,13 @@ "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "La contrase\u00f1a de {username} ya no es v\u00e1lida.", + "title": "Reautenticar tu cuenta MyQ" + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/myq/translations/et.json b/homeassistant/components/myq/translations/et.json index b51044a9e6ea8..c251d10177e38 100644 --- a/homeassistant/components/myq/translations/et.json +++ b/homeassistant/components/myq/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Teenus on juba seadistatud" + "already_configured": "Teenus on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchenduse loomine nurjus. Proovi uuesti", @@ -9,6 +10,13 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5ma" + }, + "description": "Kasutaja {username} salas\u00f5na ei kehti enam.", + "title": "Taastuvasta oma MyQ konto" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/myq/translations/fr.json b/homeassistant/components/myq/translations/fr.json index 4ae00e7495fef..e9a6bc60b82cf 100644 --- a/homeassistant/components/myq/translations/fr.json +++ b/homeassistant/components/myq/translations/fr.json @@ -9,6 +9,13 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "mot de passe" + }, + "description": "Le mot de passe de {username} n'est plus valide.", + "title": "R\u00e9authentifiez votre compte MyQ" + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/myq/translations/it.json b/homeassistant/components/myq/translations/it.json index ac793e62c6d2b..3ed9280b4620f 100644 --- a/homeassistant/components/myq/translations/it.json +++ b/homeassistant/components/myq/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "La password per {username} non \u00e8 pi\u00f9 valida.", + "title": "Riautentica il tuo account MyQ" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/myq/translations/nl.json b/homeassistant/components/myq/translations/nl.json index 65df320a544ef..09a3666541458 100644 --- a/homeassistant/components/myq/translations/nl.json +++ b/homeassistant/components/myq/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Service is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,13 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Het wachtwoord voor {username} is niet meer geldig.", + "title": "Verifieer uw MyQ account opnieuw" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/myq/translations/no.json b/homeassistant/components/myq/translations/no.json index c639088917ace..b43115f6b93de 100644 --- a/homeassistant/components/myq/translations/no.json +++ b/homeassistant/components/myq/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert" + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -9,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Passordet for {username} er ikke lenger gyldig.", + "title": "Godkjenn MyQ-kontoen din p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/myq/translations/pl.json b/homeassistant/components/myq/translations/pl.json index ee87eb52294bf..9bd66ab67d1cd 100644 --- a/homeassistant/components/myq/translations/pl.json +++ b/homeassistant/components/myq/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,6 +10,13 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Has\u0142o u\u017cytkownika {username} nie jest ju\u017c wa\u017cne.", + "title": "Ponownie uwierzytelnij konto MyQ" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/myq/translations/ru.json b/homeassistant/components/myq/translations/ru.json index c88db7d696094..d9a8e156ce014 100644 --- a/homeassistant/components/myq/translations/ru.json +++ b/homeassistant/components/myq/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +10,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f MyQ" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/myq/translations/zh-Hant.json b/homeassistant/components/myq/translations/zh-Hant.json index d50ff1810e343..fca168fdafe0b 100644 --- a/homeassistant/components/myq/translations/zh-Hant.json +++ b/homeassistant/components/myq/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "{username} \u5bc6\u78bc\u4e0d\u518d\u6709\u6548\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49 MyQ \u5e33\u865f" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index c9ad496762dce..9d23cfd24b601 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -19,7 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_DEVICES, @@ -58,18 +58,23 @@ DEFAULT_VERSION = "1.4" +def set_default_persistence_file(value: dict) -> dict: + """Set default persistence file.""" + for idx, gateway in enumerate(value): + fil = gateway.get(CONF_PERSISTENCE_FILE) + if fil is not None: + continue + new_name = f"mysensors{idx + 1}.pickle" + gateway[CONF_PERSISTENCE_FILE] = new_name + + return value + + def has_all_unique_files(value): """Validate that all persistence files are unique and set if any is set.""" - persistence_files = [gateway.get(CONF_PERSISTENCE_FILE) for gateway in value] - if None in persistence_files and any( - name is not None for name in persistence_files - ): - raise vol.Invalid( - "persistence file name of all devices must be set if any is set" - ) - if not all(name is None for name in persistence_files): - schema = vol.Schema(vol.Unique()) - schema(persistence_files) + persistence_files = [gateway[CONF_PERSISTENCE_FILE] for gateway in value] + schema = vol.Schema(vol.Unique()) + schema(persistence_files) return value @@ -128,7 +133,10 @@ def validator(config): deprecated(CONF_PERSISTENCE), { vol.Required(CONF_GATEWAYS): vol.All( - cv.ensure_list, has_all_unique_files, [GATEWAY_SCHEMA] + cv.ensure_list, + set_default_persistence_file, + has_all_unique_files, + [GATEWAY_SCHEMA], ), vol.Optional(CONF_RETAIN, default=True): cv.boolean, vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, @@ -142,7 +150,7 @@ def validator(config): ) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the MySensors component.""" hass.data[DOMAIN] = {DATA_HASS_CONFIG: config} @@ -159,7 +167,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: CONF_TOPIC_IN_PREFIX: gw.get(CONF_TOPIC_IN_PREFIX, ""), CONF_RETAIN: config[CONF_RETAIN], CONF_VERSION: config[CONF_VERSION], - CONF_PERSISTENCE_FILE: gw.get(CONF_PERSISTENCE_FILE) + CONF_PERSISTENCE_FILE: gw[CONF_PERSISTENCE_FILE] # nodes config ignored at this time. renaming nodes can now be done from the frontend. } for gw in config[CONF_GATEWAYS] @@ -182,7 +190,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an instance of the MySensors integration. Every instance has a connection to exactly one Gateway. @@ -234,18 +242,13 @@ async def finish() -> None: return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove an instance of the MySensors integration.""" gateway = get_mysensors_gateway(hass, entry.entry_id) - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS_WITH_ENTRY_SUPPORT - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, PLATFORMS_WITH_ENTRY_SUPPORT ) if not unload_ok: return False diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index c4e12d170c01e..2077f38c758c7 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,6 +1,4 @@ """Support for MySensors binary sensors.""" -from typing import Callable - from homeassistant.components import mysensors from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, @@ -16,9 +14,9 @@ from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.entity_platform import AddEntitiesCallback SENSORS = { "S_DOOR": "door", @@ -33,7 +31,9 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index b1916fc4ed104..f958f2274e06b 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -1,6 +1,4 @@ """MySensors platform that offers a Climate (MySensors-HVAC) component.""" -from typing import Callable - from homeassistant.components import mysensors from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -19,8 +17,9 @@ from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.entity_platform import AddEntitiesCallback DICT_HA_TO_MYS = { HVAC_MODE_AUTO: "AutoChangeOver", @@ -40,7 +39,9 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index bdf1b9392a82e..6676e11febfed 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -14,7 +14,11 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic +from homeassistant.components.mqtt import ( + DOMAIN as MQTT_DOMAIN, + valid_publish_topic, + valid_subscribe_topic, +) from homeassistant.components.mysensors import ( CONF_DEVICE, DEFAULT_BAUD_RATE, @@ -23,6 +27,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import CONF_RETAIN, CONF_VERSION, DEFAULT_VERSION @@ -135,18 +140,23 @@ async def async_step_user(self, user_input: dict[str, str] | None = None): """Create a config entry from frontend user input.""" schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)} schema = vol.Schema(schema) + errors = {} if user_input is not None: gw_type = self._gw_type = user_input[CONF_GATEWAY_TYPE] input_pass = user_input if CONF_DEVICE in user_input else None if gw_type == CONF_GATEWAY_TYPE_MQTT: - return await self.async_step_gw_mqtt(input_pass) + # Naive check that doesn't consider config entry state. + if MQTT_DOMAIN in self.hass.config.components: + return await self.async_step_gw_mqtt(input_pass) + + errors["base"] = "mqtt_required" if gw_type == CONF_GATEWAY_TYPE_TCP: return await self.async_step_gw_tcp(input_pass) if gw_type == CONF_GATEWAY_TYPE_SERIAL: return await self.async_step_gw_serial(input_pass) - return self.async_show_form(step_id="user", data_schema=schema) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) async def async_step_gw_serial(self, user_input: dict[str, str] | None = None): """Create config entry for a serial gateway.""" @@ -208,7 +218,7 @@ async def async_step_gw_tcp(self, user_input: dict[str, str] | None = None): return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors) def _check_topic_exists(self, topic: str) -> bool: - for other_config in self.hass.config_entries.async_entries(DOMAIN): + for other_config in self._async_current_entries(): if topic == other_config.data.get( CONF_TOPIC_IN_PREFIX ) or topic == other_config.data.get(CONF_TOPIC_OUT_PREFIX): @@ -272,7 +282,7 @@ async def async_step_gw_mqtt(self, user_input: dict[str, str] | None = None): @callback def _async_create_entry( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Create the config entry.""" return self.async_create_entry( title=f"{user_input[CONF_DEVICE]}", @@ -314,10 +324,12 @@ async def validate_common( except vol.Invalid: errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file" else: - real_persistence_path = self._normalize_persistence_file( + real_persistence_path = user_input[ + CONF_PERSISTENCE_FILE + ] = self._normalize_persistence_file( user_input[CONF_PERSISTENCE_FILE] ) - for other_entry in self.hass.config_entries.async_entries(DOMAIN): + for other_entry in self._async_current_entries(): if CONF_PERSISTENCE_FILE not in other_entry.data: continue if real_persistence_path == self._normalize_persistence_file( @@ -326,7 +338,7 @@ async def validate_common( errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file" break - for other_entry in self.hass.config_entries.async_entries(DOMAIN): + for other_entry in self._async_current_entries(): if _is_same_device(gw_type, user_input, other_entry): errors["base"] = "already_configured" break diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 7a9027d9b724e..1bd071be9a9ce 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -29,7 +29,6 @@ CONF_GATEWAY_TYPE_TCP, ] - DOMAIN: str = "mysensors" MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}" MYSENSORS_GATEWAYS: str = "mysensors_gateways" diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 33393f08defab..031efc9720978 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -1,7 +1,6 @@ """Support for MySensors covers.""" from enum import Enum, unique import logging -from typing import Callable from homeassistant.components import mysensors from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity @@ -9,8 +8,9 @@ from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.entity_platform import AddEntitiesCallback _LOGGER = logging.getLogger(__name__) @@ -26,7 +26,9 @@ class CoverState(Enum): async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 4e770f70bf0c9..c1d8c431bc0a6 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -3,7 +3,6 @@ from functools import partial import logging -from typing import Any from mysensors import BaseAsyncGateway, Sensor from mysensors.sensor import ChildSensor @@ -11,7 +10,7 @@ from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON 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 ( CHILD_CALLBACK, @@ -44,7 +43,7 @@ def __init__( node_id: int, child_id: int, value_type: int, - ): + ) -> None: """Set up the MySensors device.""" self.gateway_id: GatewayId = gateway_id self.gateway: BaseAsyncGateway = gateway @@ -109,7 +108,7 @@ def unique_id(self) -> str: return f"{self.gateway_id}-{self.node_id}-{self.child_id}-{self.value_type}" @property - def device_info(self) -> dict[str, Any] | None: + def device_info(self) -> DeviceInfo: """Return a dict that allows home assistant to puzzle all entities belonging to a node together.""" return { "identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")}, diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index 068029af9602f..45416ff7ae7e7 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -3,13 +3,13 @@ from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.mysensors import DevId, on_unload from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify async def async_setup_scanner( - hass: HomeAssistantType, config, async_see, discovery_info=None + hass: HomeAssistant, config, async_see, discovery_info=None ): """Set up the MySensors device scanner.""" if not discovery_info: @@ -53,7 +53,7 @@ async def async_setup_scanner( class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, hass: HomeAssistantType, async_see, *args): + def __init__(self, hass: HomeAssistant, async_see, *args): """Set up instance.""" super().__init__(*args) self.async_see = async_see diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 6cf8e7d738317..ec403e6e34b26 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -3,20 +3,21 @@ import asyncio from collections import defaultdict +from collections.abc import Coroutine import logging import socket import sys -from typing import Any, Callable, Coroutine +from typing import Any, Callable import async_timeout from mysensors import BaseAsyncGateway, Message, Sensor, mysensors import voluptuous as vol +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_BAUD_RATE, @@ -65,7 +66,7 @@ def is_socket_address(value): raise vol.Invalid("Device is not a valid domain name or ip address") from err -async def try_connect(hass: HomeAssistantType, user_input: dict[str, str]) -> bool: +async def try_connect(hass: HomeAssistant, user_input: dict[str, str]) -> bool: """Try to connect to a gateway and report if it worked.""" if user_input[CONF_DEVICE] == MQTT_COMPONENT: return True # dont validate mqtt. mqtt gateways dont send ready messages :( @@ -111,7 +112,7 @@ def on_conn_made(_: BaseAsyncGateway) -> None: def get_mysensors_gateway( - hass: HomeAssistantType, gateway_id: GatewayId + hass: HomeAssistant, gateway_id: GatewayId ) -> BaseAsyncGateway | None: """Return the Gateway for a given GatewayId.""" if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]: @@ -121,7 +122,7 @@ def get_mysensors_gateway( async def setup_gateway( - hass: HomeAssistantType, entry: ConfigEntry + hass: HomeAssistant, entry: ConfigEntry ) -> BaseAsyncGateway | None: """Set up the Gateway for the given ConfigEntry.""" @@ -143,7 +144,7 @@ async def setup_gateway( async def _get_gateway( - hass: HomeAssistantType, + hass: HomeAssistant, device: str, version: str, event_callback: Callable[[Message], None], @@ -162,9 +163,10 @@ async def _get_gateway( persistence_file = hass.config.path(persistence_file) if device == MQTT_COMPONENT: - # what is the purpose of this? - # if not await async_setup_component(hass, MQTT_COMPONENT, entry): - # return None + # Make sure the mqtt integration is set up. + # Naive check that doesn't consider config entry state. + if MQTT_DOMAIN not in hass.config.components: + return None mqtt = hass.components.mqtt def pub_callback(topic, payload, qos, retain): @@ -230,7 +232,7 @@ def internal_callback(msg): async def finish_setup( - hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway + hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway ): """Load any persistent devices and platforms and start gateway.""" discover_tasks = [] @@ -245,7 +247,7 @@ async def finish_setup( async def _discover_persistent_devices( - hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway + hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway ): """Discover platforms for devices loaded via persistence file.""" tasks = [] @@ -275,9 +277,7 @@ async def gw_stop(hass, entry: ConfigEntry, gateway: BaseAsyncGateway): await gateway.stop() -async def _gw_start( - hass: HomeAssistantType, entry: ConfigEntry, gateway: BaseAsyncGateway -): +async def _gw_start(hass: HomeAssistant, entry: ConfigEntry, gateway: BaseAsyncGateway): """Start the gateway.""" gateway_ready = asyncio.Event() @@ -316,7 +316,7 @@ async def stop_this_gw(_: Event): def _gw_callback_factory( - hass: HomeAssistantType, gateway_id: GatewayId + hass: HomeAssistant, gateway_id: GatewayId ) -> Callable[[Message], None]: """Return a new callback for the gateway.""" diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index d21140701f97c..8558cd01f4261 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -3,9 +3,8 @@ from mysensors import Message -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import decorator from .const import CHILD_CALLBACK, NODE_CALLBACK, DevId, GatewayId @@ -16,9 +15,7 @@ @HANDLERS.register("set") -async def handle_set( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message -) -> None: +async def handle_set(hass: HomeAssistant, gateway_id: GatewayId, msg: Message) -> None: """Handle a mysensors set message.""" validated = validate_set_msg(gateway_id, msg) _handle_child_update(hass, gateway_id, validated) @@ -26,7 +23,7 @@ async def handle_set( @HANDLERS.register("internal") async def handle_internal( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle a mysensors internal message.""" internal = msg.gateway.const.Internal(msg.sub_type) @@ -38,7 +35,7 @@ async def handle_internal( @HANDLERS.register("I_BATTERY_LEVEL") async def handle_battery_level( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an internal battery level message.""" _handle_node_update(hass, gateway_id, msg) @@ -46,7 +43,7 @@ async def handle_battery_level( @HANDLERS.register("I_HEARTBEAT_RESPONSE") async def handle_heartbeat( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an heartbeat.""" _handle_node_update(hass, gateway_id, msg) @@ -54,7 +51,7 @@ async def handle_heartbeat( @HANDLERS.register("I_SKETCH_NAME") async def handle_sketch_name( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an internal sketch name message.""" _handle_node_update(hass, gateway_id, msg) @@ -62,7 +59,7 @@ async def handle_sketch_name( @HANDLERS.register("I_SKETCH_VERSION") async def handle_sketch_version( - hass: HomeAssistantType, gateway_id: GatewayId, msg: Message + hass: HomeAssistant, gateway_id: GatewayId, msg: Message ) -> None: """Handle an internal sketch version message.""" _handle_node_update(hass, gateway_id, msg) @@ -70,7 +67,7 @@ async def handle_sketch_version( @callback def _handle_child_update( - hass: HomeAssistantType, gateway_id: GatewayId, validated: dict[str, list[DevId]] + hass: HomeAssistant, gateway_id: GatewayId, validated: dict[str, list[DevId]] ): """Handle a child update.""" signals: list[str] = [] @@ -94,7 +91,7 @@ def _handle_child_update( @callback -def _handle_node_update(hass: HomeAssistantType, gateway_id: GatewayId, msg: Message): +def _handle_node_update(hass: HomeAssistant, gateway_id: GatewayId, msg: Message): """Handle a node update.""" signal = NODE_CALLBACK.format(gateway_id, msg.node_id) async_dispatcher_send(hass, signal) diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 0d18b243520d5..9a35f67d49b41 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -4,7 +4,7 @@ from collections import defaultdict from enum import IntEnum import logging -from typing import Callable, DefaultDict +from typing import Callable from mysensors import BaseAsyncGateway, Message from mysensors.sensor import ChildSensor @@ -15,7 +15,6 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.decorator import Registry from .const import ( @@ -37,7 +36,7 @@ async def on_unload( - hass: HomeAssistantType, entry: ConfigEntry | GatewayId, fnct: Callable + hass: HomeAssistant, entry: ConfigEntry | GatewayId, fnct: Callable ) -> None: """Register a callback to be called when entry is unloaded. @@ -174,9 +173,9 @@ def validate_child( node_id: int, child: ChildSensor, value_type: int | None = None, -) -> DefaultDict[str, list[DevId]]: +) -> defaultdict[str, list[DevId]]: """Validate a child. Returns a dict mapping hass platform names to list of DevId.""" - validated: DefaultDict[str, list[DevId]] = defaultdict(list) + validated: defaultdict[str, list[DevId]] = defaultdict(list) pres: IntEnum = gateway.const.Presentation set_req: IntEnum = gateway.const.SetReq child_type_name: SensorType | None = next( diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index f90f9c5c81c77..aea99e3ee350a 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -1,6 +1,4 @@ """Support for MySensors lights.""" -from typing import Callable - from homeassistant.components import mysensors from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -16,9 +14,9 @@ from homeassistant.components.mysensors.const import MYSENSORS_DISCOVERY from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from homeassistant.util.color import rgb_hex_to_rgb_list @@ -26,7 +24,9 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index c7d439dedc44d..3b7695146ba09 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pymysensors==0.21.0"], "after_dependencies": ["mqtt"], "codeowners": ["@MartinHjelmare", "@functionpointer"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 1a5f7330ddffa..48ab6e5d3a255 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,6 +1,4 @@ """Support for MySensors sensors.""" -from typing import Callable - from awesomeversion import AwesomeVersion from homeassistant.components import mysensors @@ -25,8 +23,9 @@ VOLT, VOLUME_CUBIC_METERS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.entity_platform import AddEntitiesCallback SENSORS = { "V_TEMP": [None, "mdi:thermometer"], @@ -64,7 +63,9 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" diff --git a/homeassistant/components/mysensors/services.yaml b/homeassistant/components/mysensors/services.yaml index a93429550cd8d..e0fa5bf8e89d2 100644 --- a/homeassistant/components/mysensors/services.yaml +++ b/homeassistant/components/mysensors/services.yaml @@ -1,9 +1,19 @@ send_ir_code: + name: Send IR code description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on. fields: entity_id: - description: Name(s) of entities that should have the IR code set and be turned on. Platform dependent. + name: Entity + description: Name of entity that should have the IR code set and be turned on. Platform dependent. example: "switch.living_room_1_1" + selector: + entity: + integration: mysensors + domain: switch V_IR_SEND: + name: IR send description: IR code to send. + required: true example: "0xC284" + selector: + text: diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index 43a68f61e247a..54821877b4fd8 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -41,7 +41,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_publish_topic": "Invalid publish topic", "duplicate_topic": "Topic already in use", "same_topic": "Subscribe and publish topics are the same", @@ -52,6 +52,7 @@ "invalid_serial": "Invalid serial port", "invalid_device": "Invalid device", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "unknown": "[%key:common::config_flow::error::unknown%]" @@ -60,7 +61,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_subscribe_topic": "Invalid subscribe topic", + "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_publish_topic": "Invalid publish topic", "duplicate_topic": "Topic already in use", "same_topic": "Subscribe and publish topics are the same", diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index 14911e11090fb..32a6a9a120205 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,17 +1,16 @@ """Support for MySensors switches.""" -from typing import Callable - import voluptuous as vol from homeassistant.components import mysensors from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import on_unload from ...config_entries import ConfigEntry from ...helpers.dispatcher import async_dispatcher_connect -from ...helpers.typing import HomeAssistantType from .const import DOMAIN as MYSENSORS_DOMAIN, MYSENSORS_DISCOVERY, SERVICE_SEND_IR_CODE ATTR_IR_CODE = "V_IR_SEND" @@ -22,7 +21,9 @@ async def async_setup_entry( - hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up this platform for a specific ConfigEntry(==Gateway).""" device_class_map = { diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 844d9e51da1de..6e20f0bcbeec4 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -33,6 +33,7 @@ "invalid_serial": "Port s\u00e8rie inv\u00e0lid", "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid", "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida", + "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", "not_a_number": "Introdueix un n\u00famero", "port_out_of_range": "El n\u00famero de port ha d'estar entre 1 i 65535", "same_topic": "Els topics de publicaci\u00f3 i subscripci\u00f3 son els mateixos", diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index d05e2bdb47b61..c61c41771362a 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -3,10 +3,16 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", + "duplicate_persistence_file": "Persistenzdatei wird bereits verwendet", + "duplicate_topic": "Thema bereits in Verwendung", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_device": "Ung\u00fcltiges Ger\u00e4t", "invalid_ip": "Ung\u00fcltige IP-Adresse", + "invalid_persistence_file": "Ung\u00fcltige Persistenzdatei", + "invalid_port": "Ung\u00fcltige Portnummer", + "invalid_publish_topic": "Ung\u00fcltiges Ver\u00f6ffentlichungsthema", "invalid_serial": "Ung\u00fcltiger Serieller Port", + "invalid_subscribe_topic": "Ung\u00fcltiges Abonnementthema", "invalid_version": "Ung\u00fcltige MySensors Version", "not_a_number": "Bitte eine Nummer eingeben", "unknown": "Unerwarteter Fehler" @@ -19,6 +25,7 @@ "invalid_ip": "Ung\u00fcltige IP-Adresse", "invalid_serial": "Ung\u00fcltiger Serieller Port", "invalid_version": "Ung\u00fcltige MySensors Version", + "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", "not_a_number": "Bitte eine Nummer eingeben", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index 63af85488f0a0..7ca3516e50d1a 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -33,6 +33,7 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "same_topic": "Subscribe and publish topics are the same", diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 2a4b30910d17d..4bb5f5cfd1528 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -1,8 +1,11 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", "duplicate_persistence_file": "Archivo de persistencia ya en uso", "duplicate_topic": "Tema ya en uso", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_device": "Dispositivo no v\u00e1lido", "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida", "invalid_persistence_file": "Archivo de persistencia no v\u00e1lido", @@ -13,7 +16,8 @@ "invalid_version": "Versi\u00f3n inv\u00e1lida de MySensors", "not_a_number": "Por favor, introduzca un n\u00famero", "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", - "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos" + "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", + "unknown": "Error inesperado" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", @@ -29,6 +33,7 @@ "invalid_serial": "Puerto serie no v\u00e1lido", "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido", "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", + "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", "not_a_number": "Por favor, introduce un n\u00famero", "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index 0682610be97fe..7aff6b1c3da72 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -33,6 +33,7 @@ "invalid_serial": "Sobimatu jadaport", "invalid_subscribe_topic": "Kehtetu tellimisteema", "invalid_version": "Sobimatu MySensors versioon", + "mqtt_required": "MQTT sidumine on loomata", "not_a_number": "Sisesta number", "port_out_of_range": "Pordi number peab olema v\u00e4hemalt 1 ja k\u00f5ige rohkem 65535", "same_topic": "Tellimise ja avaldamise teemad kattuvad", diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json index 00f9831c035b3..e104c69e8154d 100644 --- a/homeassistant/components/mysensors/translations/fr.json +++ b/homeassistant/components/mysensors/translations/fr.json @@ -33,6 +33,7 @@ "invalid_serial": "Port s\u00e9rie non valide", "invalid_subscribe_topic": "Sujet d'abonnement non valide", "invalid_version": "Version de MySensors non valide", + "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", "not_a_number": "Veuillez saisir un nombre", "port_out_of_range": "Le num\u00e9ro de port doit \u00eatre au moins 1 et au plus 65535", "same_topic": "Les sujets de souscription et de publication sont identiques", diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index 7d4df1f12da2c..fefe3fd4b6c29 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -5,6 +5,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_device": "\u00c9rv\u00e9nytelen eszk\u00f6z", + "not_a_number": "Adj meg egy sz\u00e1mot.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index f256ddb95eb27..8b13912015138 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -33,6 +33,7 @@ "invalid_serial": "Porta seriale non valida", "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", + "mqtt_required": "L'integrazione MQTT non \u00e8 configurata", "not_a_number": "Per favore inserisci un numero", "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 49ddf987cef64..14055639f60be 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -33,6 +33,7 @@ "invalid_serial": "Ongeldige seri\u00eble poort", "invalid_subscribe_topic": "Ongeldig abonneer topic", "invalid_version": "Ongeldige MySensors-versie", + "mqtt_required": "De MQTT integratie is niet ingesteld", "not_a_number": "Voer een nummer in", "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", "same_topic": "De topics abonneren en publiceren zijn hetzelfde", diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index 9d028260a7686..f0e307a1ab2e5 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -33,6 +33,7 @@ "invalid_serial": "Ugyldig serieport", "invalid_subscribe_topic": "Ugyldig abonnementsemne", "invalid_version": "Ugyldig MySensors-versjon", + "mqtt_required": "MQTT-integrasjonen er ikke satt opp", "not_a_number": "Vennligst skriv inn et nummer", "port_out_of_range": "Portnummer m\u00e5 v\u00e6re minst 1 og maksimalt 65535", "same_topic": "Abonner og publiser emner er de samme", diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json index fa67ffe403042..f3233a01d5023 100644 --- a/homeassistant/components/mysensors/translations/pl.json +++ b/homeassistant/components/mysensors/translations/pl.json @@ -33,6 +33,7 @@ "invalid_serial": "Nieprawid\u0142owy port szeregowy", "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"", "invalid_version": "Nieprawid\u0142owa wersja MySensors", + "mqtt_required": "Integracja MQTT nie jest skonfigurowana", "not_a_number": "Prosz\u0119 wpisa\u0107 numer", "port_out_of_range": "Numer portu musi by\u0107 pomi\u0119dzy 1 a 65535", "same_topic": "Tematy \"subscribe\" i \"publish\" s\u0105 takie same", diff --git a/homeassistant/components/mysensors/translations/ro.json b/homeassistant/components/mysensors/translations/ro.json new file mode 100644 index 0000000000000..5a8cb19a928e9 --- /dev/null +++ b/homeassistant/components/mysensors/translations/ro.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "unknown": "Eroare nea\u0219teptat\u0103" + }, + "error": { + "already_configured": "Dispozitivul este deja configurat", + "invalid_auth": "Autentificare nereu\u0219it\u0103" + }, + "step": { + "gw_serial": { + "description": "Configurare gateway serial" + }, + "gw_tcp": { + "data": { + "device": "Adresa IP a gateway-ului", + "tcp_port": "port", + "version": "Versiunea SenzorulMeu" + }, + "description": "Configurare gateway Ethernet" + }, + "user": { + "data": { + "gateway_type": "Tip gateway" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index 6267970901761..16f23f6efffa5 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -33,6 +33,7 @@ "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", "invalid_subscribe_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438.", "invalid_version": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f MySensors.", + "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", "not_a_number": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043b\u043e.", "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 65535.", "same_topic": "\u0422\u043e\u043f\u0438\u043a\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.", diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index d0067c2d0ced0..234a2bd0b30dc 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d", "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d", @@ -20,7 +20,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "duplicate_persistence_file": "Persistence \u6a94\u6848\u5df2\u4f7f\u7528\u4e2d", "duplicate_topic": "\u4e3b\u984c\u5df2\u4f7f\u7528\u4e2d", @@ -33,6 +33,7 @@ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", + "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc", "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index 71a719be92ad6..5becef7fff26a 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mystrom", "requirements": ["python-mystrom==1.1.2"], "dependencies": ["http"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/mythicbeastsdns/manifest.json b/homeassistant/components/mythicbeastsdns/manifest.json index b710cd05c1305..50841f21f3a6e 100644 --- a/homeassistant/components/mythicbeastsdns/manifest.json +++ b/homeassistant/components/mythicbeastsdns/manifest.json @@ -3,5 +3,6 @@ "name": "Mythic Beasts DNS", "documentation": "https://www.home-assistant.io/integrations/mythicbeastsdns", "requirements": ["mbddns==0.1.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/n26/__init__.py b/homeassistant/components/n26/__init__.py deleted file mode 100644 index b1e83cd531130..0000000000000 --- a/homeassistant/components/n26/__init__.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Support for N26 bank accounts.""" -from datetime import datetime, timedelta, timezone -import logging - -from n26 import api as n26_api, config as n26_config -from requests import HTTPError -import voluptuous as vol - -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.util import Throttle - -from .const import DATA, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) - -# define configuration parameters -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ], - ) - }, - extra=vol.ALLOW_EXTRA, -) - -PLATFORMS = ["sensor", "switch"] - - -def setup(hass, config): - """Set up N26 Component.""" - acc_list = config[DOMAIN] - - api_data_list = [] - - for acc in acc_list: - user = acc[CONF_USERNAME] - password = acc[CONF_PASSWORD] - - api = n26_api.Api(n26_config.Config(user, password)) - - try: - api.get_token() - except HTTPError as err: - _LOGGER.error(str(err)) - return False - - api_data = N26Data(api) - api_data.update() - - api_data_list.append(api_data) - - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA] = api_data_list - - # Load platforms for supported devices - for platform in PLATFORMS: - load_platform(hass, platform, DOMAIN, {}, config) - - return True - - -def timestamp_ms_to_date(epoch_ms) -> datetime or None: - """Convert millisecond timestamp to datetime.""" - if epoch_ms: - return datetime.fromtimestamp(epoch_ms / 1000, timezone.utc) - - -class N26Data: - """Handle N26 API object and limit updates.""" - - def __init__(self, api): - """Initialize the data object.""" - self._api = api - - self._account_info = {} - self._balance = {} - self._limits = {} - self._account_statuses = {} - - self._cards = {} - self._spaces = {} - - @property - def api(self): - """Return N26 api client.""" - return self._api - - @property - def account_info(self): - """Return N26 account info.""" - return self._account_info - - @property - def balance(self): - """Return N26 account balance.""" - return self._balance - - @property - def limits(self): - """Return N26 account limits.""" - return self._limits - - @property - def account_statuses(self): - """Return N26 account statuses.""" - return self._account_statuses - - @property - def cards(self): - """Return N26 cards.""" - return self._cards - - def card(self, card_id: str, default: dict = None): - """Return a card by its id or the given default.""" - return next((card for card in self.cards if card["id"] == card_id), default) - - @property - def spaces(self): - """Return N26 spaces.""" - return self._spaces - - def space(self, space_id: str, default: dict = None): - """Return a space by its id or the given default.""" - return next( - (space for space in self.spaces["spaces"] if space["id"] == space_id), - default, - ) - - @Throttle(min_time=DEFAULT_SCAN_INTERVAL * 0.8) - def update_account(self): - """Get the latest account data from N26.""" - self._account_info = self._api.get_account_info() - self._balance = self._api.get_balance() - self._limits = self._api.get_account_limits() - self._account_statuses = self._api.get_account_statuses() - - @Throttle(min_time=DEFAULT_SCAN_INTERVAL * 0.8) - def update_cards(self): - """Get the latest cards data from N26.""" - self._cards = self._api.get_cards() - - @Throttle(min_time=DEFAULT_SCAN_INTERVAL * 0.8) - def update_spaces(self): - """Get the latest spaces data from N26.""" - self._spaces = self._api.get_spaces() - - def update(self): - """Get the latest data from N26.""" - self.update_account() - self.update_cards() - self.update_spaces() diff --git a/homeassistant/components/n26/const.py b/homeassistant/components/n26/const.py deleted file mode 100644 index 0a640d0f34e40..0000000000000 --- a/homeassistant/components/n26/const.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Provides the constants needed for component.""" -DOMAIN = "n26" - -DATA = "data" - -CARD_STATE_ACTIVE = "M_ACTIVE" -CARD_STATE_BLOCKED = "M_DISABLED" diff --git a/homeassistant/components/n26/manifest.json b/homeassistant/components/n26/manifest.json deleted file mode 100644 index 2dec0e6ba2de7..0000000000000 --- a/homeassistant/components/n26/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "n26", - "name": "N26", - "documentation": "https://www.home-assistant.io/integrations/n26", - "requirements": ["n26==0.2.7"], - "codeowners": [] -} diff --git a/homeassistant/components/n26/sensor.py b/homeassistant/components/n26/sensor.py deleted file mode 100644 index 98d86194b86e0..0000000000000 --- a/homeassistant/components/n26/sensor.py +++ /dev/null @@ -1,244 +0,0 @@ -"""Support for N26 bank account sensors.""" -from homeassistant.components.sensor import SensorEntity - -from . import DEFAULT_SCAN_INTERVAL, DOMAIN, timestamp_ms_to_date -from .const import DATA - -SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL - -ATTR_IBAN = "account" -ATTR_USABLE_BALANCE = "usable_balance" -ATTR_BANK_BALANCE = "bank_balance" - -ATTR_ACC_OWNER_TITLE = "owner_title" -ATTR_ACC_OWNER_FIRST_NAME = "owner_first_name" -ATTR_ACC_OWNER_LAST_NAME = "owner_last_name" -ATTR_ACC_OWNER_GENDER = "owner_gender" -ATTR_ACC_OWNER_BIRTH_DATE = "owner_birth_date" -ATTR_ACC_OWNER_EMAIL = "owner_email" -ATTR_ACC_OWNER_PHONE_NUMBER = "owner_phone_number" - -ICON_ACCOUNT = "mdi:currency-eur" -ICON_CARD = "mdi:credit-card" -ICON_SPACE = "mdi:crop-square" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the N26 sensor platform.""" - if discovery_info is None: - return - - api_list = hass.data[DOMAIN][DATA] - - sensor_entities = [] - for api_data in api_list: - sensor_entities.append(N26Account(api_data)) - - for card in api_data.cards: - sensor_entities.append(N26Card(api_data, card)) - - for space in api_data.spaces["spaces"]: - sensor_entities.append(N26Space(api_data, space)) - - add_entities(sensor_entities) - - -class N26Account(SensorEntity): - """Sensor for a N26 balance account. - - A balance account contains an amount of money (=balance). The amount may - also be negative. - """ - - def __init__(self, api_data) -> None: - """Initialize a N26 balance account.""" - self._data = api_data - self._iban = self._data.balance["iban"] - - def update(self) -> None: - """Get the current balance and currency for the account.""" - self._data.update_account() - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._iban[-4:] - - @property - def name(self) -> str: - """Friendly name of the sensor.""" - return f"n26_{self._iban[-4:]}" - - @property - def state(self) -> float: - """Return the balance of the account as state.""" - if self._data.balance is None: - return None - - return self._data.balance.get("availableBalance") - - @property - def unit_of_measurement(self) -> str: - """Use the currency as unit of measurement.""" - if self._data.balance is None: - return None - - return self._data.balance.get("currency") - - @property - def extra_state_attributes(self) -> dict: - """Additional attributes of the sensor.""" - attributes = { - ATTR_IBAN: self._data.balance.get("iban"), - ATTR_BANK_BALANCE: self._data.balance.get("bankBalance"), - ATTR_USABLE_BALANCE: self._data.balance.get("usableBalance"), - ATTR_ACC_OWNER_TITLE: self._data.account_info.get("title"), - ATTR_ACC_OWNER_FIRST_NAME: self._data.account_info.get("kycFirstName"), - ATTR_ACC_OWNER_LAST_NAME: self._data.account_info.get("kycLastName"), - ATTR_ACC_OWNER_GENDER: self._data.account_info.get("gender"), - ATTR_ACC_OWNER_BIRTH_DATE: timestamp_ms_to_date( - self._data.account_info.get("birthDate") - ), - ATTR_ACC_OWNER_EMAIL: self._data.account_info.get("email"), - ATTR_ACC_OWNER_PHONE_NUMBER: self._data.account_info.get( - "mobilePhoneNumber" - ), - } - - for limit in self._data.limits: - limit_attr_name = f"limit_{limit['limit'].lower()}" - attributes[limit_attr_name] = limit["amount"] - - return attributes - - @property - def icon(self) -> str: - """Set the icon for the sensor.""" - return ICON_ACCOUNT - - -class N26Card(SensorEntity): - """Sensor for a N26 card.""" - - def __init__(self, api_data, card) -> None: - """Initialize a N26 card.""" - self._data = api_data - self._account_name = api_data.balance["iban"][-4:] - self._card = card - - def update(self) -> None: - """Get the current balance and currency for the account.""" - self._data.update_cards() - self._card = self._data.card(self._card["id"], self._card) - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._card["id"] - - @property - def name(self) -> str: - """Friendly name of the sensor.""" - return f"{self._account_name.lower()}_card_{self._card['id']}" - - @property - def state(self) -> float: - """Return the balance of the account as state.""" - return self._card["status"] - - @property - def extra_state_attributes(self) -> dict: - """Additional attributes of the sensor.""" - attributes = { - "apple_pay_eligible": self._card.get("applePayEligible"), - "card_activated": timestamp_ms_to_date(self._card.get("cardActivated")), - "card_product": self._card.get("cardProduct"), - "card_product_type": self._card.get("cardProductType"), - "card_settings_id": self._card.get("cardSettingsId"), - "card_Type": self._card.get("cardType"), - "design": self._card.get("design"), - "exceet_actual_delivery_date": self._card.get("exceetActualDeliveryDate"), - "exceet_card_status": self._card.get("exceetCardStatus"), - "exceet_expected_delivery_date": self._card.get( - "exceetExpectedDeliveryDate" - ), - "exceet_express_card_delivery": self._card.get("exceetExpressCardDelivery"), - "exceet_express_card_delivery_email_sent": self._card.get( - "exceetExpressCardDeliveryEmailSent" - ), - "exceet_express_card_delivery_tracking_id": self._card.get( - "exceetExpressCardDeliveryTrackingId" - ), - "expiration_date": timestamp_ms_to_date(self._card.get("expirationDate")), - "google_pay_eligible": self._card.get("googlePayEligible"), - "masked_pan": self._card.get("maskedPan"), - "membership": self._card.get("membership"), - "mpts_card": self._card.get("mptsCard"), - "pan": self._card.get("pan"), - "pin_defined": timestamp_ms_to_date(self._card.get("pinDefined")), - "username_on_card": self._card.get("usernameOnCard"), - } - return attributes - - @property - def icon(self) -> str: - """Set the icon for the sensor.""" - return ICON_CARD - - -class N26Space(SensorEntity): - """Sensor for a N26 space.""" - - def __init__(self, api_data, space) -> None: - """Initialize a N26 space.""" - self._data = api_data - self._space = space - - def update(self) -> None: - """Get the current balance and currency for the account.""" - self._data.update_spaces() - self._space = self._data.space(self._space["id"], self._space) - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return f"space_{self._data.balance['iban'][-4:]}_{self._space['name'].lower()}" - - @property - def name(self) -> str: - """Friendly name of the sensor.""" - return self._space["name"] - - @property - def state(self) -> float: - """Return the balance of the account as state.""" - return self._space["balance"]["availableBalance"] - - @property - def unit_of_measurement(self) -> str: - """Use the currency as unit of measurement.""" - return self._space["balance"]["currency"] - - @property - def extra_state_attributes(self) -> dict: - """Additional attributes of the sensor.""" - goal_value = "" - if "goal" in self._space: - goal_value = self._space.get("goal").get("amount") - - attributes = { - "name": self._space.get("name"), - "goal": goal_value, - "background_image_url": self._space.get("backgroundImageUrl"), - "image_url": self._space.get("imageUrl"), - "is_card_attached": self._space.get("isCardAttached"), - "is_hidden_from_balance": self._space.get("isHiddenFromBalance"), - "is_locked": self._space.get("isLocked"), - "is_primary": self._space.get("isPrimary"), - } - return attributes - - @property - def icon(self) -> str: - """Set the icon for the sensor.""" - return ICON_SPACE diff --git a/homeassistant/components/n26/switch.py b/homeassistant/components/n26/switch.py deleted file mode 100644 index 910aa96ca492f..0000000000000 --- a/homeassistant/components/n26/switch.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Support for N26 switches.""" -from homeassistant.components.switch import SwitchEntity - -from . import DEFAULT_SCAN_INTERVAL, DOMAIN -from .const import CARD_STATE_ACTIVE, CARD_STATE_BLOCKED, DATA - -SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the N26 switch platform.""" - if discovery_info is None: - return - - api_list = hass.data[DOMAIN][DATA] - - switch_entities = [] - for api_data in api_list: - for card in api_data.cards: - switch_entities.append(N26CardSwitch(api_data, card)) - - add_entities(switch_entities) - - -class N26CardSwitch(SwitchEntity): - """Representation of a N26 card block/unblock switch.""" - - def __init__(self, api_data, card: dict): - """Initialize the N26 card block/unblock switch.""" - self._data = api_data - self._card = card - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._card["id"] - - @property - def name(self) -> str: - """Friendly name of the sensor.""" - return f"card_{self._card['id']}" - - @property - def is_on(self): - """Return true if switch is on.""" - return self._card["status"] == CARD_STATE_ACTIVE - - def turn_on(self, **kwargs): - """Block the card.""" - self._data.api.unblock_card(self._card["id"]) - self._card["status"] = CARD_STATE_ACTIVE - - def turn_off(self, **kwargs): - """Unblock the card.""" - self._data.api.block_card(self._card["id"]) - self._card["status"] = CARD_STATE_BLOCKED - - def update(self): - """Update the switch state.""" - self._data.update_cards() - self._card = self._data.card(self._card["id"], self._card) diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index 97dce35063bf1..063ceca0fd7d2 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -3,5 +3,6 @@ "name": "NAD", "documentation": "https://www.home-assistant.io/integrations/nad", "requirements": ["nad_receiver==0.0.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index e7f83c66efac9..ef8a9de37ee3f 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -11,7 +11,14 @@ SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, + STATE_OFF, + STATE_ON, +) import homeassistant.helpers.config_validation as cv DEFAULT_TYPE = "RS232" @@ -31,9 +38,7 @@ | SUPPORT_SELECT_SOURCE ) -CONF_TYPE = "type" CONF_SERIAL_PORT = "serial_port" # for NADReceiver -CONF_PORT = "port" # for NADReceiverTelnet CONF_MIN_VOLUME = "min_volume" CONF_MAX_VOLUME = "max_volume" CONF_VOLUME_STEP = "volume_step" # for NADReceiverTCP diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py new file mode 100644 index 0000000000000..7dc6701217d2b --- /dev/null +++ b/homeassistant/components/nam/__init__.py @@ -0,0 +1,106 @@ +"""The Nettigo Air Monitor component.""" +from __future__ import annotations + +import logging +from typing import cast + +from aiohttp import ClientSession +from aiohttp.client_exceptions import ClientConnectorError +import async_timeout +from nettigo_air_monitor import ( + ApiError, + DictToObj, + InvalidSensorData, + NettigoAirMonitor, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_NAME, DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["air_quality", "sensor"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Nettigo as config entry.""" + host: str = entry.data[CONF_HOST] + + websession = async_get_clientsession(hass) + + coordinator = NAMDataUpdateCoordinator(hass, websession, host, entry.unique_id) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class NAMDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Nettigo Air Monitor data.""" + + def __init__( + self, + hass: HomeAssistant, + session: ClientSession, + host: str, + unique_id: str | None, + ) -> None: + """Initialize.""" + self.host = host + self.nam = NettigoAirMonitor(session, host) + self._unique_id = unique_id + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL + ) + + async def _async_update_data(self) -> DictToObj: + """Update data via library.""" + try: + # Device firmware uses synchronous code and doesn't respond to http queries + # when reading data from sensors. The nettigo-air-quality library tries to + # get the data 4 times, so we use a longer than usual timeout here. + with async_timeout.timeout(30): + data = await self.nam.async_update() + except (ApiError, ClientConnectorError, InvalidSensorData) as error: + raise UpdateFailed(error) from error + + _LOGGER.debug(data) + + return data + + @property + def unique_id(self) -> str | None: + """Return a unique_id.""" + return self._unique_id + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, cast(str, self._unique_id))}, + "name": DEFAULT_NAME, + "sw_version": self.nam.software_version, + "manufacturer": MANUFACTURER, + } diff --git a/homeassistant/components/nam/air_quality.py b/homeassistant/components/nam/air_quality.py new file mode 100644 index 0000000000000..163b50148dbc1 --- /dev/null +++ b/homeassistant/components/nam/air_quality.py @@ -0,0 +1,103 @@ +"""Support for the Nettigo Air Monitor air_quality service.""" +from __future__ import annotations + +from homeassistant.components.air_quality import AirQualityEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NAMDataUpdateCoordinator +from .const import ( + AIR_QUALITY_SENSORS, + ATTR_MHZ14A_CARBON_DIOXIDE, + DEFAULT_NAME, + DOMAIN, + SUFFIX_P1, + SUFFIX_P2, +) + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add a Nettigo Air Monitor entities from a config_entry.""" + coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[NAMAirQuality] = [] + for sensor in AIR_QUALITY_SENSORS: + if f"{sensor}{SUFFIX_P1}" in coordinator.data: + entities.append(NAMAirQuality(coordinator, sensor)) + + async_add_entities(entities, False) + + +class NAMAirQuality(CoordinatorEntity, AirQualityEntity): + """Define an Nettigo Air Monitor air quality.""" + + coordinator: NAMDataUpdateCoordinator + + def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None: + """Initialize.""" + super().__init__(coordinator) + self.sensor_type = sensor_type + + @property + def name(self) -> str: + """Return the name.""" + return f"{DEFAULT_NAME} {AIR_QUALITY_SENSORS[self.sensor_type]}" + + @property + def particulate_matter_2_5(self) -> StateType: + """Return the particulate matter 2.5 level.""" + return round_state( + getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}") + ) + + @property + def particulate_matter_10(self) -> StateType: + """Return the particulate matter 10 level.""" + return round_state( + getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P1}") + ) + + @property + def carbon_dioxide(self) -> StateType: + """Return the particulate matter 10 level.""" + return round_state( + getattr(self.coordinator.data, ATTR_MHZ14A_CARBON_DIOXIDE, None) + ) + + @property + def unique_id(self) -> str: + """Return a unique_id for this entity.""" + return f"{self.coordinator.unique_id}-{self.sensor_type}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return self.coordinator.device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + available = super().available + + # For a short time after booting, the device does not return values for all + # sensors. For this reason, we mark entities for which data is missing as + # unavailable. + return available and bool( + getattr(self.coordinator.data, f"{self.sensor_type}{SUFFIX_P2}", None) + ) + + +def round_state(state: StateType) -> StateType: + """Round state.""" + if isinstance(state, float): + return round(state) + + return state diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py new file mode 100644 index 0000000000000..ccb5e6e6e8447 --- /dev/null +++ b/homeassistant/components/nam/config_flow.py @@ -0,0 +1,121 @@ +"""Adds config flow for Nettigo Air Monitor.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, cast + +from aiohttp.client_exceptions import ClientConnectorError +import async_timeout +from nettigo_air_monitor import ApiError, CannotGetMac, NettigoAirMonitor +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.typing import DiscoveryInfoType + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Nettigo Air Monitor.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize flow.""" + self.host: str | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + self.host = user_input[CONF_HOST] + try: + mac = await self._async_get_mac(cast(str, self.host)) + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except CannotGetMac: + return self.async_abort(reason="device_unsupported") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + + await self.async_set_unique_id(format_mac(mac)) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) + + return self.async_create_entry( + title=cast(str, self.host), + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=""): str, + } + ), + errors=errors, + ) + + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle zeroconf discovery.""" + self.host = discovery_info[CONF_HOST] + + try: + mac = await self._async_get_mac(cast(str, self.host)) + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + return self.async_abort(reason="cannot_connect") + except CannotGetMac: + return self.async_abort(reason="device_unsupported") + + await self.async_set_unique_id(format_mac(mac)) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) + + self.context["title_placeholders"] = { + ATTR_NAME: discovery_info[ATTR_NAME].split(".")[0] + } + + return await self.async_step_confirm_discovery() + + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle discovery confirm.""" + errors: dict = {} + + if user_input is not None: + return self.async_create_entry( + title=cast(str, self.host), + data={CONF_HOST: self.host}, + ) + + self._set_confirm_only() + + return self.async_show_form( + step_id="confirm_discovery", + description_placeholders={CONF_HOST: self.host}, + errors=errors, + ) + + async def _async_get_mac(self, host: str) -> str: + """Get device MAC address.""" + websession = async_get_clientsession(self.hass) + nam = NettigoAirMonitor(websession, host) + # Device firmware uses synchronous code and doesn't respond to http queries + # when reading data from sensors. The nettigo-air-monitor library tries to get + # the data 4 times, so we use a longer than usual timeout here. + with async_timeout.timeout(30): + return cast(str, await nam.async_get_mac_address()) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py new file mode 100644 index 0000000000000..8171914b832d1 --- /dev/null +++ b/homeassistant/components/nam/const.py @@ -0,0 +1,161 @@ +"""Constants for Nettigo Air Monitor integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Final + +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) + +from .model import SensorDescription + +ATTR_BME280_HUMIDITY: Final = "bme280_humidity" +ATTR_BME280_PRESSURE: Final = "bme280_pressure" +ATTR_BME280_TEMPERATURE: Final = "bme280_temperature" +ATTR_BMP280_PRESSURE: Final = "bmp280_pressure" +ATTR_BMP280_TEMPERATURE: Final = "bmp280_temperature" +ATTR_DHT22_HUMIDITY: Final = "humidity" +ATTR_DHT22_TEMPERATURE: Final = "temperature" +ATTR_HECA_HUMIDITY: Final = "heca_humidity" +ATTR_HECA_TEMPERATURE: Final = "heca_temperature" +ATTR_MHZ14A_CARBON_DIOXIDE: Final = "conc_co2_ppm" +ATTR_SHT3X_HUMIDITY: Final = "sht3x_humidity" +ATTR_SHT3X_TEMPERATURE: Final = "sht3x_temperature" +ATTR_SIGNAL_STRENGTH: Final = "signal" +ATTR_SPS30_P0: Final = "sps30_p0" +ATTR_SPS30_P4: Final = "sps30_p4" +ATTR_UPTIME: Final = "uptime" + +ATTR_ENABLED: Final = "enabled" +ATTR_LABEL: Final = "label" +ATTR_UNIT: Final = "unit" + +DEFAULT_NAME: Final = "Nettigo Air Monitor" +DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) +DOMAIN: Final = "nam" +MANUFACTURER: Final = "Nettigo" + +SUFFIX_P1: Final = "_p1" +SUFFIX_P2: Final = "_p2" + +AIR_QUALITY_SENSORS: Final[dict[str, str]] = {"sds": "SDS011", "sps30": "SPS30"} + +SENSORS: Final[dict[str, SensorDescription]] = { + ATTR_BME280_HUMIDITY: { + ATTR_LABEL: f"{DEFAULT_NAME} BME280 Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_BME280_PRESSURE: { + ATTR_LABEL: f"{DEFAULT_NAME} BME280 Pressure", + ATTR_UNIT: PRESSURE_HPA, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_BME280_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} BME280 Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_BMP280_PRESSURE: { + ATTR_LABEL: f"{DEFAULT_NAME} BMP280 Pressure", + ATTR_UNIT: PRESSURE_HPA, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_BMP280_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} BMP280 Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_HECA_HUMIDITY: { + ATTR_LABEL: f"{DEFAULT_NAME} HECA Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_HECA_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} HECA Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_SHT3X_HUMIDITY: { + ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_SHT3X_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} SHT3X Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_SPS30_P0: { + ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + }, + ATTR_SPS30_P4: { + ATTR_LABEL: f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_ENABLED: True, + }, + ATTR_DHT22_HUMIDITY: { + ATTR_LABEL: f"{DEFAULT_NAME} DHT22 Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_DHT22_TEMPERATURE: { + ATTR_LABEL: f"{DEFAULT_NAME} DHT22 Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_ENABLED: True, + }, + ATTR_SIGNAL_STRENGTH: { + ATTR_LABEL: f"{DEFAULT_NAME} Signal Strength", + ATTR_UNIT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ATTR_DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH, + ATTR_ICON: None, + ATTR_ENABLED: False, + }, + ATTR_UPTIME: { + ATTR_LABEL: f"{DEFAULT_NAME} Uptime", + ATTR_UNIT: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_ICON: None, + ATTR_ENABLED: False, + }, +} diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json new file mode 100644 index 0000000000000..3e03a0ad787ec --- /dev/null +++ b/homeassistant/components/nam/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "nam", + "name": "Nettigo Air Monitor", + "documentation": "https://www.home-assistant.io/integrations/nam", + "codeowners": ["@bieniu"], + "requirements": ["nettigo-air-monitor==0.2.6"], + "zeroconf": [{"type": "_http._tcp.local.", "name": "nam-*"}], + "config_flow": true, + "quality_scale": "platinum", + "iot_class": "local_polling" +} diff --git a/homeassistant/components/nam/model.py b/homeassistant/components/nam/model.py new file mode 100644 index 0000000000000..8d1bfe29a4a1b --- /dev/null +++ b/homeassistant/components/nam/model.py @@ -0,0 +1,14 @@ +"""Type definitions for Nettig Air Monitor integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class SensorDescription(TypedDict): + """Sensor description class.""" + + label: str + unit: str | None + device_class: str | None + icon: str | None + enabled: bool diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py new file mode 100644 index 0000000000000..2774d87f2d3ac --- /dev/null +++ b/homeassistant/components/nam/sensor.py @@ -0,0 +1,114 @@ +"""Support for the Nettigo Air Monitor service.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.dt import utcnow + +from . import NAMDataUpdateCoordinator +from .const import ATTR_ENABLED, ATTR_LABEL, ATTR_UNIT, ATTR_UPTIME, DOMAIN, SENSORS + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add a Nettigo Air Monitor entities from a config_entry.""" + coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + sensors: list[NAMSensor | NAMSensorUptime] = [] + for sensor in SENSORS: + if sensor in coordinator.data: + if sensor == ATTR_UPTIME: + sensors.append(NAMSensorUptime(coordinator, sensor)) + else: + sensors.append(NAMSensor(coordinator, sensor)) + + async_add_entities(sensors, False) + + +class NAMSensor(CoordinatorEntity, SensorEntity): + """Define an Nettigo Air Monitor sensor.""" + + coordinator: NAMDataUpdateCoordinator + + def __init__(self, coordinator: NAMDataUpdateCoordinator, sensor_type: str) -> None: + """Initialize.""" + super().__init__(coordinator) + self.sensor_type = sensor_type + self._description = SENSORS[self.sensor_type] + + @property + def name(self) -> str: + """Return the name.""" + return self._description[ATTR_LABEL] + + @property + def state(self) -> Any: + """Return the state.""" + return getattr(self.coordinator.data, self.sensor_type) + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit the value is expressed in.""" + return self._description[ATTR_UNIT] + + @property + def device_class(self) -> str | None: + """Return the class of this sensor.""" + return self._description[ATTR_DEVICE_CLASS] + + @property + def icon(self) -> str | None: + """Return the icon.""" + return self._description[ATTR_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._description[ATTR_ENABLED] + + @property + def unique_id(self) -> str: + """Return a unique_id for this entity.""" + return f"{self.coordinator.unique_id}-{self.sensor_type}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return self.coordinator.device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + available = super().available + + # For a short time after booting, the device does not return values for all + # sensors. For this reason, we mark entities for which data is missing as + # unavailable. + return available and bool( + getattr(self.coordinator.data, self.sensor_type, None) + ) + + +class NAMSensorUptime(NAMSensor): + """Define an Nettigo Air Monitor uptime sensor.""" + + @property + def state(self) -> str: + """Return the state.""" + uptime_sec = getattr(self.coordinator.data, self.sensor_type) + return ( + (utcnow() - timedelta(seconds=uptime_sec)) + .replace(microsecond=0) + .isoformat() + ) diff --git a/homeassistant/components/nam/strings.json b/homeassistant/components/nam/strings.json new file mode 100644 index 0000000000000..e8994a346bfcf --- /dev/null +++ b/homeassistant/components/nam/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Set up Nettigo Air Monitor integration.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm_discovery": { + "description": "Do you want to set up Nettigo Air Monitor at {host}?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "device_unsupported": "The device is unsupported." + } + } +} diff --git a/homeassistant/components/nam/translations/ca.json b/homeassistant/components/nam/translations/ca.json new file mode 100644 index 0000000000000..bc4ca456f4e9a --- /dev/null +++ b/homeassistant/components/nam/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "device_unsupported": "El dispositiu no \u00e9s compatible." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Vols configurar Nettigo Air Monitor a {host}?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Configura la integraci\u00f3 Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/de.json b/homeassistant/components/nam/translations/de.json new file mode 100644 index 0000000000000..823a967572631 --- /dev/null +++ b/homeassistant/components/nam/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "device_unsupported": "Das Ger\u00e4t wird nicht unterst\u00fctzt." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "M\u00f6chtest du Nettigo Air Monitor unter {host} einrichten?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/en.json b/homeassistant/components/nam/translations/en.json new file mode 100644 index 0000000000000..0ea0c7ae6c13f --- /dev/null +++ b/homeassistant/components/nam/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "device_unsupported": "The device is unsupported." + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Do you want to set up Nettigo Air Monitor at {host}?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Set up Nettigo Air Monitor integration." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json new file mode 100644 index 0000000000000..59f6df4370197 --- /dev/null +++ b/homeassistant/components/nam/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "device_unsupported": "El dispositivo no es compatible." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "\u00bfQuieres configurar Nettigo Air Monitor en {host} ?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Configurar la integraci\u00f3n de Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/et.json b/homeassistant/components/nam/translations/et.json new file mode 100644 index 0000000000000..e94cd3a46b6d4 --- /dev/null +++ b/homeassistant/components/nam/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "device_unsupported": "Seadet ei toetata." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Kas seadistada Nettigo Air Monitori asukohas {host}?" + }, + "user": { + "data": { + "host": "host" + }, + "description": "Seadista Nettigo Air Monitori sidumine." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/fr.json b/homeassistant/components/nam/translations/fr.json new file mode 100644 index 0000000000000..0c58af2a80058 --- /dev/null +++ b/homeassistant/components/nam/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "device_unsupported": "L'appareil n'est pas pris en charge." + }, + "flow_title": "{nom}", + "step": { + "confirm_discovery": { + "description": "Voulez-vous configurer Nettigo Air Monitor chez {host} ?" + }, + "user": { + "data": { + "host": "Hotes" + }, + "description": "Configurez l'int\u00e9gration Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/it.json b/homeassistant/components/nam/translations/it.json new file mode 100644 index 0000000000000..9a208cbfd3c57 --- /dev/null +++ b/homeassistant/components/nam/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "device_unsupported": "Il dispositivo non \u00e8 supportato." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Vuoi configurare Nettigo Air Monitor su {host} ?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Configura l'integrazione di Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/nl.json b/homeassistant/components/nam/translations/nl.json new file mode 100644 index 0000000000000..c6171ead0f458 --- /dev/null +++ b/homeassistant/components/nam/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "device_unsupported": "Het apparaat wordt niet ondersteund." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Wilt u Nettigo Air Monitor instellen bij {host} ?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Stel Nettigo Air Monitor integratie in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/no.json b/homeassistant/components/nam/translations/no.json new file mode 100644 index 0000000000000..923efe4937b1e --- /dev/null +++ b/homeassistant/components/nam/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "device_unsupported": "Enheten st\u00f8ttes ikke." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Vil du konfigurere Nettigo Air Monitor p\u00e5 {host} ?" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Sett opp integrering av Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/pl.json b/homeassistant/components/nam/translations/pl.json new file mode 100644 index 0000000000000..bdf5014428d04 --- /dev/null +++ b/homeassistant/components/nam/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "device_unsupported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "Czy chcesz skonfigurowa\u0107 Nettigo Air Monitor {host}?" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Konfiguracja integracji Nettigo Air Monitor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/ru.json b/homeassistant/components/nam/translations/ru.json new file mode 100644 index 0000000000000..d475081285bc8 --- /dev/null +++ b/homeassistant/components/nam/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "device_unsupported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nettigo Air Monitor ({host})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Nettigo Air Monitor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sv.json b/homeassistant/components/nam/translations/sv.json new file mode 100644 index 0000000000000..15a583f12a2b0 --- /dev/null +++ b/homeassistant/components/nam/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "device_unsupported": "Enheten st\u00f6ds ej" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta ", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/zh-Hant.json b/homeassistant/components/nam/translations/zh-Hant.json new file mode 100644 index 0000000000000..5d0b3f179afe6 --- /dev/null +++ b/homeassistant/components/nam/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "device_unsupported": "\u88dd\u7f6e\u4e0d\u652f\u63f4\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u5740\u70ba {host} \u7684 Nettigo Air Monitor\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8a2d\u5b9a Nettigo Air Monitor \u6574\u5408\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/namecheapdns/manifest.json b/homeassistant/components/namecheapdns/manifest.json index 9015f2dc8470a..128d7feaccb23 100644 --- a/homeassistant/components/namecheapdns/manifest.json +++ b/homeassistant/components/namecheapdns/manifest.json @@ -2,6 +2,7 @@ "domain": "namecheapdns", "name": "Namecheap FreeDNS", "documentation": "https://www.home-assistant.io/integrations/namecheapdns", - "requirements": ["defusedxml==0.6.0"], - "codeowners": [] + "requirements": ["defusedxml==0.7.1"], + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 1f0fbf80983c6..0984962fb7300 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,5 +3,6 @@ "name": "Nanoleaf", "documentation": "https://www.home-assistant.io/integrations/nanoleaf", "requirements": ["pynanoleaf==0.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index bb0db8ebd8559..f61db94332b1a 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,5 +1,4 @@ """Support for Neato botvac connected vacuum cleaners.""" -import asyncio from datetime import timedelta import logging @@ -7,16 +6,12 @@ from pybotvac.exceptions import NeatoException import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_SOURCE, - CONF_TOKEN, -) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import api, config_flow @@ -47,7 +42,7 @@ PLATFORMS = ["camera", "vacuum", "switch", "sensor"] -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Neato component.""" hass.data[NEATO_DOMAIN] = {} @@ -71,17 +66,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up config entry.""" if CONF_TOKEN not in entry.data: - # Init reauth flow - hass.async_create_task( - hass.config_entries.flow.async_init( - NEATO_DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - ) - ) - return False + raise ConfigEntryAuthFailed implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( @@ -103,22 +91,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data[NEATO_LOGIN] = hub - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: """Unload config entry.""" - unload_functions = ( - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ) - - unload_ok = all(await asyncio.gather(*unload_functions)) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[NEATO_DOMAIN].pop(entry.entry_id) @@ -128,9 +108,9 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool class NeatoHub: """A My Neato hub wrapper class.""" - def __init__(self, hass: HomeAssistantType, neato: Account): + def __init__(self, hass: HomeAssistant, neato: Account) -> None: """Initialize the Neato hub.""" - self._hass: HomeAssistantType = hass + self._hass = hass self.my_neato: Account = neato @Throttle(timedelta(minutes=1)) diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py index 31988fc175e73..a22b1b48e74e1 100644 --- a/homeassistant/components/neato/api.py +++ b/homeassistant/components/neato/api.py @@ -15,7 +15,7 @@ def __init__( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ): + ) -> None: """Initialize Neato Botvac Auth.""" self.hass = hass self.session = config_entry_oauth2_flow.OAuth2Session( diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 3f7f7831f54b4..580faffe8ffbf 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -5,7 +5,6 @@ import voluptuous as vol -from homeassistant import config_entries from homeassistant.const import CONF_TOKEN from homeassistant.helpers import config_entry_oauth2_flow @@ -18,7 +17,6 @@ class OAuth2FlowHandler( """Config flow to handle Neato Botvac OAuth2 authentication.""" DOMAIN = NEATO_DOMAIN - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @property def logger(self) -> logging.Logger: diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 5cd6a7558b112..7632360d13c68 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -3,14 +3,8 @@ "name": "Neato Botvac", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", - "requirements": [ - "pybotvac==0.0.20" - ], - "codeowners": [ - "@dshokouhi", - "@Santobert" - ], - "dependencies": [ - "http" - ] -} \ No newline at end of file + "requirements": ["pybotvac==0.0.20"], + "codeowners": ["@dshokouhi", "@Santobert"], + "dependencies": ["http"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 83add4ff3f7de..98208698037e1 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -80,7 +80,7 @@ def available(self): @property def state(self): """Return the state.""" - return self._state["details"]["charge"] + return self._state["details"]["charge"] if self._state else None @property def unit_of_measurement(self): diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml index 2c5b2bd3181d5..eb0c7bffba919 100644 --- a/homeassistant/components/neato/services.yaml +++ b/homeassistant/components/neato/services.yaml @@ -1,18 +1,45 @@ custom_cleaning: + name: Zone Cleaning service description: Zone Cleaning service call specific to Neato Botvacs. + target: + entity: + integration: neato + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. [Required] - example: "vacuum.neato" mode: + name: Set cleaning mode description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." + default: 2 example: 2 + selector: + number: + min: 1 + max: 2 + mode: box navigation: + name: Set navigation mode description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." + default: 1 example: 1 + selector: + number: + min: 1 + max: 3 + mode: box category: + name: Use cleaning map description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." + default: 4 example: 2 + selector: + number: + min: 2 + max: 4 + step: 2 + mode: box zone: + name: Name of the zone to clean (Only Botvac D7) description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. example: "Kitchen" + selector: + text: diff --git a/homeassistant/components/neato/translations/bg.json b/homeassistant/components/neato/translations/bg.json index f652830217a49..d8f67f2185d98 100644 --- a/homeassistant/components/neato/translations/bg.json +++ b/homeassistant/components/neato/translations/bg.json @@ -5,17 +5,6 @@ }, "create_entry": { "default": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url})." - }, - "step": { - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", - "vendor": "\u0414\u043e\u0441\u0442\u0430\u0432\u0447\u0438\u043a" - }, - "description": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url}).", - "title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 Neato \u0430\u043a\u0430\u0443\u043d\u0442" - } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/ca.json b/homeassistant/components/neato/translations/ca.json index f818135c51f79..ab8210afb2f1d 100644 --- a/homeassistant/components/neato/translations/ca.json +++ b/homeassistant/components/neato/translations/ca.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" @@ -11,25 +10,12 @@ "create_entry": { "default": "Autenticaci\u00f3 exitosa" }, - "error": { - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" - }, "step": { "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" }, "reauth_confirm": { "title": "Vols comen\u00e7ar la configuraci\u00f3?" - }, - "user": { - "data": { - "password": "Contrasenya", - "username": "Nom d'usuari", - "vendor": "Venedor" - }, - "description": "Consulta la [documentaci\u00f3 de Neato]({docs_url}).", - "title": "Informaci\u00f3 del compte Neato" } } }, diff --git a/homeassistant/components/neato/translations/cs.json b/homeassistant/components/neato/translations/cs.json index 5d45710f4a68f..6cf7e314c4b0f 100644 --- a/homeassistant/components/neato/translations/cs.json +++ b/homeassistant/components/neato/translations/cs.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" @@ -11,24 +10,12 @@ "create_entry": { "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" }, - "error": { - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, "step": { "pick_implementation": { "title": "Vyberte metodu ov\u011b\u0159en\u00ed" }, "reauth_confirm": { "title": "Chcete za\u010d\u00edt nastavovat?" - }, - "user": { - "data": { - "password": "Heslo", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "description": "Viz [dokumentace Neato]({docs_url}).", - "title": "Informace o \u00fa\u010dtu Neato" } } }, diff --git a/homeassistant/components/neato/translations/da.json b/homeassistant/components/neato/translations/da.json index 785bf4aca9d14..c68c48896315f 100644 --- a/homeassistant/components/neato/translations/da.json +++ b/homeassistant/components/neato/translations/da.json @@ -5,17 +5,6 @@ }, "create_entry": { "default": "Se [Neato-dokumentation]({docs_url})." - }, - "step": { - "user": { - "data": { - "password": "Adgangskode", - "username": "Brugernavn", - "vendor": "Udbyder" - }, - "description": "Se [Neato-dokumentation]({docs_url}).", - "title": "Neato-kontooplysninger" - } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index 272191a42216d..dac965a13bf96 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "invalid_auth": "Ung\u00fcltige Authentifizierung", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler sind [im Hilfebereich]({docs_url}) zu finden", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" @@ -11,25 +10,12 @@ "create_entry": { "default": "Erfolgreich authentifiziert" }, - "error": { - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" - }, "step": { "pick_implementation": { "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { "title": "M\u00f6chten Sie mit der Einrichtung beginnen?" - }, - "user": { - "data": { - "password": "Passwort", - "username": "Benutzername", - "vendor": "Hersteller" - }, - "description": "Siehe [Neato-Dokumentation]({docs_url}).", - "title": "Neato-Kontoinformationen" } } }, diff --git a/homeassistant/components/neato/translations/el.json b/homeassistant/components/neato/translations/el.json deleted file mode 100644 index 36bd6653da6d4..0000000000000 --- a/homeassistant/components/neato/translations/el.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "abort": { - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" - }, - "error": { - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c5\u03b8\u03b5\u03bd\u03c4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", - "unknown": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/en.json b/homeassistant/components/neato/translations/en.json index cc6339796455d..684d667678b8a 100644 --- a/homeassistant/components/neato/translations/en.json +++ b/homeassistant/components/neato/translations/en.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Device is already configured", "authorize_url_timeout": "Timeout generating authorize URL.", - "invalid_auth": "Invalid authentication", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful" @@ -11,25 +10,12 @@ "create_entry": { "default": "Successfully authenticated" }, - "error": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, "step": { "pick_implementation": { "title": "Pick Authentication Method" }, "reauth_confirm": { "title": "Do you want to start set up?" - }, - "user": { - "data": { - "password": "Password", - "username": "Username", - "vendor": "Vendor" - }, - "description": "See [Neato documentation]({docs_url}).", - "title": "Neato Account Info" } } }, diff --git a/homeassistant/components/neato/translations/es-419.json b/homeassistant/components/neato/translations/es-419.json index 46ae7ba2f860e..5efd26cea61e1 100644 --- a/homeassistant/components/neato/translations/es-419.json +++ b/homeassistant/components/neato/translations/es-419.json @@ -5,17 +5,6 @@ }, "create_entry": { "default": "Consulte [Documentaci\u00f3n de Neato] ({docs_url})." - }, - "step": { - "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Nombre de usuario", - "vendor": "Vendedor" - }, - "description": "Consulte [Documentaci\u00f3n de Neato] ({docs_url}).", - "title": "Informaci\u00f3n de cuenta de Neato" - } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index b88a9d0cfa474..f9b6fe54e22d3 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" @@ -11,25 +10,12 @@ "create_entry": { "default": "Ver [documentaci\u00f3n Neato]({docs_url})." }, - "error": { - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unknown": "Error inesperado" - }, "step": { "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { "title": "\u00bfQuieres iniciar la configuraci\u00f3n?" - }, - "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Usuario", - "vendor": "Vendedor" - }, - "description": "Ver [documentaci\u00f3n Neato]({docs_url}).", - "title": "Informaci\u00f3n de la cuenta de Neato" } } }, diff --git a/homeassistant/components/neato/translations/et.json b/homeassistant/components/neato/translations/et.json index 0c0aaa5f17292..40e601dfe9dd9 100644 --- a/homeassistant/components/neato/translations/et.json +++ b/homeassistant/components/neato/translations/et.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", - "invalid_auth": "Tuvastamise viga", "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", "no_url_available": "URL-i pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", "reauth_successful": "Taastuvastamine \u00f5nnestus" @@ -11,25 +10,12 @@ "create_entry": { "default": "Tuvastamine \u00f5nnestus" }, - "error": { - "invalid_auth": "Tuvastamise viga", - "unknown": "Ootamatu t\u00f5rge" - }, "step": { "pick_implementation": { "title": "Vali tuvastusmeetod" }, "reauth_confirm": { "title": "Kas soovid alustada seadistamist?" - }, - "user": { - "data": { - "password": "Salas\u00f5na", - "username": "Kasutajanimi", - "vendor": "Tootja" - }, - "description": "Vaata [Neato documentation] ( {docs_url} ).", - "title": "Neato konto teave" } } }, diff --git a/homeassistant/components/neato/translations/fr.json b/homeassistant/components/neato/translations/fr.json index 26b97e83c0bf5..bf11c9edfe306 100644 --- a/homeassistant/components/neato/translations/fr.json +++ b/homeassistant/components/neato/translations/fr.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "D\u00e9j\u00e0 configur\u00e9", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", - "invalid_auth": "Authentification invalide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation ", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" @@ -11,25 +10,12 @@ "create_entry": { "default": "Voir [Documentation Neato]({docs_url})." }, - "error": { - "invalid_auth": "Authentification invalide", - "unknown": "Erreur inattendue" - }, "step": { "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" }, "reauth_confirm": { "title": "Voulez-vous commencer la configuration ?" - }, - "user": { - "data": { - "password": "Mot de passe", - "username": "Nom d'utilisateur", - "vendor": "Vendeur" - }, - "description": "Voir [Documentation Neato] ( {docs_url} ).", - "title": "Informations compte Neato" } } }, diff --git a/homeassistant/components/neato/translations/he.json b/homeassistant/components/neato/translations/he.json deleted file mode 100644 index 6f4191da70d53..0000000000000 --- a/homeassistant/components/neato/translations/he.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json index 3cb6ffd33641b..90fb417e6a6b6 100644 --- a/homeassistant/components/neato/translations/hu.json +++ b/homeassistant/components/neato/translations/hu.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" @@ -11,25 +10,12 @@ "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" }, - "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, "step": { "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" }, "reauth_confirm": { "title": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" - }, - "user": { - "data": { - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v", - "vendor": "Sz\u00e1ll\u00edt\u00f3" - }, - "description": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3]({docs_url}).", - "title": "Neato Fi\u00f3kinform\u00e1ci\u00f3" } } }, diff --git a/homeassistant/components/neato/translations/id.json b/homeassistant/components/neato/translations/id.json index 17eee515787ba..b1811fe298e23 100644 --- a/homeassistant/components/neato/translations/id.json +++ b/homeassistant/components/neato/translations/id.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", - "invalid_auth": "Autentikasi tidak valid", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", "reauth_successful": "Autentikasi ulang berhasil" @@ -11,25 +10,12 @@ "create_entry": { "default": "Berhasil diautentikasi" }, - "error": { - "invalid_auth": "Autentikasi tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" - }, "step": { "pick_implementation": { "title": "Pilih Metode Autentikasi" }, "reauth_confirm": { "title": "Ingin memulai penyiapan?" - }, - "user": { - "data": { - "password": "Kata Sandi", - "username": "Nama Pengguna", - "vendor": "Vendor" - }, - "description": "Baca [dokumentasi Neato]({docs_url}).", - "title": "Info Akun Neato" } } }, diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index b559c23bb1a82..51d9119eb2c1c 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "invalid_auth": "Autenticazione non valida", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" @@ -11,25 +10,12 @@ "create_entry": { "default": "Autenticazione riuscita" }, - "error": { - "invalid_auth": "Autenticazione non valida", - "unknown": "Errore imprevisto" - }, "step": { "pick_implementation": { "title": "Scegli il metodo di autenticazione" }, "reauth_confirm": { "title": "Vuoi iniziare la configurazione?" - }, - "user": { - "data": { - "password": "Password", - "username": "Nome utente", - "vendor": "Fornitore" - }, - "description": "Vedere la [Documentazione di Neato]({docs_url}).", - "title": "Informazioni sull'account Neato" } } }, diff --git a/homeassistant/components/neato/translations/ko.json b/homeassistant/components/neato/translations/ko.json index d08000871ea8f..dc6519396651c 100644 --- a/homeassistant/components/neato/translations/ko.json +++ b/homeassistant/components/neato/translations/ko.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" @@ -11,25 +10,12 @@ "create_entry": { "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, - "error": { - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, "step": { "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" }, "reauth_confirm": { "title": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" - }, - "user": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", - "vendor": "\uacf5\uae09 \uc5c5\uccb4" - }, - "description": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "title": "Neato \uacc4\uc815 \uc815\ubcf4" } } }, diff --git a/homeassistant/components/neato/translations/lb.json b/homeassistant/components/neato/translations/lb.json index adc42ae840dcc..d54443e66715c 100644 --- a/homeassistant/components/neato/translations/lb.json +++ b/homeassistant/components/neato/translations/lb.json @@ -3,32 +3,18 @@ "abort": { "already_configured": "Apparat ass scho konfigur\u00e9iert", "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", - "invalid_auth": "Ong\u00eblteg Authentifikatioun", "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich" }, "create_entry": { "default": "Kuckt [Neato Dokumentatioun]({docs_url})." }, - "error": { - "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "unknown": "Onerwaarte Feeler" - }, "step": { "pick_implementation": { "title": "Authentifikatiouns Method auswielen" }, "reauth_confirm": { "title": "Soll den Ariichtungs Prozess gestart ginn?" - }, - "user": { - "data": { - "password": "Passwuert", - "username": "Benotzernumm", - "vendor": "Hiersteller" - }, - "description": "Kuckt [Neato Dokumentatioun]({docs_url}).", - "title": "Neato Kont Informatiounen" } } }, diff --git a/homeassistant/components/neato/translations/lv.json b/homeassistant/components/neato/translations/lv.json deleted file mode 100644 index 6ada7a1745214..0000000000000 --- a/homeassistant/components/neato/translations/lv.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "Parole", - "username": "Lietot\u0101jv\u0101rds" - }, - "title": "Neato konta inform\u0101cija" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json index d03bc1d216a76..3d7bbab2e7555 100644 --- a/homeassistant/components/neato/translations/nl.json +++ b/homeassistant/components/neato/translations/nl.json @@ -3,33 +3,19 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "invalid_auth": "Ongeldige authenticatie", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "reauth_successful": "Herauthenticatie was succesvol" }, "create_entry": { "default": "Succesvol geauthenticeerd" }, - "error": { - "invalid_auth": "Ongeldige authenticatie", - "unknown": "Onverwachte fout" - }, "step": { "pick_implementation": { "title": "Kies een authenticatie methode" }, "reauth_confirm": { "title": "Wil je beginnen met instellen?" - }, - "user": { - "data": { - "password": "Wachtwoord", - "username": "Gebruikersnaam", - "vendor": "Leverancier" - }, - "description": "Zie [Neato-documentatie] ({docs_url}).", - "title": "Neato-account info" } } }, diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json index a788c79ff5dc3..fc50308eda84b 100644 --- a/homeassistant/components/neato/translations/no.json +++ b/homeassistant/components/neato/translations/no.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", - "invalid_auth": "Ugyldig godkjenning", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" @@ -11,25 +10,12 @@ "create_entry": { "default": "Vellykket godkjenning" }, - "error": { - "invalid_auth": "Ugyldig godkjenning", - "unknown": "Uventet feil" - }, "step": { "pick_implementation": { "title": "Velg godkjenningsmetode" }, "reauth_confirm": { "title": "Vil du starte oppsettet?" - }, - "user": { - "data": { - "password": "Passord", - "username": "Brukernavn", - "vendor": "Leverand\u00f8r" - }, - "description": "Se [Neato dokumentasjon]({docs_url}).", - "title": "Neato kontoinformasjon" } } }, diff --git a/homeassistant/components/neato/translations/pl.json b/homeassistant/components/neato/translations/pl.json index 3177ed9d8e8fd..34f1d42bda5c6 100644 --- a/homeassistant/components/neato/translations/pl.json +++ b/homeassistant/components/neato/translations/pl.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", - "invalid_auth": "Niepoprawne uwierzytelnienie", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" @@ -11,25 +10,12 @@ "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" }, - "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, "step": { "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" }, "reauth_confirm": { "title": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" - }, - "user": { - "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika", - "vendor": "Dostawca" - }, - "description": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url}).", - "title": "Informacje o koncie Neato" } } }, diff --git a/homeassistant/components/neato/translations/pt.json b/homeassistant/components/neato/translations/pt.json index 48e73c763f003..030211ca1cf40 100644 --- a/homeassistant/components/neato/translations/pt.json +++ b/homeassistant/components/neato/translations/pt.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" @@ -11,22 +10,12 @@ "create_entry": { "default": "Autenticado com sucesso" }, - "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" - }, "step": { "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" }, "reauth_confirm": { "title": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" - }, - "user": { - "data": { - "password": "Palavra-passe", - "username": "Nome de Utilizador" - } } } } diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json index 25bb616a63837..29201df669d54 100644 --- a/homeassistant/components/neato/translations/ru.json +++ b/homeassistant/components/neato/translations/ru.json @@ -3,33 +3,19 @@ "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.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\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.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, - "error": { - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, "step": { "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" }, "reauth_confirm": { "title": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" - }, - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", - "vendor": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c" - }, - "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]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", - "title": "Neato" } } }, diff --git a/homeassistant/components/neato/translations/sl.json b/homeassistant/components/neato/translations/sl.json index 96af3b0453f4c..1aaae76ada878 100644 --- a/homeassistant/components/neato/translations/sl.json +++ b/homeassistant/components/neato/translations/sl.json @@ -16,15 +16,6 @@ }, "reauth_confirm": { "title": "Bi radi zagnali namestitev?" - }, - "user": { - "data": { - "password": "Geslo", - "username": "Uporabni\u0161ko ime", - "vendor": "Prodajalec" - }, - "description": "Glejte [neato dokumentacija] ({docs_url}).", - "title": "Podatki o ra\u010dunu Neato" } } }, diff --git a/homeassistant/components/neato/translations/sv.json b/homeassistant/components/neato/translations/sv.json index 544b6d2b292c4..71f24f595e5b2 100644 --- a/homeassistant/components/neato/translations/sv.json +++ b/homeassistant/components/neato/translations/sv.json @@ -5,17 +5,6 @@ }, "create_entry": { "default": "Se [Neato-dokumentation]({docs_url})." - }, - "step": { - "user": { - "data": { - "password": "L\u00f6senord", - "username": "Anv\u00e4ndarnamn", - "vendor": "Leverant\u00f6r" - }, - "description": "Se [Neato-dokumentation] ({docs_url}).", - "title": "Neato-kontoinfo" - } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json index 53a8e0503cb46..18fa4749d88b0 100644 --- a/homeassistant/components/neato/translations/tr.json +++ b/homeassistant/components/neato/translations/tr.json @@ -2,23 +2,11 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, - "error": { - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unknown": "Beklenmeyen hata" - }, "step": { "pick_implementation": { "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" - }, - "user": { - "data": { - "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "title": "Neato Hesap Bilgisi" } } } diff --git a/homeassistant/components/neato/translations/uk.json b/homeassistant/components/neato/translations/uk.json index 58b56a52f6c4e..353005546cf34 100644 --- a/homeassistant/components/neato/translations/uk.json +++ b/homeassistant/components/neato/translations/uk.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" @@ -11,25 +10,12 @@ "create_entry": { "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." }, - "error": { - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", - "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, "step": { "pick_implementation": { "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" }, "reauth_confirm": { "title": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" - }, - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", - "vendor": "\u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a" - }, - "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]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c.", - "title": "Neato" } } }, diff --git a/homeassistant/components/neato/translations/zh-Hans.json b/homeassistant/components/neato/translations/zh-Hans.json deleted file mode 100644 index b0b26b0226108..0000000000000 --- a/homeassistant/components/neato/translations/zh-Hans.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/zh-Hant.json b/homeassistant/components/neato/translations/zh-Hant.json index beddee423a461..781ee1f952bb2 100644 --- a/homeassistant/components/neato/translations/zh-Hant.json +++ b/homeassistant/components/neato/translations/zh-Hant.json @@ -1,9 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" @@ -11,25 +10,12 @@ "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" }, - "error": { - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, "step": { "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" }, "reauth_confirm": { "title": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" - }, - "user": { - "data": { - "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31", - "vendor": "\u5ee0\u5546" - }, - "description": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002", - "title": "Neato \u5e33\u865f\u8cc7\u8a0a" } } }, diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index e0b3c7b779f03..b6cf43a6a3e1b 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -87,7 +87,7 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug("Adding vacuums %s", dev) async_add_entities(dev, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() assert platform is not None platform.async_register_entity_service( @@ -395,6 +395,7 @@ def neato_custom_cleaning(self, mode, navigation, category, zone=None): "Zone '%s' was not found for the robot '%s'", zone, self.entity_id ) return + _LOGGER.info("Start cleaning zone '%s' with robot %s", zone, self.entity_id) self._clean_state = STATE_CLEANING try: diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 01372e744fbc7..92de680c17af9 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -3,5 +3,6 @@ "name": "Nederlandse Spoorwegen (NS)", "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", "requirements": ["nsapi==3.0.4"], - "codeowners": ["@YarmoM"] + "codeowners": ["@YarmoM"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nello/manifest.json b/homeassistant/components/nello/manifest.json index c8324022b6316..790b861054386 100644 --- a/homeassistant/components/nello/manifest.json +++ b/homeassistant/components/nello/manifest.json @@ -3,5 +3,6 @@ "name": "Nello", "documentation": "https://www.home-assistant.io/integrations/nello", "requirements": ["pynello==2.0.3"], - "codeowners": ["@pschmitt"] + "codeowners": ["@pschmitt"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 1977328c33a19..57c89e52ee86d 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -3,5 +3,6 @@ "name": "Ness Alarm", "documentation": "https://www.home-assistant.io/integrations/ness_alarm", "requirements": ["nessclient==0.9.15"], - "codeowners": ["@nickw444"] + "codeowners": ["@nickw444"], + "iot_class": "local_push" } diff --git a/homeassistant/components/ness_alarm/services.yaml b/homeassistant/components/ness_alarm/services.yaml index eb35c48b9f44c..8e4219a79212f 100644 --- a/homeassistant/components/ness_alarm/services.yaml +++ b/homeassistant/components/ness_alarm/services.yaml @@ -1,19 +1,34 @@ # Describes the format for available ness alarm services aux: + name: Aux description: Trigger an aux output. fields: output_id: - description: The aux output you wish to change. A number from 1-4. + name: Output ID + description: The aux output you wish to change. + required: true example: 1 + selector: + number: + min: 1 + max: 4 state: - description: The On/Off State, represented as true/false. Default is true. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E. + name: State + description: The On/Off State. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E. example: true default: true + selector: + boolean: panic: + name: Panic description: Trigger a panic fields: code: + name: Code description: The user code to use to trigger the panic. + required: true example: 1234 + selector: + text: diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index cd3f6ed9ed336..5cd84effbc8d5 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,6 +1,5 @@ """Support for Nest devices.""" -import asyncio import logging from google_nest_sdm.event import EventMessage @@ -12,7 +11,7 @@ from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_CLIENT_ID, @@ -22,7 +21,7 @@ CONF_STRUCTURE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -107,7 +106,7 @@ async def async_setup(hass: HomeAssistant, config: dict): class SignalUpdateCallback: """An EventCallback invoked when new events arrive from subscriber.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize EventCallback.""" self._hass = hass @@ -167,14 +166,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await subscriber.start_async() except AuthException as err: _LOGGER.debug("Subscriber authentication error: %s", err) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False + raise ConfigEntryAuthFailed from err except ConfigurationException as err: _LOGGER.error("Configuration error: %s", err) subscriber.stop_async() @@ -198,10 +190,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -214,14 +203,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.debug("Stopping nest subscriber") subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] subscriber.stop_async() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(DATA_SUBSCRIBER) hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 3b571354c0f39..29f39f5aec30d 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -22,7 +22,7 @@ def __init__( oauth_session: config_entry_oauth2_flow.OAuth2Session, client_id: str, client_secret: str, - ): + ) -> None: """Initialize Google Nest Device Access auth.""" super().__init__(websession, API_URL) self._oauth_session = oauth_session diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index d49ec8535cc96..0bf65f2163cca 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -1,14 +1,14 @@ """Support for Nest binary sensors that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DATA_SDM from .legacy.binary_sensor import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the binary sensors.""" assert DATA_SDM not in entry.data diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index f0e0b8e05fa11..ca117f0cbf17a 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -1,7 +1,7 @@ """Support for Nest cameras that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .camera_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +9,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the cameras.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index ce6ff897a2fac..f8f2db506e281 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -16,9 +16,9 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from .const import DATA_SUBSCRIBER, DOMAIN @@ -31,7 +31,7 @@ async def async_setup_sdm_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the cameras.""" @@ -56,7 +56,7 @@ async def async_setup_sdm_entry( class NestCamera(Camera): """Devices that support cameras.""" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: """Initialize the camera.""" super().__init__() self._device = device diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index a74a50b0f3615..1644cc46004b7 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -1,7 +1,7 @@ """Support for Nest climate that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .climate_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +9,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the climate platform.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index e02ebcd2dee58..ab987ff332f78 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -35,8 +35,8 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo @@ -78,7 +78,7 @@ async def async_setup_sdm_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the client entities.""" @@ -98,7 +98,7 @@ async def async_setup_sdm_entry( class ThermostatEntity(ClimateEntity): """A nest thermostat climate entity.""" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" self._device = device self._device_info = DeviceInfo(device) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index fd5eef34c7d38..c6ebe543c99e5 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -21,7 +21,6 @@ import async_timeout import voluptuous as vol -from homeassistant import config_entries from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow @@ -65,7 +64,6 @@ class UnexpectedStateError(HomeAssistantError): """Raised when the config flow is invoked in a 'should not happen' case.""" -@config_entries.HANDLERS.register(DOMAIN) class NestFlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN ): @@ -73,7 +71,6 @@ class NestFlowHandler( DOMAIN = DOMAIN VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH def __init__(self): """Initialize NestFlowHandler.""" @@ -115,7 +112,7 @@ async def async_oauth_create_entry(self, data: dict) -> dict: # Update existing config entry when in the reauth flow. This # integration only supports one config entry so remove any prior entries # added before the "single_instance_allowed" check was added - existing_entries = self.hass.config_entries.async_entries(DOMAIN) + existing_entries = self._async_current_entries() if existing_entries: updated = False for entry in existing_entries: @@ -151,7 +148,7 @@ async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self.is_sdm_api(): # Reauth will update an existing entry - if self.hass.config_entries.async_entries(DOMAIN) and not self._reauth: + if self._async_current_entries() and not self._reauth: return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) return await self.async_step_init(user_input) @@ -162,7 +159,7 @@ async def async_step_init(self, user_input=None): flows = self.hass.data.get(DATA_FLOW_IMPL, {}) - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") if not flows: @@ -232,7 +229,7 @@ async def async_step_import(self, info): """Import existing auth from Nest.""" assert not self.is_sdm_api(), "Step only supported for legacy API" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") config_path = info["nest_conf_path"] diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 36419d0dd6b7a..579733de8ad96 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -18,7 +18,7 @@ class DeviceInfo: device_brand = "Google Nest" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: """Initialize the DeviceInfo.""" self._device = device diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index 60faa90e8b462..b0083dcf99016 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -249,7 +249,9 @@ def shut_down(event): """Stop Nest update event listener.""" nest.update_event.set() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + ) _LOGGER.debug("async_setup_nest is done") diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 734261d9b0861..201ae40583e90 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -7,5 +7,10 @@ "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.12"], "codeowners": ["@allenporter"], "quality_scale": "platinum", - "dhcp": [{"macaddress":"18B430*"}] + "dhcp": [ + { + "macaddress": "18B430*" + } + ], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index 0dcc89e2262cc..c58ad26112d89 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -1,7 +1,7 @@ """Support for Nest sensors that dispatches between API versions.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from .const import DATA_SDM from .legacy.sensor import async_setup_legacy_entry @@ -9,7 +9,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 06e2b68d7cf60..8182ef3ed951d 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -15,8 +15,8 @@ PERCENTAGE, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo @@ -33,7 +33,7 @@ async def async_setup_sdm_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" @@ -56,7 +56,7 @@ async def async_setup_sdm_entry( class SensorBase(SensorEntity): """Representation of a dynamically updated Sensor.""" - def __init__(self, device: Device): + def __init__(self, device: Device) -> None: """Initialize the sensor.""" self._device = device self._device_info = DeviceInfo(device) diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index e10e626464378..b2ae06c3430f9 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,37 +1,71 @@ # Describes the format for available Nest services set_away_mode: + name: Set away mode description: Set the away mode for a Nest structure. fields: away_mode: - description: New mode to set. Valid modes are "away" or "home". + name: Away mode + description: New mode to set. example: "away" + required: true + selector: + select: + options: + - 'away' + - 'home' structure: + name: Structure description: Name(s) of structure(s) to change. Defaults to all structures if not specified. example: "Apartment" + selector: + object: set_eta: + name: Set estimated time of arrival description: Set or update the estimated time of arrival window for a Nest structure. fields: eta: + name: ETA description: Estimated time of arrival from now. example: "00:10:30" + required: true + selector: + time: eta_window: - description: Estimated time of arrival window. Default is 1 minute. + name: ETA window + description: Estimated time of arrival window. example: "00:05" + default: "00:01" + selector: + time: trip_id: + name: Trip ID description: Unique ID for the trip. Default is auto-generated using a timestamp. example: "Leave Work" + selector: + text: structure: + name: Structure description: Name(s) of structure(s) to change. Defaults to all structures if not specified. example: "Apartment" + selector: + object: cancel_eta: + name: Cancel ETA description: Cancel an existing estimated time of arrival window for a Nest structure. fields: trip_id: + name: Trip ID description: Unique ID for the trip. + required: true example: "Leave Work" + selector: + text: structure: + name: Structure description: Name(s) of structure(s) to change. Defaults to all structures if not specified. example: "Apartment" + selector: + object: diff --git a/homeassistant/components/nest/translations/bg.json b/homeassistant/components/nest/translations/bg.json index b45171d28171f..e53ab436a778b 100644 --- a/homeassistant/components/nest/translations/bg.json +++ b/homeassistant/components/nest/translations/bg.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a." }, "error": { diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index bc19d5c2c7c1d..35cc6eff9463d 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index 843ce983305e5..cbba19dac1d32 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", diff --git a/homeassistant/components/nest/translations/da.json b/homeassistant/components/nest/translations/da.json index 234c9ba97f445..054b444250697 100644 --- a/homeassistant/components/nest/translations/da.json +++ b/homeassistant/components/nest/translations/da.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", "authorize_url_timeout": "Timeout ved generering af autoriseret url." }, "error": { diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 3925b7537b220..dde725681d8af 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index d7b000d921fe2..4487beb0f434e 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", diff --git a/homeassistant/components/nest/translations/es-419.json b/homeassistant/components/nest/translations/es-419.json index 94c1d95e4d77c..185856f61de95 100644 --- a/homeassistant/components/nest/translations/es-419.json +++ b/homeassistant/components/nest/translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n." }, "error": { diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 4c0b8b2617c91..f9e88c9180f17 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 1319bc2ca4b79..773835ea29be4 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.", "authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.", "missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.", "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", diff --git a/homeassistant/components/nest/translations/fi.json b/homeassistant/components/nest/translations/fi.json index d81891d89b405..5365f73b72111 100644 --- a/homeassistant/components/nest/translations/fi.json +++ b/homeassistant/components/nest/translations/fi.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "authorize_url_fail": "Tuntematon virhe luotaessa valtuutuksen URL-osoitetta." - }, "step": { "init": { "data": { diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index ce716fb3083bc..2d74b179d19ed 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index 9d0974128ddb2..6f43df5ac81eb 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea.", "authorize_url_timeout": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e6\u05d9\u05e8\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea" }, "error": { diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 9400ea7875c6c..5690724c4a001 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 757c53e286622..d035433361fcc 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index c6e62db314d7f..bb4c916384dde 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", diff --git a/homeassistant/components/nest/translations/ko.json b/homeassistant/components/nest/translations/ko.json index f8d6de2244a85..048a0c7d10076 100644 --- a/homeassistant/components/nest/translations/ko.json +++ b/homeassistant/components/nest/translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/nest/translations/lb.json b/homeassistant/components/nest/translations/lb.json index 612d1f302589d..010f34e0dc545 100644 --- a/homeassistant/components/nest/translations/lb.json +++ b/homeassistant/components/nest/translations/lb.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich", diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index b4a965f495574..53066e9e72097 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -1,9 +1,8 @@ { "config": { "abort": { - "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", "reauth_successful": "Herauthenticatie was succesvol", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", diff --git a/homeassistant/components/nest/translations/nn.json b/homeassistant/components/nest/translations/nn.json index d9251d1e3277e..19094b460ecef 100644 --- a/homeassistant/components/nest/translations/nn.json +++ b/homeassistant/components/nest/translations/nn.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ukjent feil ved generering av autentiserings-URL", "authorize_url_timeout": "Tida gjekk ut for generert autentikasjons-URL" }, "error": { diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index d6b6c89bcaa14..914545d5a5476 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index d1147e03afc31..98506684dbb55 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index ae2d88eeeaae0..aab253057dda5 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Excedido tempo limite de url de autoriza\u00e7\u00e3o" }, "error": { diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 33ff857af7e6c..13a7439b93ddd 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", diff --git a/homeassistant/components/nest/translations/ro.json b/homeassistant/components/nest/translations/ro.json index afad668a98bee..be88440071785 100644 --- a/homeassistant/components/nest/translations/ro.json +++ b/homeassistant/components/nest/translations/ro.json @@ -1,6 +1,12 @@ { "config": { + "error": { + "unknown": "Eroare nea\u0219teptat\u0103" + }, "step": { + "init": { + "description": "Alege metoda de autentificare" + }, "link": { "data": { "code": "Cod PIN" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 07ac5246cbfa1..1919f8db89ee9 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -1,9 +1,8 @@ { "config": { "abort": { - "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \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.", "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.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "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.", @@ -38,7 +37,7 @@ }, "reauth_confirm": { "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Nest", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } }, diff --git a/homeassistant/components/nest/translations/sl.json b/homeassistant/components/nest/translations/sl.json index 25660b4805ec6..84f07fdcf429d 100644 --- a/homeassistant/components/nest/translations/sl.json +++ b/homeassistant/components/nest/translations/sl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", "reauth_successful": "Ponovna overitev je uspela.", "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index cddb9e2fe79a4..d929451e504b0 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "Ok\u00e4nt fel vid generering av autentisieringsadress.", "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress." }, "error": { diff --git a/homeassistant/components/nest/translations/uk.json b/homeassistant/components/nest/translations/uk.json index f2869a76f4258..f2ee64e7fc4b6 100644 --- a/homeassistant/components/nest/translations/uk.json +++ b/homeassistant/components/nest/translations/uk.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", diff --git a/homeassistant/components/nest/translations/zh-Hans.json b/homeassistant/components/nest/translations/zh-Hans.json index 38be428a0c711..3d481dec9d5db 100644 --- a/homeassistant/components/nest/translations/zh-Hans.json +++ b/homeassistant/components/nest/translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002" }, "error": { diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index a271ca666f42c..d9b29c7fa7ad4 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index b9b04a08febbe..354ce2cf9429a 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,5 +1,4 @@ """The Netatmo integration.""" -import asyncio import logging import secrets @@ -20,7 +19,11 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers import ( + aiohttp_client, + config_entry_oauth2_flow, + config_validation as cv, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -41,6 +44,10 @@ DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN, + PLATFORMS, + WEBHOOK_ACTIVATION, + WEBHOOK_DEACTIVATION, + WEBHOOK_PUSH_TYPE, ) from .data_handler import NetatmoDataHandler from .webhook import async_handle_webhook @@ -59,8 +66,6 @@ extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["camera", "climate", "light", "sensor"] - async def async_setup(hass: HomeAssistant, config: dict): """Set up the Netatmo component.""" @@ -103,18 +108,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) hass.data[DOMAIN][entry.entry_id] = { - AUTH: api.ConfigEntryNetatmoAuth(hass, entry, implementation) + AUTH: api.AsyncConfigEntryNetatmoAuth( + aiohttp_client.async_get_clientsession(hass), session + ) } data_handler = NetatmoDataHandler(hass, entry) await data_handler.async_setup() hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) async def unregister_webhook(_): if CONF_WEBHOOK_ID not in entry.data: @@ -123,9 +128,10 @@ async def unregister_webhook(_): async_dispatcher_send( hass, f"signal-{DOMAIN}-webhook-None", - {"type": "None", "data": {"push_type": "webhook_deactivation"}}, + {"type": "None", "data": {WEBHOOK_PUSH_TYPE: WEBHOOK_DEACTIVATION}}, ) webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook() async def register_webhook(event): if CONF_WEBHOOK_ID not in entry.data: @@ -146,9 +152,9 @@ async def register_webhook(event): entry.data[CONF_WEBHOOK_ID] ) - if entry.data["auth_implementation"] == "cloud" and not webhook_url.startswith( - "https://" - ): + if entry.data[ + "auth_implementation" + ] == cloud.DOMAIN and not webhook_url.startswith("https://"): _LOGGER.warning( "Webhook not registered - " "https and port 443 is required to register the webhook" @@ -166,7 +172,7 @@ async def register_webhook(event): async def handle_event(event): """Handle webhook events.""" - if event["data"]["push_type"] == "webhook_activation": + if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: if activation_listener is not None: activation_listener() @@ -179,16 +185,16 @@ async def handle_event(event): handle_event, ) - activation_timeout = async_call_later(hass, 10, unregister_webhook) + activation_timeout = async_call_later(hass, 30, unregister_webhook) - await hass.async_add_executor_job( - hass.data[DOMAIN][entry.entry_id][AUTH].addwebhook, webhook_url - ) + await hass.data[DOMAIN][entry.entry_id][AUTH].async_addwebhook(webhook_url) _LOGGER.info("Register Netatmo webhook: %s", webhook_url) except pyatmo.ApiError as err: _LOGGER.error("Error during webhook registration - %s", err) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + ) if hass.state == CoreState.running: await register_webhook(None) @@ -204,21 +210,13 @@ async def handle_event(event): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" if CONF_WEBHOOK_ID in entry.data: - await hass.async_add_executor_job( - hass.data[DOMAIN][entry.entry_id][AUTH].dropwebhook - ) + webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook() _LOGGER.info("Unregister Netatmo webhook") await hass.data[DOMAIN][entry.entry_id][DATA_HANDLER].async_cleanup() - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index c13a0b899c7ff..19dfdac359b81 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -1,34 +1,24 @@ """API for Netatmo bound to HASS OAuth.""" -from asyncio import run_coroutine_threadsafe - +from aiohttp import ClientSession import pyatmo -from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow -class ConfigEntryNetatmoAuth(pyatmo.auth.NetatmoOAuth2): +class AsyncConfigEntryNetatmoAuth(pyatmo.auth.AbstractAsyncAuth): """Provide Netatmo authentication tied to an OAuth2 based config entry.""" def __init__( self, - hass: core.HomeAssistant, - config_entry: config_entries.ConfigEntry, - implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ): - """Initialize Netatmo Auth.""" - self.hass = hass - self.session = config_entry_oauth2_flow.OAuth2Session( - hass, config_entry, implementation - ) - super().__init__(token=self.session.token) - - def refresh_tokens( - self, - ) -> dict: - """Refresh and return new Netatmo tokens using Home Assistant OAuth2 session.""" - run_coroutine_threadsafe( - self.session.async_ensure_token_valid(), self.hass.loop - ).result() + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ) -> None: + """Initialize the auth.""" + super().__init__(websession) + self._oauth_session = oauth_session - return self.session.token + async def async_get_access_token(self): + """Return a valid access token for Netatmo API.""" + if not self._oauth_session.valid_token: + await self._oauth_session.async_ensure_token_valid() + return self._oauth_session.token["access_token"] diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 5445231282cce..7004ef0c47229 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,8 +1,8 @@ """Support for the Netatmo cameras.""" import logging +import aiohttp import pyatmo -import requests import voluptuous as vol from homeassistant.components.camera import SUPPORT_STREAM, Camera @@ -31,6 +31,9 @@ SERVICE_SET_PERSON_AWAY, SERVICE_SET_PERSONS_HOME, SIGNAL_NAME, + WEBHOOK_LIGHT_MODE, + WEBHOOK_NACAMERA_CONNECTION, + WEBHOOK_PUSH_TYPE, ) from .data_handler import CAMERA_DATA_CLASS_NAME from .netatmo_entity_base import NetatmoBase @@ -46,60 +49,42 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.info( "Cameras are currently not supported with this authentication method" ) - return data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] await data_handler.register_data_class( CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None ) + data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) - if CAMERA_DATA_CLASS_NAME not in data_handler.data: + if not data_class or not data_class.raw_data: raise PlatformNotReady - async def get_entities(): - """Retrieve Netatmo entities.""" - - if not data_handler.data.get(CAMERA_DATA_CLASS_NAME): - return [] - - data_class = data_handler.data[CAMERA_DATA_CLASS_NAME] - - entities = [] - try: - all_cameras = [] - for home in data_class.cameras.values(): - for camera in home.values(): - all_cameras.append(camera) - - for camera in all_cameras: - _LOGGER.debug("Adding camera %s %s", camera["id"], camera["name"]) - entities.append( - NetatmoCamera( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], - DEFAULT_QUALITY, - ) - ) - - for person_id, person_data in data_handler.data[ - CAMERA_DATA_CLASS_NAME - ].persons.items(): - hass.data[DOMAIN][DATA_PERSONS][person_id] = person_data.get( - ATTR_PSEUDO - ) - except pyatmo.NoDevice: - _LOGGER.debug("No cameras found") - - return entities + all_cameras = [] + for home in data_class.cameras.values(): + for camera in home.values(): + all_cameras.append(camera) + + entities = [ + NetatmoCamera( + data_handler, + camera["id"], + camera["type"], + camera["home_id"], + DEFAULT_QUALITY, + ) + for camera in all_cameras + ] - async_add_entities(await get_entities(), True) + for person_id, person_data in data_handler.data[ + CAMERA_DATA_CLASS_NAME + ].persons.items(): + hass.data[DOMAIN][DATA_PERSONS][person_id] = person_data.get(ATTR_PSEUDO) - await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None) + _LOGGER.debug("Adding cameras %s", entities) + async_add_entities(entities, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_PERSONS_HOME, @@ -176,45 +161,34 @@ def handle_event(self, event): return if data["home_id"] == self._home_id and data["camera_id"] == self._id: - if data["push_type"] in ["NACamera-off", "NACamera-disconnection"]: + if data[WEBHOOK_PUSH_TYPE] in ["NACamera-off", "NACamera-disconnection"]: self.is_streaming = False self._status = "off" - elif data["push_type"] in ["NACamera-on", "NACamera-connection"]: + elif data[WEBHOOK_PUSH_TYPE] in [ + "NACamera-on", + WEBHOOK_NACAMERA_CONNECTION, + ]: self.is_streaming = True self._status = "on" - elif data["push_type"] == "NOC-light_mode": + elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE: self._light_state = data["sub_type"] self.async_write_ha_state() return - def camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" try: - if self._localurl: - response = requests.get( - f"{self._localurl}/live/snapshot_720.jpg", timeout=10 - ) - elif self._vpnurl: - response = requests.get( - f"{self._vpnurl}/live/snapshot_720.jpg", - timeout=10, - verify=True, - ) - else: - _LOGGER.error("Welcome/Presence VPN URL is None") - (self._vpnurl, self._localurl) = self._data.camera_urls( - camera_id=self._id - ) - return None - - except requests.exceptions.RequestException as error: - _LOGGER.info("Welcome/Presence URL changed: %s", error) - self._data.update_camera_urls(camera_id=self._id) - (self._vpnurl, self._localurl) = self._data.camera_urls(camera_id=self._id) - return None - - return response.content + return await self._data.async_get_live_snapshot(camera_id=self._id) + except ( + aiohttp.ClientPayloadError, + aiohttp.ContentTypeError, + aiohttp.ServerDisconnectedError, + aiohttp.ClientConnectorError, + pyatmo.exceptions.ApiError, + ) as err: + _LOGGER.debug("Could not fetch live camera image (%s)", err) + return None @property def extra_state_attributes(self): @@ -255,15 +229,17 @@ def is_on(self): """Return true if on.""" return self.is_streaming - def turn_off(self): + async def async_turn_off(self): """Turn off camera.""" - self._data.set_state( + await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, monitoring="off" ) - def turn_on(self): + async def async_turn_on(self): """Turn on camera.""" - self._data.set_state(home_id=self._home_id, camera_id=self._id, monitoring="on") + await self._data.async_set_state( + home_id=self._home_id, camera_id=self._id, monitoring="on" + ) async def stream_source(self): """Return the stream source.""" @@ -312,7 +288,7 @@ def process_events(self, events): ] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" return events - def _service_set_persons_home(self, **kwargs): + async def _service_set_persons_home(self, **kwargs): """Service to change current home schedule.""" persons = kwargs.get(ATTR_PERSONS) person_ids = [] @@ -321,10 +297,12 @@ def _service_set_persons_home(self, **kwargs): if data.get("pseudo") == person: person_ids.append(pid) - self._data.set_persons_home(person_ids=person_ids, home_id=self._home_id) + await self._data.async_set_persons_home( + person_ids=person_ids, home_id=self._home_id + ) _LOGGER.debug("Set %s as at home", persons) - def _service_set_person_away(self, **kwargs): + async def _service_set_person_away(self, **kwargs): """Service to mark a person as away or set the home as empty.""" person = kwargs.get(ATTR_PERSON) person_id = None @@ -333,25 +311,25 @@ def _service_set_person_away(self, **kwargs): if data.get("pseudo") == person: person_id = pid - if person_id is not None: - self._data.set_persons_away( + if person_id: + await self._data.async_set_persons_away( person_id=person_id, home_id=self._home_id, ) _LOGGER.debug("Set %s as away", person) else: - self._data.set_persons_away( + await self._data.async_set_persons_away( person_id=person_id, home_id=self._home_id, ) _LOGGER.debug("Set home as empty") - def _service_set_camera_light(self, **kwargs): + async def _service_set_camera_light(self, **kwargs): """Service to set light mode.""" mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE) _LOGGER.debug("Turn %s camera light for '%s'", mode, self._name) - self._data.set_state( + await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, floodlight=mode, diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 9993b4efac2e7..ce1eba11b70ab 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -116,55 +116,47 @@ async def async_setup_entry(hass, entry, async_add_entities): ) home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME) - if HOMEDATA_DATA_CLASS_NAME not in data_handler.data: + if not home_data or home_data.raw_data == {}: raise PlatformNotReady - async def get_entities(): - """Retrieve Netatmo entities.""" - entities = [] - - for home_id in get_all_home_ids(home_data): - _LOGGER.debug("Setting up home %s", home_id) - for room_id in home_data.rooms[home_id].keys(): - room_name = home_data.rooms[home_id][room_id]["name"] - _LOGGER.debug("Setting up room %s (%s)", room_name, room_id) - signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( - HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id - ) - home_status = data_handler.data.get(signal_name) - if home_status and room_id in home_status.rooms: - entities.append(NetatmoThermostat(data_handler, home_id, room_id)) - - hass.data[DOMAIN][DATA_SCHEDULES][home_id] = { - schedule_id: schedule_data.get("name") - for schedule_id, schedule_data in ( - data_handler.data[HOMEDATA_DATA_CLASS_NAME] - .schedules[home_id] - .items() - ) - } + if HOMEDATA_DATA_CLASS_NAME not in data_handler.data: + raise PlatformNotReady - hass.data[DOMAIN][DATA_HOMES] = { - home_id: home_data.get("name") - for home_id, home_data in ( - data_handler.data[HOMEDATA_DATA_CLASS_NAME].homes.items() + entities = [] + for home_id in get_all_home_ids(home_data): + for room_id in home_data.rooms[home_id]: + signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}" + await data_handler.register_data_class( + HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id + ) + home_status = data_handler.data.get(signal_name) + if home_status and room_id in home_status.rooms: + entities.append(NetatmoThermostat(data_handler, home_id, room_id)) + + hass.data[DOMAIN][DATA_SCHEDULES][home_id] = { + schedule_id: schedule_data.get("name") + for schedule_id, schedule_data in ( + data_handler.data[HOMEDATA_DATA_CLASS_NAME].schedules[home_id].items() ) } - return entities - - async_add_entities(await get_entities(), True) + hass.data[DOMAIN][DATA_HOMES] = { + home_id: home_data.get("name") + for home_id, home_data in ( + data_handler.data[HOMEDATA_DATA_CLASS_NAME].homes.items() + ) + } - await data_handler.unregister_data_class(HOMEDATA_DATA_CLASS_NAME, None) + _LOGGER.debug("Adding climate devices %s", entities) + async_add_entities(entities, True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() if home_data is not None: platform.async_register_entity_service( SERVICE_SET_SCHEDULE, {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, - "_service_set_schedule", + "_async_service_set_schedule", ) @@ -205,7 +197,6 @@ def __init__(self, data_handler, home_id, room_id): self._model = NA_THERM break - self._state = None self._device_name = self._data.rooms[home_id][room_id]["name"] self._name = f"{MANUFACTURER} {self._device_name}" self._current_temperature = None @@ -357,24 +348,24 @@ def hvac_action(self) -> str | None: return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_OFF: - self.turn_off() + await self.async_turn_off() elif hvac_mode == HVAC_MODE_AUTO: if self.hvac_mode == HVAC_MODE_OFF: - self.turn_on() - self.set_preset_mode(PRESET_SCHEDULE) + await self.async_turn_on() + await self.async_set_preset_mode(PRESET_SCHEDULE) elif hvac_mode == HVAC_MODE_HEAT: - self.set_preset_mode(PRESET_BOOST) + await self.async_set_preset_mode(PRESET_BOOST) - def set_preset_mode(self, preset_mode: str) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if self.hvac_mode == HVAC_MODE_OFF: - self.turn_on() + await self.async_turn_on() if self.target_temperature == 0: - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, STATE_NETATMO_HOME, ) @@ -384,14 +375,14 @@ def set_preset_mode(self, preset_mode: str) -> None: and self._model == NA_VALVE and self.hvac_mode == HVAC_MODE_HEAT ): - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, STATE_NETATMO_HOME, ) elif ( preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self._model == NA_VALVE ): - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, @@ -400,13 +391,15 @@ def set_preset_mode(self, preset_mode: str) -> None: preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self.hvac_mode == HVAC_MODE_HEAT ): - self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_HOME) + await self._home_status.async_set_room_thermpoint( + self._id, STATE_NETATMO_HOME + ) elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]: - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, PRESET_MAP_NETATMO[preset_mode] ) elif preset_mode in [PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY]: - self._home_status.set_thermmode(PRESET_MAP_NETATMO[preset_mode]) + await self._home_status.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode]) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) @@ -422,12 +415,14 @@ def preset_modes(self) -> list[str] | None: """Return a list of available preset modes.""" return SUPPORT_PRESET - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature for 2 hours.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is None: return - self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_MANUAL, temp) + await self._home_status.async_set_room_thermpoint( + self._id, STATE_NETATMO_MANUAL, temp + ) self.async_write_ha_state() @@ -449,21 +444,23 @@ def extra_state_attributes(self): return attr - def turn_off(self): + async def async_turn_off(self): """Turn the entity off.""" if self._model == NA_VALVE: - self._home_status.set_room_thermpoint( + await self._home_status.async_set_room_thermpoint( self._id, STATE_NETATMO_MANUAL, DEFAULT_MIN_TEMP, ) elif self.hvac_mode != HVAC_MODE_OFF: - self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_OFF) + await self._home_status.async_set_room_thermpoint( + self._id, STATE_NETATMO_OFF + ) self.async_write_ha_state() - def turn_on(self): + async def async_turn_on(self): """Turn the entity on.""" - self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_HOME) + await self._home_status.async_set_room_thermpoint(self._id, STATE_NETATMO_HOME) self.async_write_ha_state() @property @@ -475,6 +472,11 @@ def available(self) -> bool: def async_update_callback(self): """Update the entity's state.""" self._home_status = self.data_handler.data[self._home_status_class] + if self._home_status is None: + if self.available: + self._connected = False + return + self._room_status = self._home_status.rooms.get(self._id) self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id) @@ -506,7 +508,7 @@ def async_update_callback(self): self._target_temperature = roomstatus["target_temperature"] self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] self._hvac_mode = HVAC_MAP_NETATMO[self._preset] - self._battery_level = roomstatus.get("battery_level") + self._battery_level = roomstatus.get("battery_state") self._connected = True self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] @@ -546,7 +548,7 @@ def _build_room_status(self): roomstatus["heating_status"] = self._boilerstatus batterylevel = self._home_status.thermostats[ roomstatus["module_id"] - ].get("battery_level") + ].get("battery_state") elif roomstatus["module_type"] == NA_VALVE: roomstatus["heating_power_request"] = self._room_status[ "heating_power_request" @@ -557,16 +559,11 @@ def _build_room_status(self): self._boilerstatus and roomstatus["heating_status"] ) batterylevel = self._home_status.valves[roomstatus["module_id"]].get( - "battery_level" + "battery_state" ) if batterylevel: - batterypct = interpolate(batterylevel, roomstatus["module_type"]) - if ( - not roomstatus.get("battery_level") - or batterypct < roomstatus["battery_level"] - ): - roomstatus["battery_level"] = batterypct + roomstatus["battery_state"] = batterylevel return roomstatus @@ -575,7 +572,7 @@ def _build_room_status(self): return {} - def _service_set_schedule(self, **kwargs): + async def _async_service_set_schedule(self, **kwargs): schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_id = None for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): @@ -583,12 +580,12 @@ def _service_set_schedule(self, **kwargs): schedule_id = sid if not schedule_id: - _LOGGER.error( - "%s is not a invalid schedule", kwargs.get(ATTR_SCHEDULE_NAME) - ) + _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return - self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id) + await self._data.async_switch_home_schedule( + home_id=self._home_id, schedule_id=schedule_id + ) _LOGGER.debug( "Setting %s schedule to %s (%s)", self._home_id, @@ -602,48 +599,6 @@ def device_info(self): return {**super().device_info, "suggested_area": self._room_data["name"]} -def interpolate(batterylevel: int, module_type: str) -> int: - """Interpolate battery level depending on device type.""" - na_battery_levels = { - NA_THERM: { - "full": 4100, - "high": 3600, - "medium": 3300, - "low": 3000, - "empty": 2800, - }, - NA_VALVE: { - "full": 3200, - "high": 2700, - "medium": 2400, - "low": 2200, - "empty": 2200, - }, - } - - levels = sorted(na_battery_levels[module_type].values()) - steps = [20, 50, 80, 100] - - na_battery_level = na_battery_levels[module_type] - if batterylevel >= na_battery_level["full"]: - return 100 - if batterylevel >= na_battery_level["high"]: - i = 3 - elif batterylevel >= na_battery_level["medium"]: - i = 2 - elif batterylevel >= na_battery_level["low"]: - i = 1 - else: - return 0 - - pct = steps[i - 1] + ( - (steps[i] - steps[i - 1]) - * (batterylevel - levels[i]) - / (levels[i + 1] - levels[i]) - ) - return int(pct) - - def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]: """Get all the home ids returned by NetAtmo API.""" if home_data is None: @@ -651,8 +606,5 @@ def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]: return [ home_data.homes[home_id]["id"] for home_id in home_data.homes - if ( - "therm_schedules" in home_data.homes[home_id] - and "modules" in home_data.homes[home_id] - ) + if "modules" in home_data.homes[home_id] ] diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 6217ef1a0e641..ea44339b99f13 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -29,7 +29,6 @@ class NetatmoFlowHandler( """Config flow to handle Netatmo OAuth2 authentication.""" DOMAIN = DOMAIN - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback @@ -76,7 +75,7 @@ async def async_step_user(self, user_input=None): class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): """Handle Netatmo options.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Netatmo options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index b0a312fa1f391..2f840baa4c326 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -1,9 +1,16 @@ """Constants used by the Netatmo component.""" +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + API = "api" DOMAIN = "netatmo" MANUFACTURER = "Netatmo" +PLATFORMS = [CAMERA_DOMAIN, CLIMATE_DOMAIN, LIGHT_DOMAIN, SENSOR_DOMAIN] + MODEL_NAPLUG = "Relay" MODEL_NATHERM1 = "Smart Thermostat" MODEL_NRV = "Smart Radiator Valves" @@ -156,3 +163,9 @@ MODE_LIGHT_OFF = "off" MODE_LIGHT_AUTO = "auto" CAMERA_LIGHT_MODES = [MODE_LIGHT_ON, MODE_LIGHT_OFF, MODE_LIGHT_AUTO] + +WEBHOOK_ACTIVATION = "webhook_activation" +WEBHOOK_DEACTIVATION = "webhook_deactivation" +WEBHOOK_NACAMERA_CONNECTION = "NACamera-connection" +WEBHOOK_PUSH_TYPE = "push_type" +WEBHOOK_LIGHT_MODE = "NOC-light_mode" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 6982a651a4543..83215bd3af56d 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -1,13 +1,12 @@ """The Netatmo data handler.""" from __future__ import annotations +import asyncio from collections import deque from datetime import timedelta -from functools import partial from itertools import islice import logging from time import time -from typing import Deque import pyatmo @@ -16,26 +15,34 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval -from .const import AUTH, DOMAIN, MANUFACTURER +from .const import ( + AUTH, + DOMAIN, + MANUFACTURER, + WEBHOOK_ACTIVATION, + WEBHOOK_DEACTIVATION, + WEBHOOK_NACAMERA_CONNECTION, + WEBHOOK_PUSH_TYPE, +) _LOGGER = logging.getLogger(__name__) -CAMERA_DATA_CLASS_NAME = "CameraData" -WEATHERSTATION_DATA_CLASS_NAME = "WeatherStationData" -HOMECOACH_DATA_CLASS_NAME = "HomeCoachData" -HOMEDATA_DATA_CLASS_NAME = "HomeData" -HOMESTATUS_DATA_CLASS_NAME = "HomeStatus" -PUBLICDATA_DATA_CLASS_NAME = "PublicData" +CAMERA_DATA_CLASS_NAME = "AsyncCameraData" +WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData" +HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData" +HOMEDATA_DATA_CLASS_NAME = "AsyncHomeData" +HOMESTATUS_DATA_CLASS_NAME = "AsyncHomeStatus" +PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData" NEXT_SCAN = "next_scan" DATA_CLASSES = { - WEATHERSTATION_DATA_CLASS_NAME: pyatmo.WeatherStationData, - HOMECOACH_DATA_CLASS_NAME: pyatmo.HomeCoachData, - CAMERA_DATA_CLASS_NAME: pyatmo.CameraData, - HOMEDATA_DATA_CLASS_NAME: pyatmo.HomeData, - HOMESTATUS_DATA_CLASS_NAME: pyatmo.HomeStatus, - PUBLICDATA_DATA_CLASS_NAME: pyatmo.PublicData, + WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData, + HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData, + CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData, + HOMEDATA_DATA_CLASS_NAME: pyatmo.AsyncHomeData, + HOMESTATUS_DATA_CLASS_NAME: pyatmo.AsyncHomeStatus, + PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData, } BATCH_SIZE = 3 @@ -53,14 +60,14 @@ class NetatmoDataHandler: """Manages the Netatmo data handling.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize self.""" self.hass = hass self._auth = hass.data[DOMAIN][entry.entry_id][AUTH] self.listeners: list[CALLBACK_TYPE] = [] - self._data_classes: dict = {} + self.data_classes: dict = {} self.data = {} - self._queue: Deque = deque() + self._queue = deque() self._webhook: bool = False async def async_setup(self): @@ -88,21 +95,19 @@ async def async_update(self, event_time): for data_class in islice(self._queue, 0, BATCH_SIZE): if data_class[NEXT_SCAN] > time(): continue - self._data_classes[data_class["name"]][NEXT_SCAN] = ( + self.data_classes[data_class["name"]][NEXT_SCAN] = ( time() + data_class["interval"] ) - await self.async_fetch_data( - data_class["class"], data_class["name"], **data_class["kwargs"] - ) + await self.async_fetch_data(data_class["name"]) self._queue.rotate(BATCH_SIZE) @callback def async_force_update(self, data_class_entry): """Prioritize data retrieval for given data class entry.""" - self._data_classes[data_class_entry][NEXT_SCAN] = time() - self._queue.rotate(-(self._queue.index(self._data_classes[data_class_entry]))) + self.data_classes[data_class_entry][NEXT_SCAN] = time() + self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) async def async_cleanup(self): """Clean up the Netatmo data handler.""" @@ -111,31 +116,22 @@ async def async_cleanup(self): async def handle_event(self, event): """Handle webhook events.""" - if event["data"]["push_type"] == "webhook_activation": + if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: _LOGGER.info("%s webhook successfully registered", MANUFACTURER) self._webhook = True - elif event["data"]["push_type"] == "webhook_deactivation": + elif event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_DEACTIVATION: _LOGGER.info("%s webhook unregistered", MANUFACTURER) self._webhook = False - elif event["data"]["push_type"] == "NACamera-connection": + elif event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_NACAMERA_CONNECTION: _LOGGER.debug("%s camera reconnected", MANUFACTURER) self.async_force_update(CAMERA_DATA_CLASS_NAME) - async def async_fetch_data(self, data_class, data_class_entry, **kwargs): + async def async_fetch_data(self, data_class_entry): """Fetch data and notify.""" try: - self.data[data_class_entry] = await self.hass.async_add_executor_job( - partial(data_class, **kwargs), - self._auth, - ) - - for update_callback in self._data_classes[data_class_entry][ - "subscriptions" - ]: - if update_callback: - update_callback() + await self.data[data_class_entry].async_update() except pyatmo.NoDevice as err: _LOGGER.debug(err) @@ -144,42 +140,46 @@ async def async_fetch_data(self, data_class, data_class_entry, **kwargs): except pyatmo.ApiError as err: _LOGGER.debug(err) + except asyncio.TimeoutError as err: + _LOGGER.debug(err) + return + + for update_callback in self.data_classes[data_class_entry]["subscriptions"]: + if update_callback: + update_callback() + async def register_data_class( self, data_class_name, data_class_entry, update_callback, **kwargs ): """Register data class.""" - if data_class_entry in self._data_classes: - self._data_classes[data_class_entry]["subscriptions"].append( - update_callback - ) + if data_class_entry in self.data_classes: + self.data_classes[data_class_entry]["subscriptions"].append(update_callback) return - self._data_classes[data_class_entry] = { - "class": DATA_CLASSES[data_class_name], + self.data_classes[data_class_entry] = { "name": data_class_entry, "interval": DEFAULT_INTERVALS[data_class_name], NEXT_SCAN: time() + DEFAULT_INTERVALS[data_class_name], - "kwargs": kwargs, "subscriptions": [update_callback], } - await self.async_fetch_data( - DATA_CLASSES[data_class_name], data_class_entry, **kwargs + self.data[data_class_entry] = DATA_CLASSES[data_class_name]( + self._auth, **kwargs ) - self._queue.append(self._data_classes[data_class_entry]) + await self.async_fetch_data(data_class_entry) + + self._queue.append(self.data_classes[data_class_entry]) _LOGGER.debug("Data class %s added", data_class_entry) async def unregister_data_class(self, data_class_entry, update_callback): """Unregister data class.""" - if update_callback not in self._data_classes[data_class_entry]["subscriptions"]: - return - - self._data_classes[data_class_entry]["subscriptions"].remove(update_callback) + self.data_classes[data_class_entry]["subscriptions"].remove(update_callback) - if not self._data_classes[data_class_entry].get("subscriptions"): - self._queue.remove(self._data_classes[data_class_entry]) - self._data_classes.pop(data_class_entry) + if not self.data_classes[data_class_entry].get("subscriptions"): + self._queue.remove(self.data_classes[data_class_entry]) + self.data_classes.pop(data_class_entry) + self.data.pop(data_class_entry) _LOGGER.debug("Data class %s removed", data_class_entry) @property diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 08744c462e840..160fb00be6b51 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -1,8 +1,6 @@ """Support for the Netatmo camera lights.""" import logging -import pyatmo - from homeassistant.components.light import LightEntity from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady @@ -14,6 +12,8 @@ EVENT_TYPE_LIGHT_MODE, MANUFACTURER, SIGNAL_NAME, + WEBHOOK_LIGHT_MODE, + WEBHOOK_PUSH_TYPE, ) from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler from .netatmo_entity_base import NetatmoBase @@ -34,41 +34,29 @@ async def async_setup_entry(hass, entry, async_add_entities): await data_handler.register_data_class( CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None ) + data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) - if CAMERA_DATA_CLASS_NAME not in data_handler.data: + if not data_class or data_class.raw_data == {}: raise PlatformNotReady - async def get_entities(): - """Retrieve Netatmo entities.""" - - entities = [] - all_cameras = [] - - try: - for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values(): - for camera in home.values(): - all_cameras.append(camera) - - except pyatmo.NoDevice: - _LOGGER.debug("No cameras found") - - for camera in all_cameras: - if camera["type"] == "NOC": - _LOGGER.debug("Adding camera light %s %s", camera["id"], camera["name"]) - entities.append( - NetatmoLight( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], - ) - ) - - return entities - - async_add_entities(await get_entities(), True) + all_cameras = [] + for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values(): + for camera in home.values(): + all_cameras.append(camera) + + entities = [ + NetatmoLight( + data_handler, + camera["id"], + camera["type"], + camera["home_id"], + ) + for camera in all_cameras + if camera["type"] == "NOC" + ] - await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None) + _LOGGER.debug("Adding camera lights %s", entities) + async_add_entities(entities, True) class NetatmoLight(NetatmoBase, LightEntity): @@ -80,7 +68,7 @@ def __init__( camera_id: str, camera_type: str, home_id: str, - ): + ) -> None: """Initialize a Netatmo Presence camera light.""" LightEntity.__init__(self) super().__init__(data_handler) @@ -119,7 +107,7 @@ def handle_event(self, event): if ( data["home_id"] == self._home_id and data["camera_id"] == self._id - and data["push_type"] == "NOC-light_mode" + and data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE ): self._is_on = bool(data["sub_type"] == "on") @@ -136,19 +124,19 @@ def is_on(self): """Return true if light is on.""" return self._is_on - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn camera floodlight on.""" _LOGGER.debug("Turn camera '%s' on", self._name) - self._data.set_state( + await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, floodlight="on", ) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn camera floodlight into auto mode.""" _LOGGER.debug("Turn camera '%s' to auto mode", self._name) - self._data.set_state( + await self._data.async_set_state( home_id=self._home_id, camera_id=self._id, floodlight="auto", diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 34307f2311d3a..60a54df8a6e73 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==4.2.2" + "pyatmo==5.0.1" ], "after_dependencies": [ "cloud", @@ -23,5 +23,6 @@ "Presence", "Welcome" ] - } + }, + "iot_class": "cloud_polling" } \ No newline at end of file diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index db00df5129f25..99f52d95ad44e 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -41,7 +41,7 @@ class NetatmoSource(MediaSource): name: str = MANUFACTURER - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize Netatmo source.""" super().__init__(DOMAIN) self.hass = hass @@ -159,7 +159,7 @@ def async_parse_identifier( item: MediaSourceItem, ) -> tuple[str, str, int | None]: """Parse identifier.""" - if not item.identifier: + if not item.identifier or "/" not in item.identifier: return "events", "", None source, path = item.identifier.lstrip("/").split("/", 1) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index e41b873bdc4d4..1fcd4a121d872 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -7,7 +7,7 @@ from homeassistant.helpers.entity import Entity from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME -from .data_handler import NetatmoDataHandler +from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler _LOGGER = logging.getLogger(__name__) @@ -29,7 +29,6 @@ def __init__(self, data_handler: NetatmoDataHandler) -> None: async def async_added_to_hass(self) -> None: """Entity created.""" - _LOGGER.debug("New client %s", self.entity_id) for data_class in self._data_classes: signal_name = data_class[SIGNAL_NAME] @@ -41,7 +40,7 @@ async def async_added_to_hass(self) -> None: home_id=data_class["home_id"], ) - elif data_class["name"] == "PublicData": + elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME: await self.data_handler.register_data_class( data_class["name"], signal_name, @@ -57,7 +56,9 @@ async def async_added_to_hass(self) -> None: data_class["name"], signal_name, self.async_update_callback ) - await self.data_handler.unregister_data_class(signal_name, None) + for sub in self.data_handler.data_classes[signal_name].get("subscriptions"): + if sub is None: + await self.data_handler.unregister_data_class(signal_name, None) registry = await self.hass.helpers.device_registry.async_get_registry() device = registry.async_get_device({(DOMAIN, self._id)}, set()) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 4c6facb3eca16..e56847386a38a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -132,18 +132,8 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - await data_handler.register_data_class( - WEATHERSTATION_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME, None - ) - await data_handler.register_data_class( - HOMECOACH_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME, None - ) - async def find_entities(data_class_name): """Find all entities.""" - if data_class_name not in data_handler.data: - raise PlatformNotReady - all_module_infos = {} data = data_handler.data @@ -167,11 +157,6 @@ async def find_entities(data_class_name): _LOGGER.debug("Skipping module %s", module.get("module_name")) continue - _LOGGER.debug( - "Adding module %s %s", - module.get("module_name"), - module.get("_id"), - ) conditions = [ c.lower() for c in data_class.get_monitored_conditions(module_id=module["_id"]) @@ -188,14 +173,19 @@ async def find_entities(data_class_name): NetatmoSensor(data_handler, data_class_name, module, condition) ) - await data_handler.unregister_data_class(data_class_name, None) - + _LOGGER.debug("Adding weather sensors %s", entities) return entities for data_class_name in [ WEATHERSTATION_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME, ]: + await data_handler.register_data_class(data_class_name, data_class_name, None) + data_class = data_handler.data.get(data_class_name) + + if not data_class or not data_class.raw_data: + raise PlatformNotReady + async_add_entities(await find_entities(data_class_name), True) device_registry = await hass.helpers.device_registry.async_get_registry() @@ -333,7 +323,7 @@ def entity_registry_enabled_default(self) -> bool: return self._enabled_default @callback - def async_update_callback(self): + def async_update_callback(self): # noqa: C901 """Update the entity's state.""" if self._data is None: if self._state is None: @@ -410,6 +400,8 @@ def async_update_callback(self): self._state = None return + self.async_write_ha_state() + def fix_angle(angle: int) -> int: """Fix angle when value is negative.""" @@ -615,13 +607,6 @@ async def async_config_update_callback(self, area): @callback def async_update_callback(self): """Update the entity's state.""" - if self._data is None: - if self._state is None: - return - _LOGGER.warning("No data from update") - self._state = None - return - data = None if self.type == "temperature": @@ -655,3 +640,5 @@ def async_update_callback(self): self._state = round(sum(values) / len(values), 1) elif self._mode == "max": self._state = max(values) + + self.async_write_ha_state() diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 06f56d084c640..4cbb7cba2ba46 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -48,10 +48,10 @@ set_persons_home: fields: persons: description: List of names - example: Bob + example: "[Alice, Bob]" required: true selector: - text: + object: set_person_away: name: Set person away diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index dccb58577487e..1037d100909df 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -48,7 +48,8 @@ "lon_sw": "L\u00e4ngengrad S\u00fcdwest-Ecke", "mode": "Berechnung", "show_on_map": "Auf Karte anzeigen" - } + }, + "title": "\u00d6ffentlicher Netatmo Wettersensor" }, "public_weather_areas": { "data": { diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json index b25e084396748..1fadfb90773be 100644 --- a/homeassistant/components/netatmo/translations/ru.json +++ b/homeassistant/components/netatmo/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.", "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.", "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." }, diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index 37badeaab533c..32dfd2db6a0fc 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -3,5 +3,10 @@ "create_entry": { "default": "Autentiserad med Netatmo." } + }, + "device_automation": { + "trigger_subtype": { + "schedule": "schema" + } } } \ No newline at end of file diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 02a5bbddacd37..9d79f54450c9e 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -3,5 +3,6 @@ "name": "Netdata", "documentation": "https://www.home-assistant.io/integrations/netdata", "requirements": ["netdata==0.2.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index f30277086de47..504faef70ebe8 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -7,7 +7,7 @@ from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( @@ -25,7 +25,7 @@ CONF_APS = "accesspoints" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=""): cv.string, vol.Optional(CONF_SSL, default=False): cv.boolean, diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 1126bbe558f31..713101f657f38 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -3,5 +3,6 @@ "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", "requirements": ["pynetgear==0.6.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index e910132e78424..c02393e0f54cf 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -3,5 +3,6 @@ "name": "NETGEAR LTE", "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": ["eternalegypt==0.0.12"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/netgear_lte/services.yaml b/homeassistant/components/netgear_lte/services.yaml index 564fb914cf97b..116c2f61a2e2f 100644 --- a/homeassistant/components/netgear_lte/services.yaml +++ b/homeassistant/components/netgear_lte/services.yaml @@ -1,36 +1,69 @@ delete_sms: + name: Delete SMS description: Delete messages from the modem inbox. fields: host: + name: Host description: The modem that should have a message deleted. example: 192.168.5.1 + selector: + text: sms_id: + name: SMS ID description: Integer or list of integers with inbox IDs of messages to delete. + required: true example: 7 + selector: + object: set_option: + name: Set option description: Set options on the modem. fields: host: + name: Host description: The modem to set options on. example: 192.168.5.1 + selector: + text: failover: - description: Failover mode, auto/wire/mobile. + name: Failover + description: Failover mode. example: auto + selector: + select: + options: + - 'auto' + - 'mobile' + - 'wire' autoconnect: - description: Auto-connect mode, never/home/always. + name: Auto-connect + description: Auto-connect mode. example: home + selector: + select: + options: + - 'always' + - 'home' + - 'never' connect_lte: + name: Connect LTE description: Ask the modem to establish the LTE connection. fields: host: + name: Host description: The modem that should connect. example: 192.168.5.1 + selector: + text: disconnect_lte: + name: Disconnect LTE description: Ask the modem to close the LTE connection. fields: host: description: The modem that should disconnect. example: 192.168.5.1 + selector: + text: diff --git a/homeassistant/components/netio/manifest.json b/homeassistant/components/netio/manifest.json index ef3d4a9519fec..3a246404c9152 100644 --- a/homeassistant/components/netio/manifest.json +++ b/homeassistant/components/netio/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/netio", "requirements": ["pynetio==0.1.9.1"], "dependencies": ["http"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/neurio_energy/manifest.json b/homeassistant/components/neurio_energy/manifest.json index bba814966dfd6..a46acb46dc623 100644 --- a/homeassistant/components/neurio_energy/manifest.json +++ b/homeassistant/components/neurio_energy/manifest.json @@ -3,5 +3,6 @@ "name": "Neurio energy", "documentation": "https://www.home-assistant.io/integrations/neurio_energy", "requirements": ["neurio==0.3.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 4dde208440011..65be22b57f1b4 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -1,9 +1,9 @@ """Support for Nexia / Trane XL Thermostats.""" -import asyncio from datetime import timedelta from functools import partial import logging +from nexia.const import BRAND_NEXIA from nexia.home import NexiaHome from requests.exceptions import ConnectTimeout, HTTPError @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR +from .const import CONF_BRAND, DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) @@ -24,20 +24,13 @@ DEFAULT_UPDATE_RATE = 120 -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the nexia component from YAML.""" - - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Configure the base Nexia device for Home Assistant.""" conf = entry.data username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] + brand = conf.get(CONF_BRAND, BRAND_NEXIA) state_file = hass.config.path(f"nexia_config_{username}.conf") @@ -49,6 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): password=password, device_name=hass.config.location_name, state_file=state_file, + brand=brand, ) ) except ConnectTimeout as ex: @@ -75,29 +69,20 @@ async def _async_update_data(): update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { NEXIA_DEVICE: nexia_home, UPDATE_COORDINATOR: coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index aff3711cdaef6..2dff498f28165 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -34,12 +34,7 @@ SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -63,21 +58,13 @@ SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" -SET_AIRCLEANER_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_AIRCLEANER_MODE): cv.string, - } -) +SET_AIRCLEANER_SCHEMA = { + vol.Required(ATTR_AIRCLEANER_MODE): cv.string, +} -SET_HUMIDITY_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_HUMIDITY): vol.All( - vol.Coerce(int), vol.Range(min=35, max=65) - ), - } -) +SET_HUMIDITY_SCHEMA = { + vol.Required(ATTR_HUMIDITY): vol.All(vol.Coerce(int), vol.Range(min=35, max=65)), +} # @@ -108,7 +95,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): nexia_home = nexia_data[NEXIA_DEVICE] coordinator = nexia_data[UPDATE_COORDINATOR] - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_HUMIDIFY_SETPOINT, diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index e68564706faf2..18c20a8f92a34 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Nexia integration.""" import logging +from nexia.const import BRAND_ASAIR, BRAND_NEXIA from nexia.home import NexiaHome from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -8,12 +9,20 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN +from .const import BRAND_ASAIR_NAME, BRAND_NEXIA_NAME, CONF_BRAND, DOMAIN from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str}) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_BRAND, default=BRAND_NEXIA): vol.In( + {BRAND_NEXIA: BRAND_NEXIA_NAME, BRAND_ASAIR: BRAND_ASAIR_NAME} + ), + } +) async def validate_input(hass: core.HomeAssistant, data): @@ -27,6 +36,7 @@ async def validate_input(hass: core.HomeAssistant, data): nexia_home = NexiaHome( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], + brand=data[CONF_BRAND], auto_login=False, auto_update=False, device_name=hass.config.location_name, @@ -54,7 +64,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Nexia.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index dbe7b71705c6f..d6e3e5f800819 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -7,6 +7,8 @@ NOTIFICATION_ID = "nexia_notification" NOTIFICATION_TITLE = "Nexia Setup" +CONF_BRAND = "brand" + NEXIA_DEVICE = "device" NEXIA_SCAN_INTERVAL = "scan_interval" @@ -29,3 +31,6 @@ SIGNAL_ZONE_UPDATE = "NEXIA_CLIMATE_ZONE_UPDATE" SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE" + +BRAND_NEXIA_NAME = "Nexia" +BRAND_ASAIR_NAME = "American Standard" diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 253400c886d15..ed1247ee9e3db 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,9 +1,15 @@ { "domain": "nexia", - "name": "Nexia", - "requirements": ["nexia==0.9.6"], + "name": "Nexia/American Standard", + "requirements": ["nexia==0.9.7"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, - "dhcp": [{"hostname":"xl857-*","macaddress":"000231*"}] + "dhcp": [ + { + "hostname": "xl857-*", + "macaddress": "000231*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nexia/services.yaml b/homeassistant/components/nexia/services.yaml index 23a9498746bf6..0b822dce186f8 100644 --- a/homeassistant/components/nexia/services.yaml +++ b/homeassistant/components/nexia/services.yaml @@ -1,19 +1,38 @@ set_aircleaner_mode: + name: Set air cleaner mode description: "The air cleaner mode." + target: + entity: + integration: nexia + domain: climate fields: - entity_id: - description: "This setting will affect all zones connected to the thermostat." - example: climate.master_bedroom aircleaner_mode: - description: 'The air cleaner mode to set. Options include "auto", "quick", or "allergy".' + name: Air cleaner mode + description: 'The air cleaner mode to set.' + required: true example: allergy + selector: + select: + options: + - 'allergy' + - 'auto' + - 'quick' set_humidify_setpoint: + name: Set humidify set point description: "The humidification set point." + target: + entity: + integration: nexia + domain: climate fields: - entity_id: - description: "This setting will affect all zones connected to the thermostat." - example: climate.master_bedroom humidity: - description: "The humidification setpoint as an int, range 35-65." + name: Humidify + description: "The humidification setpoint." + required: true example: 45 + selector: + number: + min: 35 + max: 65 + unit_of_measurement: '%' diff --git a/homeassistant/components/nexia/strings.json b/homeassistant/components/nexia/strings.json index 876ea2d656f11..c9bc84243da81 100644 --- a/homeassistant/components/nexia/strings.json +++ b/homeassistant/components/nexia/strings.json @@ -2,8 +2,8 @@ "config": { "step": { "user": { - "title": "Connect to mynexia.com", "data": { + "brand": "Brand", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/nexia/translations/ca.json b/homeassistant/components/nexia/translations/ca.json index beb5b1a1d9dcf..e0f0c87f60fc0 100644 --- a/homeassistant/components/nexia/translations/ca.json +++ b/homeassistant/components/nexia/translations/ca.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marca", "password": "Contrasenya", "username": "Nom d'usuari" }, diff --git a/homeassistant/components/nexia/translations/en.json b/homeassistant/components/nexia/translations/en.json index fad0b8e542a56..20b0a1379703a 100644 --- a/homeassistant/components/nexia/translations/en.json +++ b/homeassistant/components/nexia/translations/en.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Brand", "password": "Password", "username": "Username" }, diff --git a/homeassistant/components/nexia/translations/et.json b/homeassistant/components/nexia/translations/et.json index ac8c354c3b3f5..2f9348c1eed75 100644 --- a/homeassistant/components/nexia/translations/et.json +++ b/homeassistant/components/nexia/translations/et.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Tootja", "password": "Salas\u00f5na", "username": "Kasutajanimi" }, diff --git a/homeassistant/components/nexia/translations/it.json b/homeassistant/components/nexia/translations/it.json index 254617d718be9..65d06c3c8f067 100644 --- a/homeassistant/components/nexia/translations/it.json +++ b/homeassistant/components/nexia/translations/it.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Marca", "password": "Password", "username": "Nome utente" }, diff --git a/homeassistant/components/nexia/translations/nl.json b/homeassistant/components/nexia/translations/nl.json index faa19d3b63c05..14efdfcd22139 100644 --- a/homeassistant/components/nexia/translations/nl.json +++ b/homeassistant/components/nexia/translations/nl.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Brand", "password": "Wachtwoord", "username": "Gebruikersnaam" }, diff --git a/homeassistant/components/nexia/translations/no.json b/homeassistant/components/nexia/translations/no.json index a3f143898f865..4533b94e48e79 100644 --- a/homeassistant/components/nexia/translations/no.json +++ b/homeassistant/components/nexia/translations/no.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Merke", "password": "Passord", "username": "Brukernavn" }, diff --git a/homeassistant/components/nexia/translations/ru.json b/homeassistant/components/nexia/translations/ru.json index 74ec08ec2cfef..b5572c9f2daa6 100644 --- a/homeassistant/components/nexia/translations/ru.json +++ b/homeassistant/components/nexia/translations/ru.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "\u041c\u0430\u0440\u043a\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, diff --git a/homeassistant/components/nexia/translations/zh-Hant.json b/homeassistant/components/nexia/translations/zh-Hant.json index 0dc0931afe520..c066a433d1b66 100644 --- a/homeassistant/components/nexia/translations/zh-Hant.json +++ b/homeassistant/components/nexia/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "\u54c1\u724c", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 0f32505536a7a..71001bfc52c6e 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -3,5 +3,6 @@ "name": "NextBus", "documentation": "https://www.home-assistant.io/integrations/nextbus", "codeowners": ["@vividboarder"], - "requirements": ["py_nextbusnext==0.1.4"] + "requirements": ["py_nextbusnext==0.1.4"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nextcloud/manifest.json b/homeassistant/components/nextcloud/manifest.json index 73ec2a138b3ba..03b1f429fea35 100644 --- a/homeassistant/components/nextcloud/manifest.json +++ b/homeassistant/components/nextcloud/manifest.json @@ -3,5 +3,6 @@ "name": "Nextcloud", "documentation": "https://www.home-assistant.io/integrations/nextcloud", "requirements": ["nextcloudmonitor==1.1.0"], - "codeowners": ["@meichthys"] + "codeowners": ["@meichthys"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index e727c47b1e329..6f29d4d410e13 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -2,5 +2,6 @@ "domain": "nfandroidtv", "name": "Notifications for Android TV / FireTV", "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index e71d81f2b7904..ad2f3fb3706e9 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -266,7 +266,7 @@ def load_file( if local_path is not None: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): - return open(local_path, "rb") + return open(local_path, "rb") # pylint: disable=consider-using-with _LOGGER.warning("'%s' is not secure to load data from!", local_path) else: _LOGGER.warning("Neither URL nor local path found in params!") diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index dfaaf28048e80..8608386c483c4 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -1,5 +1,4 @@ """The Nightscout integration.""" -import asyncio from asyncio import TimeoutError as AsyncIOTimeoutError from aiohttp import ClientError @@ -19,12 +18,6 @@ _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Nightscout component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Nightscout from a config entry.""" server_url = entry.data[CONF_URL] @@ -36,6 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except (ClientError, AsyncIOTimeoutError, OSError) as error: raise ConfigEntryNotReady from error + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api device_registry = await dr.async_get_registry(hass) @@ -48,25 +42,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_type="service", ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py index 2b91395d37733..1f3f62835bc33 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -39,7 +39,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Nightscout.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" @@ -67,7 +66,7 @@ async def async_step_user(self, user_input=None): class InputValidationError(exceptions.HomeAssistantError): """Error to indicate we cannot proceed due to invalid input.""" - def __init__(self, base: str): + def __init__(self, base: str) -> None: """Initialize with error base.""" super().__init__() self.base = base diff --git a/homeassistant/components/nightscout/manifest.json b/homeassistant/components/nightscout/manifest.json index ecc44258e907f..49cb077dc7956 100644 --- a/homeassistant/components/nightscout/manifest.json +++ b/homeassistant/components/nightscout/manifest.json @@ -3,11 +3,8 @@ "name": "Nightscout", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nightscout", - "requirements": [ - "py-nightscout==1.2.2" - ], - "codeowners": [ - "@marciogranzotto" - ], - "quality_scale": "platinum" -} \ No newline at end of file + "requirements": ["py-nightscout==1.2.2"], + "codeowners": ["@marciogranzotto"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index ea2ea549cec47..183755298d65b 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -4,7 +4,6 @@ from asyncio import TimeoutError as AsyncIOTimeoutError from datetime import timedelta import logging -from typing import Callable from aiohttp import ClientError from py_nightscout import Api as NightscoutAPI @@ -13,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DATE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN @@ -27,7 +26,7 @@ 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 Glucose Sensor.""" api = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json index 2240bcec02b57..709788c581830 100644 --- a/homeassistant/components/nightscout/strings.json +++ b/homeassistant/components/nightscout/strings.json @@ -1,6 +1,5 @@ { "config": { - "flow_title": "Nightscout", "step": { "user": { "title": "Enter your Nightscout server information.", diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json index 7b480bcc0f7af..83b7066b23c3e 100644 --- a/homeassistant/components/nightscout/translations/zh-Hant.json +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index f9e3cf8573b96..bb015a059b9d4 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -3,5 +3,6 @@ "name": "Niko Home Control", "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "requirements": ["niko-home-control==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index d6fcad3ac7edd..fb5b4c757985d 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -158,7 +158,7 @@ def update(self): class NiluSensor(AirQualityEntity): """Single nilu station air sensor.""" - def __init__(self, api_data: NiluData, name: str, show_on_map: bool): + def __init__(self, api_data: NiluData, name: str, show_on_map: bool) -> None: """Initialize the sensor.""" self._api = api_data self._name = f"{name} {api_data.data.name}" diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index 1eb9464290270..bdc9220994798 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -3,5 +3,6 @@ "name": "Norwegian Institute for Air Research (NILU)", "documentation": "https://www.home-assistant.io/integrations/nilu", "requirements": ["niluclient==0.1.2"], - "codeowners": ["@hfurubotten"] + "codeowners": ["@hfurubotten"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index db78e5ce0e980..298343d2d8d25 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -3,5 +3,6 @@ "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", "requirements": ["pycarwings2==2.10"], - "codeowners": ["@filcole"] + "codeowners": ["@filcole"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nissan_leaf/services.yaml b/homeassistant/components/nissan_leaf/services.yaml index 096f4f5b8b421..901e70de414fb 100644 --- a/homeassistant/components/nissan_leaf/services.yaml +++ b/homeassistant/components/nissan_leaf/services.yaml @@ -1,20 +1,30 @@ # Describes the format for available services for nissan_leaf start_charge: + name: Start charge description: > Start the vehicle charging. It must be plugged in first! fields: vin: + name: VIN description: > The vehicle identification number (VIN) of the vehicle, 17 characters + required: true example: WBANXXXXXX1234567 + selector: + text: update: + name: Update description: > Fetch the last state of the vehicle of all your accounts, requesting an update from of the state from the car if possible. fields: vin: + name: VIN description: > The vehicle identification number (VIN) of the vehicle, 17 characters + required: true example: WBANXXXXXX1234567 + selector: + text: diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 1b049b54a07b6..9f81c0facaf70 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -3,5 +3,6 @@ "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", "requirements": ["python-nmap==0.6.1", "getmac==0.8.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nmbs/manifest.json b/homeassistant/components/nmbs/manifest.json index e9b1d1ecbf759..82723f9792433 100644 --- a/homeassistant/components/nmbs/manifest.json +++ b/homeassistant/components/nmbs/manifest.json @@ -3,5 +3,6 @@ "name": "NMBS", "documentation": "https://www.home-assistant.io/integrations/nmbs", "requirements": ["pyrail==0.0.3"], - "codeowners": ["@thibmaek"] + "codeowners": ["@thibmaek"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 32e4fd87e2917..58ad547eaecd7 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -152,7 +152,7 @@ def update(self): """Set the state equal to the next departure.""" liveboard = self._api_client.get_liveboard(self._station) - if liveboard is None or not liveboard["departures"]: + if liveboard is None or not liveboard.get("departures"): return next_departure = liveboard["departures"]["departure"][0] @@ -269,7 +269,7 @@ def update(self): self._station_from, self._station_to ) - if connections is None or not connections["connection"]: + if connections is None or not connections.get("connection"): return if int(connections["connection"][0]["departure"]["left"]) > 0: diff --git a/homeassistant/components/no_ip/manifest.json b/homeassistant/components/no_ip/manifest.json index 8294ba650721a..565ef8a78407d 100644 --- a/homeassistant/components/no_ip/manifest.json +++ b/homeassistant/components/no_ip/manifest.json @@ -2,5 +2,6 @@ "domain": "no_ip", "name": "No-IP.com", "documentation": "https://www.home-assistant.io/integrations/no_ip", - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index f0343d88c843f..8ad99c8a5c22e 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -3,5 +3,6 @@ "name": "NOAA Tides", "documentation": "https://www.home-assistant.io/integrations/noaa_tides", "requirements": ["noaa-coops==0.1.8"], - "codeowners": ["@jdelaney72"] + "codeowners": ["@jdelaney72"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 788f900ef70be..480121846e954 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -67,7 +67,7 @@ def _decorator(self): class AirSensor(AirQualityEntity): - """Representation of an Yr.no sensor.""" + """Representation of an air quality sensor.""" def __init__(self, name, coordinates, forecast, session): """Initialize the sensor.""" diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 193d96e2a189b..69b2e85808bc3 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,6 +2,7 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.8.1"], - "codeowners": [] + "requirements": ["pyMetno==0.8.3"], + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index e64ceb48a217a..41953ddfc7541 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import async_get_integration, bind_hass -from homeassistant.setup import async_prepare_setup_platform +from homeassistant.setup import async_prepare_setup_platform, async_start_setup from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml @@ -116,7 +116,6 @@ class BaseNotificationService: # 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 # Name => target @@ -289,47 +288,52 @@ async def async_setup_platform( _LOGGER.error("Unknown notification service specified") return - _LOGGER.info("Setting up %s.%s", DOMAIN, integration_name) - notify_service = None - try: - if hasattr(platform, "async_get_service"): - notify_service = await platform.async_get_service( - hass, p_config, discovery_info - ) - elif hasattr(platform, "get_service"): - notify_service = await hass.async_add_executor_job( - platform.get_service, hass, p_config, discovery_info - ) - else: - raise HomeAssistantError("Invalid notify platform.") - - if notify_service is None: - # Platforms can decide not to create a service based - # on discovery data. - if discovery_info is None: - _LOGGER.error( - "Failed to initialize notification service %s", integration_name + full_name = f"{DOMAIN}.{integration_name}" + _LOGGER.info("Setting up %s", full_name) + with async_start_setup(hass, [full_name]): + notify_service = None + try: + if hasattr(platform, "async_get_service"): + notify_service = await platform.async_get_service( + hass, p_config, discovery_info + ) + elif hasattr(platform, "get_service"): + notify_service = await hass.async_add_executor_job( + platform.get_service, hass, p_config, discovery_info ) + else: + raise HomeAssistantError("Invalid notify platform.") + + if notify_service is None: + # Platforms can decide not to create a service based + # on discovery data. + if discovery_info is None: + _LOGGER.error( + "Failed to initialize notification service %s", + integration_name, + ) + return + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error setting up platform %s", integration_name) return - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error setting up platform %s", integration_name) - return - - if discovery_info is None: - discovery_info = {} + if discovery_info is None: + discovery_info = {} - conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) - target_service_name_prefix = conf_name or integration_name - service_name = slugify(conf_name or SERVICE_NOTIFY) + conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) + target_service_name_prefix = conf_name or integration_name + service_name = slugify(conf_name or SERVICE_NOTIFY) - await notify_service.async_setup(hass, service_name, target_service_name_prefix) - await notify_service.async_register_services() + await notify_service.async_setup( + hass, service_name, target_service_name_prefix + ) + await notify_service.async_register_services() - hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append( - notify_service - ) - hass.config.components.add(f"{DOMAIN}.{integration_name}") + hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append( + notify_service + ) + hass.config.components.add(f"{DOMAIN}.{integration_name}") return True diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index f6918b6c09c6b..6bbd15c94cac3 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -7,12 +7,13 @@ notify: message: name: Message description: Message body of the notification. + required: true example: The garage door has been open for 10 minutes. selector: text: title: name: Title - description: Optional title for your notification. + description: Title for your notification. example: "Your Garage Door Friend" selector: text: @@ -21,6 +22,8 @@ notify: An array of targets to send the notification to. Optional depending on the platform. example: platform specific + selector: + object: data: name: Data description: @@ -36,10 +39,15 @@ persistent_notification: fields: message: description: Message body of the notification. + required: true example: The garage door has been open for 10 minutes. + selector: + text: title: - description: Optional title for your notification. + description: Title for your notification. example: "Your Garage Door Friend" + selector: + text: apns_register: name: Register APNS device diff --git a/homeassistant/components/notify_events/manifest.json b/homeassistant/components/notify_events/manifest.json index 9f0055e01642a..96eda381506ac 100644 --- a/homeassistant/components/notify_events/manifest.json +++ b/homeassistant/components/notify_events/manifest.json @@ -3,5 +3,6 @@ "name": "Notify.Events", "documentation": "https://www.home-assistant.io/integrations/notify_events", "codeowners": ["@matrozov", "@papajojo"], - "requirements": ["notify-events==1.0.4"] + "requirements": ["notify-events==1.0.4"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index ca0ccf08c8974..141086bb2be5d 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -14,6 +14,7 @@ config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -99,24 +100,14 @@ async def async_update(): await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a Notion config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) @@ -150,7 +141,7 @@ def __init__( system_id: str, name: str, device_class: str, - ): + ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} @@ -185,7 +176,7 @@ def extra_state_attributes(self) -> dict: return self._attrs @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" bridge = self.coordinator.data["bridges"].get(self._bridge_id, {}) sensor = self.coordinator.data["sensors"][self._sensor_id] diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 74ad724c50b65..168e35a3a97ad 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -1,6 +1,4 @@ """Support for Notion binary sensors.""" -from typing import Callable - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_DOOR, @@ -11,6 +9,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NotionEntity from .const import ( @@ -44,7 +43,7 @@ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ): """Set up Notion sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index 425357c3105c3..13386a67c027b 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -14,7 +14,6 @@ class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Notion config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize the config flow.""" diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 94d123ed17f0a..191f66ee59d7d 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/notion", "requirements": ["aionotion==1.1.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 4f034408fe2d9..2494ed2d2e840 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,10 +1,9 @@ """Support for Notion sensors.""" -from typing import Callable - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import NotionEntity @@ -14,7 +13,7 @@ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ): """Set up Notion sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] @@ -56,7 +55,7 @@ def __init__( name: str, device_class: str, unit: str, - ): + ) -> None: """Initialize the entity.""" super().__init__( coordinator, task_id, sensor_id, bridge_id, system_id, name, device_class diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json index bdc9847c14fc5..4dca09e77eaf7 100644 --- a/homeassistant/components/nsw_fuel_station/manifest.json +++ b/homeassistant/components/nsw_fuel_station/manifest.json @@ -3,5 +3,6 @@ "name": "NSW Fuel Station Price", "documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station", "requirements": ["nsw-fuel-api-client==1.0.10"], - "codeowners": ["@nickw444"] + "codeowners": ["@nickw444"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 6c8061294e9fa..9522d6c430f72 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -148,7 +148,7 @@ def get_station_name(self) -> str: class StationPriceSensor(SensorEntity): """Implementation of a sensor that reports the fuel price for a station.""" - def __init__(self, station_data: StationPriceData, fuel_type: str): + def __init__(self, station_data: StationPriceData, fuel_type: str) -> None: """Initialize the sensor.""" self._station_data = station_data self._fuel_type = fuel_type diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index 08e62e6c6a376..8df3520e24229 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -19,14 +19,14 @@ EVENT_HOMEASSISTANT_STOP, LENGTH_KILOMETERS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -66,7 +66,7 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up the NSW Rural Fire Service Feed platform.""" scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index aa8275ad0842b..debc255ec7f2b 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -3,5 +3,6 @@ "name": "NSW Rural Fire Service Incidents", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": ["aio_geojson_nsw_rfs_incidents==0.3"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 9fe4764e1afec..db50a9a70d9e5 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,5 +1,4 @@ """Support for NuHeat thermostats.""" -import asyncio from datetime import timedelta import logging @@ -25,12 +24,6 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the NuHeat component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - def _get_thermostat(api, serial_number): """Authenticate and create the thermostat object.""" api.authenticate() @@ -78,26 +71,17 @@ async def _async_update_data(): update_interval=timedelta(minutes=5), ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py index 2cbe1105e97e9..e47f3c8eb6e27 100644 --- a/homeassistant/components/nuheat/config_flow.py +++ b/homeassistant/components/nuheat/config_flow.py @@ -64,7 +64,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for NuHeat.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index 92527f5066086..d2dbb12ebc536 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -3,7 +3,13 @@ "name": "NuHeat", "documentation": "https://www.home-assistant.io/integrations/nuheat", "requirements": ["nuheat==0.3.0"], - "codeowners": ["@bdraco"], + "codeowners": [], "config_flow": true, - "dhcp": [{"hostname":"nuheat","macaddress":"002338*"}] + "dhcp": [ + { + "hostname": "nuheat", + "macaddress": "002338*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nuheat/translations/zh-Hant.json b/homeassistant/components/nuheat/translations/zh-Hant.json index d04a5b165b1ce..7987032ee8f11 100644 --- a/homeassistant/components/nuheat/translations/zh-Hant.json +++ b/homeassistant/components/nuheat/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 6aa945a52bf53..ea224612d821f 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1,28 +1,53 @@ """The nuki component.""" from datetime import timedelta +import logging -import voluptuous as vol +import async_timeout +from pynuki import NukiBridge +from pynuki.bridge import InvalidCredentialsException +from requests.exceptions import RequestException -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant import exceptions from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_HOST, CONF_PLATFORM, CONF_PORT, CONF_TOKEN +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + DATA_BRIDGE, + DATA_COORDINATOR, + DATA_LOCKS, + DATA_OPENERS, + DEFAULT_PORT, + DEFAULT_TIMEOUT, + DOMAIN, + ERROR_STATES, +) -from .const import DEFAULT_PORT, DOMAIN +_LOGGER = logging.getLogger(__name__) -PLATFORMS = ["lock"] +PLATFORMS = ["binary_sensor", "lock"] UPDATE_INTERVAL = timedelta(seconds=30) -NUKI_SCHEMA = vol.Schema( - vol.All( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_TOKEN): cv.string, - }, - ) -) + +def _get_bridge_devices(bridge): + return bridge.locks, bridge.openers + + +def _update_devices(devices): + for device in devices: + for level in (False, True): + try: + device.update(level) + except RequestException: + continue + + if device.state not in ERROR_STATES: + break async def async_setup(hass, config): @@ -35,19 +60,101 @@ async def async_setup(hass, config): continue for conf in confs: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + if CONF_PLATFORM in conf and conf[CONF_PLATFORM] == DOMAIN: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: conf[CONF_HOST], + CONF_PORT: conf.get(CONF_PORT, DEFAULT_PORT), + CONF_TOKEN: conf[CONF_TOKEN], + }, + ) ) - ) return True async def async_setup_entry(hass, entry): """Set up the Nuki entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, LOCK_DOMAIN) + + hass.data.setdefault(DOMAIN, {}) + + try: + bridge = await hass.async_add_executor_job( + NukiBridge, + entry.data[CONF_HOST], + entry.data[CONF_TOKEN], + entry.data[CONF_PORT], + True, + DEFAULT_TIMEOUT, + ) + + locks, openers = await hass.async_add_executor_job(_get_bridge_devices, bridge) + except InvalidCredentialsException as err: + raise exceptions.ConfigEntryAuthFailed from err + except RequestException as err: + raise exceptions.ConfigEntryNotReady from err + + async def async_update_data(): + """Fetch data from Nuki bridge.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + await hass.async_add_executor_job(_update_devices, locks + openers) + except InvalidCredentialsException as err: + raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err + except RequestException as err: + raise UpdateFailed(f"Error communicating with Bridge: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="nuki devices", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=UPDATE_INTERVAL, ) + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinator, + DATA_BRIDGE: bridge, + DATA_LOCKS: locks, + DATA_OPENERS: openers, + } + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass, entry): + """Unload the Nuki entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class NukiEntity(CoordinatorEntity): + """An entity using CoordinatorEntity. + + The CoordinatorEntity class provides: + should_poll + async_update + async_added_to_hass + available + + """ + + def __init__(self, coordinator, nuki_device): + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self._nuki_device = nuki_device diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py new file mode 100644 index 0000000000000..37641dbf15a98 --- /dev/null +++ b/homeassistant/components/nuki/binary_sensor.py @@ -0,0 +1,73 @@ +"""Doorsensor Support for the Nuki Lock.""" + +import logging + +from pynuki import STATE_DOORSENSOR_OPENED + +from homeassistant.components.binary_sensor import DEVICE_CLASS_DOOR, BinarySensorEntity + +from . import NukiEntity +from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Nuki lock binary sensor.""" + data = hass.data[NUKI_DOMAIN][entry.entry_id] + coordinator = data[DATA_COORDINATOR] + + entities = [] + + for lock in data[DATA_LOCKS]: + if lock.is_door_sensor_activated: + entities.extend([NukiDoorsensorEntity(coordinator, lock)]) + + async_add_entities(entities) + + +class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): + """Representation of a Nuki Lock Doorsensor.""" + + @property + def name(self): + """Return the name of the lock.""" + return self._nuki_device.name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._nuki_device.nuki_id}_doorsensor" + + @property + def extra_state_attributes(self): + """Return the device specific state attributes.""" + data = { + ATTR_NUKI_ID: self._nuki_device.nuki_id, + } + return data + + @property + def available(self): + """Return true if door sensor is present and activated.""" + return super().available and self._nuki_device.is_door_sensor_activated + + @property + def door_sensor_state(self): + """Return the state of the door sensor.""" + return self._nuki_device.door_sensor_state + + @property + def door_sensor_state_name(self): + """Return the state name of the door sensor.""" + return self._nuki_device.door_sensor_state_name + + @property + def is_on(self): + """Return true if the door is open.""" + return self.door_sensor_state == STATE_DOORSENSOR_OPENED + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_DOOR diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 7d7a846aa80d6..7a98ad2f00d03 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -22,6 +22,8 @@ } ) +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): str}) + async def validate_input(hass, data): """Validate the user input allows us to connect. @@ -54,6 +56,7 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Nuki config flow.""" self.discovery_schema = {} + self._data = {} async def async_step_import(self, user_input=None): """Handle a flow initiated by import.""" @@ -79,6 +82,50 @@ async def async_step_dhcp(self, discovery_info: dict): return await self.async_step_validate() + async def async_step_reauth(self, data): + """Perform reauth upon an API authentication error.""" + self._data = data + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Dialog that inform the user that reauth is required.""" + errors = {} + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", data_schema=REAUTH_SCHEMA + ) + + conf = { + CONF_HOST: self._data[CONF_HOST], + CONF_PORT: self._data[CONF_PORT], + CONF_TOKEN: user_input[CONF_TOKEN], + } + + try: + info = await validate_input(self.hass, conf) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + existing_entry = await self.async_set_unique_id(info["ids"]["hardwareId"]) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=conf) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="reauth_confirm", data_schema=REAUTH_SCHEMA, errors=errors + ) + async def async_step_validate(self, user_input=None): """Handle init step of a flow.""" @@ -102,7 +149,6 @@ async def async_step_validate(self, user_input=None): ) data_schema = self.discovery_schema or USER_SCHEMA - return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) diff --git a/homeassistant/components/nuki/const.py b/homeassistant/components/nuki/const.py index 07ef49ebd8872..da12a3a074ddb 100644 --- a/homeassistant/components/nuki/const.py +++ b/homeassistant/components/nuki/const.py @@ -1,6 +1,19 @@ """Constants for Nuki.""" DOMAIN = "nuki" +# Attributes +ATTR_BATTERY_CRITICAL = "battery_critical" +ATTR_NUKI_ID = "nuki_id" +ATTR_UNLATCH = "unlatch" + +# Data +DATA_BRIDGE = "nuki_bridge_data" +DATA_LOCKS = "nuki_locks_data" +DATA_OPENERS = "nuki_openers_data" +DATA_COORDINATOR = "nuki_coordinator" + # Defaults DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 20 + +ERROR_STATES = (0, 254, 255) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 360153d14feab..ca6e72bde8f86 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,31 +1,28 @@ """Nuki.io lock platform.""" from abc import ABC, abstractmethod -from datetime import timedelta import logging -from pynuki import NukiBridge -from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.helpers import config_validation as cv, entity_platform -from .const import DEFAULT_PORT, DEFAULT_TIMEOUT +from . import NukiEntity +from .const import ( + ATTR_BATTERY_CRITICAL, + ATTR_NUKI_ID, + ATTR_UNLATCH, + DATA_COORDINATOR, + DATA_LOCKS, + DATA_OPENERS, + DEFAULT_PORT, + DOMAIN as NUKI_DOMAIN, + ERROR_STATES, +) _LOGGER = logging.getLogger(__name__) -ATTR_BATTERY_CRITICAL = "battery_critical" -ATTR_NUKI_ID = "nuki_id" -ATTR_UNLATCH = "unlatch" - -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=5) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) - -NUKI_DATA = "nuki" - -ERROR_STATES = (0, 254, 255) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -42,30 +39,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Nuki lock platform.""" - config = config_entry.data - - def get_entities(): - bridge = NukiBridge( - config[CONF_HOST], - config[CONF_TOKEN], - config[CONF_PORT], - True, - DEFAULT_TIMEOUT, - ) - - entities = [NukiLockEntity(lock) for lock in bridge.locks] - entities.extend([NukiOpenerEntity(opener) for opener in bridge.openers]) - return entities - - entities = await hass.async_add_executor_job(get_entities) + data = hass.data[NUKI_DOMAIN][entry.entry_id] + coordinator = data[DATA_COORDINATOR] + entities = [NukiLockEntity(coordinator, lock) for lock in data[DATA_LOCKS]] + entities.extend( + [NukiOpenerEntity(coordinator, opener) for opener in data[DATA_OPENERS]] + ) async_add_entities(entities) - platform = entity_platform.current_platform.get() - assert platform is not None - + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( "lock_n_go", { @@ -75,14 +60,9 @@ def get_entities(): ) -class NukiDeviceEntity(LockEntity, ABC): +class NukiDeviceEntity(NukiEntity, LockEntity, ABC): """Representation of a Nuki device.""" - def __init__(self, nuki_device): - """Initialize the lock.""" - self._nuki_device = nuki_device - self._available = nuki_device.state not in ERROR_STATES - @property def name(self): """Return the name of the lock.""" @@ -115,22 +95,7 @@ def supported_features(self): @property def available(self) -> bool: """Return True if entity is available.""" - return self._available - - def update(self): - """Update the nuki lock properties.""" - for level in (False, True): - try: - self._nuki_device.update(aggressive=level) - except RequestException: - _LOGGER.warning("Network issues detect with %s", self.name) - self._available = False - continue - - # If in error state, we force an update and repoll data - self._available = self._nuki_device.state not in ERROR_STATES - if self._available: - break + return super().available and self._nuki_device.state not in ERROR_STATES @abstractmethod def lock(self, **kwargs): diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 7fb9a134c4c55..4cc2599900d04 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -1,9 +1,14 @@ { - "domain": "nuki", - "name": "Nuki", - "documentation": "https://www.home-assistant.io/integrations/nuki", - "requirements": ["pynuki==1.3.8"], - "codeowners": ["@pschmitt", "@pvizeli", "@pree"], - "config_flow": true, - "dhcp": [{ "hostname": "nuki_bridge_*" }] -} \ No newline at end of file + "domain": "nuki", + "name": "Nuki", + "documentation": "https://www.home-assistant.io/integrations/nuki", + "requirements": ["pynuki==1.4.1"], + "codeowners": ["@pschmitt", "@pvizeli", "@pree"], + "config_flow": true, + "dhcp": [ + { + "hostname": "nuki_bridge_*" + } + ], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml index 9e3be794cb7c1..85e0e67ea50d7 100644 --- a/homeassistant/components/nuki/services.yaml +++ b/homeassistant/components/nuki/services.yaml @@ -1,9 +1,15 @@ lock_n_go: + name: Lock 'n' go description: "Nuki Lock 'n' Go" + target: + entity: + integration: nuki + domain: lock fields: - entity_id: - description: Entity id of the Nuki lock. - example: "lock.front_door" unlatch: + name: unlatch description: Whether to unlatch the lock. example: false + default: false + selector: + boolean: diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 9e1e4f5e5ab24..3f6de25122a87 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -7,12 +7,22 @@ "port": "[%key:common::config_flow::data::port%]", "token": "[%key:common::config_flow::data::access_token%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Nuki integration needs to re-authenticate with your bridge.", + "data": { + "token": "[%key:common::config_flow::data::access_token%]" + } } }, "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": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ca.json b/homeassistant/components/nuki/translations/ca.json index a08308e78977f..e7b149349db98 100644 --- a/homeassistant/components/nuki/translations/ca.json +++ b/homeassistant/components/nuki/translations/ca.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token d'acc\u00e9s" + }, + "description": "La integraci\u00f3 Nuki ha de tornar a autenticar-se amb la passarel\u00b7la d'enlla\u00e7.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/nuki/translations/cs.json b/homeassistant/components/nuki/translations/cs.json index 349c92805cf8f..52c1e3e9a8e17 100644 --- a/homeassistant/components/nuki/translations/cs.json +++ b/homeassistant/components/nuki/translations/cs.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "token": "P\u0159\u00edstupov\u00fd token" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/nuki/translations/de.json b/homeassistant/components/nuki/translations/de.json index 30d7e6865cdc8..ae1322d7641be 100644 --- a/homeassistant/components/nuki/translations/de.json +++ b/homeassistant/components/nuki/translations/de.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "token": "Zugangstoken" + }, + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json index 135e8de2b2f6d..99c43859eb0ae 100644 --- a/homeassistant/components/nuki/translations/en.json +++ b/homeassistant/components/nuki/translations/en.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Re-authentication was successful" + }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "token": "Access Token" + }, + "description": "The Nuki integration needs to re-authenticate with your bridge.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/es.json b/homeassistant/components/nuki/translations/es.json index 8def4e2780d2e..33fe3f462df3f 100644 --- a/homeassistant/components/nuki/translations/es.json +++ b/homeassistant/components/nuki/translations/es.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token de acceso" + }, + "description": "La integraci\u00f3n de Nuki debe volver a autenticarse con tu bridge.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/et.json b/homeassistant/components/nuki/translations/et.json index 750afff003c24..e587458bbf0a8 100644 --- a/homeassistant/components/nuki/translations/et.json +++ b/homeassistant/components/nuki/translations/et.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Vigane autentimine", "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "token": "Juurdep\u00e4\u00e4sut\u00f5end" + }, + "description": "Nuki sidumise peab sillaga uuesti autentima.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/fr.json b/homeassistant/components/nuki/translations/fr.json index 035c07325766c..248acf70133c1 100644 --- a/homeassistant/components/nuki/translations/fr.json +++ b/homeassistant/components/nuki/translations/fr.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, "error": { "cannot_connect": "\u00c9chec de la connexion ", "invalid_auth": "Authentification invalide ", "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "token": "Jeton d'acc\u00e8s" + }, + "description": "L'int\u00e9gration Nuki doit s'authentifier de nouveau avec votre pont.", + "title": "R\u00e9authentifier l'int\u00e9gration" + }, "user": { "data": { "host": "Hote", diff --git a/homeassistant/components/nuki/translations/id.json b/homeassistant/components/nuki/translations/id.json index d9e5e1de2c31a..1294b18b460ca 100644 --- a/homeassistant/components/nuki/translations/id.json +++ b/homeassistant/components/nuki/translations/id.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Autentikasi ulang berhasil" + }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token Akses" + }, + "description": "Integrasi Nuki perlu mengautentikasi ulang dengan bridge Anda.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/it.json b/homeassistant/components/nuki/translations/it.json index 899093e1f4182..eaf0a8e52e4e2 100644 --- a/homeassistant/components/nuki/translations/it.json +++ b/homeassistant/components/nuki/translations/it.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token di accesso" + }, + "description": "L'integrazione Nuki deve essere nuovamente autenticata con il tuo bridge.", + "title": "Autenticare nuovamente l'integrazione" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/ko.json b/homeassistant/components/nuki/translations/ko.json index 68f43847d6c78..3015596e7d467 100644 --- a/homeassistant/components/nuki/translations/ko.json +++ b/homeassistant/components/nuki/translations/ko.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_confirm": { + "data": { + "token": "\uc561\uc138\uc2a4 \ud1a0\ud070" + }, + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", diff --git a/homeassistant/components/nuki/translations/nl.json b/homeassistant/components/nuki/translations/nl.json index 4e220dbe78d79..4157d21ffd3b5 100644 --- a/homeassistant/components/nuki/translations/nl.json +++ b/homeassistant/components/nuki/translations/nl.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Herauthenticatie was succesvol" + }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "token": "Toegangstoken" + }, + "description": "De Nuki integratie moet opnieuw authenticeren met uw bridge.", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/nuki/translations/no.json b/homeassistant/components/nuki/translations/no.json index 8cdbac230d735..1ae4eb0362442 100644 --- a/homeassistant/components/nuki/translations/no.json +++ b/homeassistant/components/nuki/translations/no.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "token": "Tilgangstoken" + }, + "description": "Nuki-integrasjonen m\u00e5 godkjennes p\u00e5 nytt med broen din.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/nuki/translations/pl.json b/homeassistant/components/nuki/translations/pl.json index 77a7c31ee34e0..c51a431cfe7e7 100644 --- a/homeassistant/components/nuki/translations/pl.json +++ b/homeassistant/components/nuki/translations/pl.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "token": "Token dost\u0119pu" + }, + "description": "Integracja Nuki wymaga ponownego uwierzytelnienia z Twoim mostkiem.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/nuki/translations/ro.json b/homeassistant/components/nuki/translations/ro.json new file mode 100644 index 0000000000000..0b5f3c35ea709 --- /dev/null +++ b/homeassistant/components/nuki/translations/ro.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autentificare efectuata cu succes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ru.json b/homeassistant/components/nuki/translations/ru.json index a7fe1c61f5b62..a39f1429e140c 100644 --- a/homeassistant/components/nuki/translations/ru.json +++ b/homeassistant/components/nuki/translations/ru.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Nuki.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/nuki/translations/zh-Hant.json b/homeassistant/components/nuki/translations/zh-Hant.json index 4bf21552952d2..fb486faced1a6 100644 --- a/homeassistant/components/nuki/translations/zh-Hant.json +++ b/homeassistant/components/nuki/translations/zh-Hant.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "token": "\u5b58\u53d6\u6b0a\u6756" + }, + "description": "Nuki \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49 Bridge\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json index 6138f401ec264..a65c4998554c6 100644 --- a/homeassistant/components/numato/manifest.json +++ b/homeassistant/components/numato/manifest.json @@ -3,5 +3,6 @@ "name": "Numato USB GPIO Expander", "documentation": "https://www.home-assistant.io/integrations/numato", "requirements": ["numato-gpio==0.10.0"], - "codeowners": ["@clssn"] + "codeowners": ["@clssn"], + "iot_class": "local_push" } diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index e61398f6582f3..046895ac29c57 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -9,13 +9,14 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_MAX, @@ -38,7 +39,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -54,12 +55,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" return await hass.data[DOMAIN].async_setup_entry(entry) # type: ignore -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.data[DOMAIN].async_unload_entry(entry) # type: ignore diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py index 4364dffe1e853..dbf4af1f860e7 100644 --- a/homeassistant/components/number/reproduce_state.py +++ b/homeassistant/components/number/reproduce_state.py @@ -2,12 +2,12 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE @@ -15,7 +15,7 @@ async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -49,7 +49,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/number/services.yaml b/homeassistant/components/number/services.yaml index a684fef7d5ddd..2014c4c5221da 100644 --- a/homeassistant/components/number/services.yaml +++ b/homeassistant/components/number/services.yaml @@ -4,6 +4,8 @@ set_value: name: Set description: Set the value of a Number entity. target: + entity: + domain: number fields: value: name: Value diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index be86ca5951c31..77458b2cfb739 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -1,5 +1,4 @@ """The nut component.""" -import asyncio from datetime import timedelta import logging @@ -37,13 +36,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Network UPS Tools (NUT) component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Network UPS Tools (NUT) from a config entry.""" @@ -90,6 +82,7 @@ async def async_update_data(): if unique_id is None: unique_id = entry.entry_id + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, PYNUT_DATA: data, @@ -101,10 +94,7 @@ async def async_update_data(): UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -175,14 +165,7 @@ def find_resources_in_config_entry(config_entry): 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 07e135b8ebdf6..0b5ad8bbc1ff3 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -116,7 +116,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Network UPS Tools (NUT).""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the nut config flow.""" @@ -228,7 +227,7 @@ def async_get_options_flow(config_entry): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for nut.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 693b225c6dda1..388858b93f0be 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pynut2==2.1.2"], "codeowners": ["@bdraco"], "config_flow": true, - "zeroconf": ["_nut._tcp.local."] + "zeroconf": ["_nut._tcp.local."], + "iot_class": "local_polling" } diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 2e3826935fe20..1eb67e45aa56a 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -98,11 +98,14 @@ def __init__( self._firmware = firmware self._model = model self._device_name = name - self._name = f"{name} {SENSOR_TYPES[sensor_type][SENSOR_NAME]}" - self._unit = SENSOR_TYPES[sensor_type][SENSOR_UNIT] self._data = data self._unique_id = unique_id + self._attr_device_class = SENSOR_TYPES[self._type][SENSOR_DEVICE_CLASS] + self._attr_icon = SENSOR_TYPES[self._type][SENSOR_ICON] + self._attr_name = f"{name} {SENSOR_TYPES[sensor_type][SENSOR_NAME]}" + self._attr_unit_of_measurement = SENSOR_TYPES[sensor_type][SENSOR_UNIT] + @property def device_info(self): """Device info for the ups.""" @@ -127,25 +130,6 @@ def unique_id(self): return None return f"{self._unique_id}_{self._type}" - @property - def name(self): - """Return the name of the UPS sensor.""" - return self._name - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - if SENSOR_TYPES[self._type][SENSOR_DEVICE_CLASS]: - # The UI will assign an icon - # if it has a class - return None - return SENSOR_TYPES[self._type][SENSOR_ICON] - - @property - def device_class(self): - """Device class of the sensor.""" - return SENSOR_TYPES[self._type][SENSOR_DEVICE_CLASS] - @property def state(self): """Return entity state from ups.""" @@ -155,11 +139,6 @@ def state(self): return _format_display_state(self._data.status) return self._data.status.get(self._type) - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit - @property def extra_state_attributes(self): """Return the sensor attributes.""" diff --git a/homeassistant/components/nut/translations/cs.json b/homeassistant/components/nut/translations/cs.json index d5cf361ba03d1..37d73391ecf50 100644 --- a/homeassistant/components/nut/translations/cs.json +++ b/homeassistant/components/nut/translations/cs.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/es.json b/homeassistant/components/nut/translations/es.json index c76fc0da7983a..234f34082e164 100644 --- a/homeassistant/components/nut/translations/es.json +++ b/homeassistant/components/nut/translations/es.json @@ -33,6 +33,10 @@ } }, "options": { + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/sv.json b/homeassistant/components/nut/translations/sv.json index 70dccdca51ecc..45832197f681c 100644 --- a/homeassistant/components/nut/translations/sv.json +++ b/homeassistant/components/nut/translations/sv.json @@ -31,6 +31,10 @@ } }, "options": { + "error": { + "cannot_connect": "Kunde inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json index 822d2e785f22c..5f48541792d88 100644 --- a/homeassistant/components/nut/translations/zh-Hant.json +++ b/homeassistant/components/nut/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 569a8adf83b70..474657392509d 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -1,10 +1,10 @@ """The National Weather Service integration.""" from __future__ import annotations -import asyncio +from collections.abc import Awaitable import datetime import logging -from typing import Awaitable, Callable +from typing import Callable from pynws import SimpleNWS @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["weather"] +PLATFORMS = ["sensor", "weather"] DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10) FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1) @@ -40,11 +40,6 @@ def base_unique_id(latitude, longitude): return f"{latitude}_{longitude}" -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the National Weather Service (NWS) component.""" - return True - - class NwsDataUpdateCoordinator(DataUpdateCoordinator): """ NWS data update coordinator. @@ -62,7 +57,7 @@ def __init__( failed_update_interval: datetime.timedelta, update_method: Callable[[], Awaitable] | None = None, request_refresh_debouncer: debounce.Debouncer | None = None, - ): + ) -> None: """Initialize NWS coordinator.""" super().__init__( hass, @@ -159,23 +154,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator_forecast.async_refresh() await coordinator_forecast_hourly.async_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) if len(hass.data[DOMAIN]) == 0: diff --git a/homeassistant/components/nws/config_flow.py b/homeassistant/components/nws/config_flow.py index cfe43f3a52894..517062e23d892 100644 --- a/homeassistant/components/nws/config_flow.py +++ b/homeassistant/components/nws/config_flow.py @@ -43,7 +43,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for National Weather Service (NWS).""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index f055bab020346..f82a70ea4e0ff 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -1,4 +1,6 @@ """Constants for National Weather Service Integration.""" +from datetime import timedelta + from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, @@ -14,6 +16,21 @@ ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_INHG, + PRESSURE_PA, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, +) DOMAIN = "nws" @@ -23,6 +40,11 @@ ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" ATTR_FORECAST_DAYTIME = "daytime" +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_UNIT = "unit" +ATTR_UNIT_CONVERT = "unit_convert" +ATTR_UNIT_CONVERT_METHOD = "unit_convert_method" CONDITION_CLASSES = { ATTR_CONDITION_EXCEPTIONAL: [ @@ -75,3 +97,86 @@ COORDINATOR_OBSERVATION = "coordinator_observation" COORDINATOR_FORECAST = "coordinator_forecast" COORDINATOR_FORECAST_HOURLY = "coordinator_forecast_hourly" + +OBSERVATION_VALID_TIME = timedelta(minutes=20) +FORECAST_VALID_TIME = timedelta(minutes=45) + +SENSOR_TYPES = { + "dewpoint": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Dew Point", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "temperature": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "windChill": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Wind Chill", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "heatIndex": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: "Heat Index", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_UNIT_CONVERT: TEMP_CELSIUS, + }, + "relativeHumidity": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_LABEL: "Relative Humidity", + ATTR_UNIT: PERCENTAGE, + ATTR_UNIT_CONVERT: PERCENTAGE, + }, + "windSpeed": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Speed", + ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, + }, + "windGust": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Gust", + ATTR_UNIT: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_CONVERT: SPEED_MILES_PER_HOUR, + }, + "windDirection": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:compass-rose", + ATTR_LABEL: "Wind Direction", + ATTR_UNIT: DEGREE, + ATTR_UNIT_CONVERT: DEGREE, + }, + "barometricPressure": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: "Barometric Pressure", + ATTR_UNIT: PRESSURE_PA, + ATTR_UNIT_CONVERT: PRESSURE_INHG, + }, + "seaLevelPressure": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: "Sea Level Pressure", + ATTR_UNIT: PRESSURE_PA, + ATTR_UNIT_CONVERT: PRESSURE_INHG, + }, + "visibility": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:eye", + ATTR_LABEL: "Visibility", + ATTR_UNIT: LENGTH_METERS, + ATTR_UNIT_CONVERT: LENGTH_MILES, + }, +} diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index ef0a35b846a68..d1e7158ab20c9 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@MatthewFlamm"], "requirements": ["pynws==1.3.0"], "quality_scale": "platinum", - "config_flow": true + "config_flow": true, + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py new file mode 100644 index 0000000000000..bff5cdca58954 --- /dev/null +++ b/homeassistant/components/nws/sensor.py @@ -0,0 +1,156 @@ +"""Sensors for National Weather Service (NWS).""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + CONF_LATITUDE, + CONF_LONGITUDE, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_INHG, + PRESSURE_PA, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.dt import utcnow +from homeassistant.util.pressure import convert as convert_pressure + +from . import base_unique_id +from .const import ( + ATTR_ICON, + ATTR_LABEL, + ATTR_UNIT, + ATTR_UNIT_CONVERT, + ATTRIBUTION, + CONF_STATION, + COORDINATOR_OBSERVATION, + DOMAIN, + NWS_DATA, + OBSERVATION_VALID_TIME, + SENSOR_TYPES, +) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the NWS weather platform.""" + hass_data = hass.data[DOMAIN][entry.entry_id] + station = entry.data[CONF_STATION] + + entities = [] + for sensor_type, sensor_data in SENSOR_TYPES.items(): + if hass.config.units.is_metric: + unit = sensor_data[ATTR_UNIT] + else: + unit = sensor_data[ATTR_UNIT_CONVERT] + entities.append( + NWSSensor( + entry.data, + hass_data, + sensor_type, + station, + sensor_data[ATTR_LABEL], + sensor_data[ATTR_ICON], + sensor_data[ATTR_DEVICE_CLASS], + unit, + ), + ) + + async_add_entities(entities, False) + + +class NWSSensor(CoordinatorEntity, SensorEntity): + """An NWS Sensor Entity.""" + + def __init__( + self, + entry_data, + hass_data, + sensor_type, + station, + label, + icon, + device_class, + unit, + ): + """Initialise the platform with a data instance.""" + super().__init__(hass_data[COORDINATOR_OBSERVATION]) + self._nws = hass_data[NWS_DATA] + self._latitude = entry_data[CONF_LATITUDE] + self._longitude = entry_data[CONF_LONGITUDE] + self._type = sensor_type + self._station = station + self._label = label + self._icon = icon + self._device_class = device_class + self._unit = unit + + @property + def state(self): + """Return the state.""" + value = self._nws.observation.get(self._type) + if value is None: + return None + if self._unit == SPEED_MILES_PER_HOUR: + return round(convert_distance(value, LENGTH_KILOMETERS, LENGTH_MILES)) + if self._unit == LENGTH_MILES: + return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES)) + if self._unit == PRESSURE_INHG: + return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2) + if self._unit == TEMP_CELSIUS: + return round(value, 1) + if self._unit == PERCENTAGE: + return round(value) + return value + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def device_state_attributes(self): + """Return the attribution.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def name(self): + """Return the name of the station.""" + return f"{self._station} {self._label}" + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{base_unique_id(self._latitude, self._longitude)}_{self._type}" + + @property + def available(self): + """Return if state is available.""" + if self.coordinator.last_update_success_time: + last_success_time = ( + utcnow() - self.coordinator.last_update_success_time + < OBSERVATION_VALID_TIME + ) + else: + last_success_time = False + return self.coordinator.last_update_success or last_success_time + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 9f4e69bdb8c06..a8f3e55c2700c 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,6 +1,4 @@ """Support for NWS weather service.""" -from datetime import timedelta - from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, @@ -24,8 +22,8 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from homeassistant.util.distance import convert as convert_distance from homeassistant.util.dt import utcnow from homeassistant.util.pressure import convert as convert_pressure @@ -42,15 +40,14 @@ COORDINATOR_OBSERVATION, DAYNIGHT, DOMAIN, + FORECAST_VALID_TIME, HOURLY, NWS_DATA, + OBSERVATION_VALID_TIME, ) PARALLEL_UPDATES = 0 -OBSERVATION_VALID_TIME = timedelta(minutes=20) -FORECAST_VALID_TIME = timedelta(minutes=45) - def convert_condition(time, weather): """ @@ -81,7 +78,7 @@ def convert_condition(time, weather): async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigType, async_add_entities + hass: HomeAssistant, entry: ConfigType, async_add_entities ) -> None: """Set up the NWS weather platform.""" hass_data = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 6366831b598c1..12f47de70601b 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -7,7 +7,9 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, +) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -35,7 +37,7 @@ SERVICE_UNBYPASS_ZONE = "unbypass_zone" ATTR_ZONE = "zone" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -65,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity = NX584Alarm(name, alarm_client, url) async_add_entities([entity]) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_BYPASS_ZONE, diff --git a/homeassistant/components/nx584/manifest.json b/homeassistant/components/nx584/manifest.json index 57676870ce765..2aa3df8d167f2 100644 --- a/homeassistant/components/nx584/manifest.json +++ b/homeassistant/components/nx584/manifest.json @@ -3,5 +3,6 @@ "name": "NX584", "documentation": "https://www.home-assistant.io/integrations/nx584", "requirements": ["pynx584==0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/nx584/services.yaml b/homeassistant/components/nx584/services.yaml index 13f5da8db2505..25ef4c20702c5 100644 --- a/homeassistant/components/nx584/services.yaml +++ b/homeassistant/components/nx584/services.yaml @@ -1,21 +1,37 @@ # Describes the format for available nx584 services bypass_zone: + name: Bypass zone description: Bypass a zone. + target: + entity: + integration: nx584 + domain: alarm_control_panel fields: - entity_id: - description: Name of the alarm control panel which state has to be updated. - example: "alarm_control_panel.downstairs" zone: + name: Zone description: The number of the zone to be bypassed. + required: true example: "1" + selector: + number: + min: 1 + max: 255 unbypass_zone: + name: Un-bypass zone description: Un-Bypass a zone. + target: + entity: + integration: nx584 + domain: alarm_control_panel fields: - entity_id: - description: Name of the alarm control panel which state has to be updated. - example: "alarm_control_panel.downstairs" zone: + name: Zone description: The number of the zone to be un-bypassed. + required: true example: "1" + selector: + number: + min: 1 + max: 255 diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 48abe597f5aeb..71f885ce4919e 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -1,6 +1,4 @@ """The NZBGet integration.""" -import asyncio - import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -13,8 +11,8 @@ CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -36,21 +34,24 @@ PLATFORMS = ["sensor", "switch"] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -59,7 +60,7 @@ ) -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the NZBGet integration.""" hass.data.setdefault(DOMAIN, {}) @@ -78,7 +79,7 @@ async def async_setup(hass: HomeAssistantType, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NZBGet from a config entry.""" if not entry.options: options = { @@ -103,26 +104,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool DATA_UNDO_UPDATE_LISTENER: undo_listener, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) _async_register_services(hass, coordinator) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() @@ -132,7 +123,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo def _async_register_services( - hass: HomeAssistantType, + hass: HomeAssistant, coordinator: NZBGetDataUpdateCoordinator, ) -> None: """Register integration-level services.""" @@ -156,7 +147,7 @@ def set_speed(call) -> None: ) -async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index a352c4df6ed47..71419dae6411d 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol -from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -17,8 +17,9 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import callback -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import ConfigType from .const import ( DEFAULT_NAME, @@ -33,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) -def validate_input(hass: HomeAssistantType, data: dict) -> dict[str, Any]: +def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -56,7 +57,6 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for NZBGet.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL @staticmethod @callback @@ -66,16 +66,16 @@ def async_get_options_flow(config_entry): async def async_step_import( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initiated by configuration file.""" if CONF_SCAN_INTERVAL in user_input: - user_input[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL].seconds + user_input[CONF_SCAN_INTERVAL] = user_input[ + CONF_SCAN_INTERVAL + ].total_seconds() return await self.async_step_user(user_input) - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index 9a76d802bdd3a..5851bb21b41fc 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -14,7 +14,7 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -25,7 +25,7 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching NZBGet data.""" - def __init__(self, hass: HomeAssistantType, *, config: dict, options: dict): + def __init__(self, hass: HomeAssistant, *, config: dict, options: dict) -> None: """Initialize global NZBGet data updater.""" self.nzbget = NZBGetAPI( config[CONF_HOST], diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 7c5e9cf5e8d4c..951d5237736be 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nzbget", "requirements": ["pynzbgetapi==0.2.0"], "codeowners": ["@chriscla"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 54a88c89f5311..97bced9e9c2c7 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -3,7 +3,6 @@ from datetime import timedelta import logging -from typing import Callable from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -13,8 +12,8 @@ DATA_RATE_MEGABYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from . import NZBGetEntity @@ -42,9 +41,9 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up NZBGet sensor based on a config entry.""" coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ @@ -78,7 +77,7 @@ def __init__( sensor_type: str, sensor_name: str, unit_of_measurement: str | None = None, - ): + ) -> None: """Initialize a new NZBGet sensor.""" self._sensor_type = sensor_type self._unique_id = f"{entry_id}_{sensor_type}" diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml index 88a6267860e5d..290b3761ab858 100644 --- a/homeassistant/components/nzbget/services.yaml +++ b/homeassistant/components/nzbget/services.yaml @@ -1,14 +1,24 @@ # Describes the format for available nzbget services pause: + name: Pause description: Pause download queue. resume: + name: Resume description: Resume download queue. set_speed: + name: Set speed description: Set download speed limit fields: speed: - description: Speed limit in kB/s. 0 is unlimited. + name: Speed + description: Speed limit. 0 is unlimited. example: 1000 + default: 1000 + selector: + number: + min: 0 + max: 1000000 + unit_of_measurement: 'kB/s' diff --git a/homeassistant/components/nzbget/strings.json b/homeassistant/components/nzbget/strings.json index 96049ea936986..fc7d8508a1225 100644 --- a/homeassistant/components/nzbget/strings.json +++ b/homeassistant/components/nzbget/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Connect to NZBGet", diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index 4f0eae17c23dc..4e4cca34aa868 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -1,13 +1,11 @@ """Support for NZBGet switches.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NZBGetEntity from .const import DATA_COORDINATOR, DOMAIN @@ -15,9 +13,9 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up NZBGet sensor based on a config entry.""" coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ @@ -43,7 +41,7 @@ def __init__( coordinator: NZBGetDataUpdateCoordinator, entry_id: str, entry_name: str, - ): + ) -> None: """Initialize a new NZBGet switch.""" self._unique_id = f"{entry_id}_download" diff --git a/homeassistant/components/nzbget/translations/ca.json b/homeassistant/components/nzbget/translations/ca.json index 4bd1b53b762ee..396a63da66c09 100644 --- a/homeassistant/components/nzbget/translations/ca.json +++ b/homeassistant/components/nzbget/translations/ca.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/en.json b/homeassistant/components/nzbget/translations/en.json index b46c7b1d799f5..76b42126ff2a6 100644 --- a/homeassistant/components/nzbget/translations/en.json +++ b/homeassistant/components/nzbget/translations/en.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/et.json b/homeassistant/components/nzbget/translations/et.json index ee83b59928da5..719ec008119db 100644 --- a/homeassistant/components/nzbget/translations/et.json +++ b/homeassistant/components/nzbget/translations/et.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/it.json b/homeassistant/components/nzbget/translations/it.json index 7eaf5c78e08a9..17d7d93a6d681 100644 --- a/homeassistant/components/nzbget/translations/it.json +++ b/homeassistant/components/nzbget/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json index 89d58d14292ae..5f98d7435ec28 100644 --- a/homeassistant/components/nzbget/translations/nl.json +++ b/homeassistant/components/nzbget/translations/nl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Kon niet verbinden" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/no.json b/homeassistant/components/nzbget/translations/no.json index cb9fce35897d7..170d99d585317 100644 --- a/homeassistant/components/nzbget/translations/no.json +++ b/homeassistant/components/nzbget/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/nzbget/translations/pl.json b/homeassistant/components/nzbget/translations/pl.json index 9ae5b46b68079..624bccef15432 100644 --- a/homeassistant/components/nzbget/translations/pl.json +++ b/homeassistant/components/nzbget/translations/pl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/ru.json b/homeassistant/components/nzbget/translations/ru.json index 4c5d73795270d..ea48445df343b 100644 --- a/homeassistant/components/nzbget/translations/ru.json +++ b/homeassistant/components/nzbget/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": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/zh-Hant.json b/homeassistant/components/nzbget/translations/zh-Hant.json index 26fd5f3411728..28edec03d67c6 100644 --- a/homeassistant/components/nzbget/translations/zh-Hant.json +++ b/homeassistant/components/nzbget/translations/zh-Hant.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "NZBGet\uff1a{name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/oasa_telematics/manifest.json b/homeassistant/components/oasa_telematics/manifest.json index 84f5e78fec26c..a1d672ba59518 100644 --- a/homeassistant/components/oasa_telematics/manifest.json +++ b/homeassistant/components/oasa_telematics/manifest.json @@ -3,5 +3,6 @@ "name": "OASA Telematics", "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", "requirements": ["oasatelematics==0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index 78123cc07f507..05121c81ac73c 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,5 +3,6 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/integrations/obihai", "requirements": ["pyobihai==1.3.1"], - "codeowners": ["@dshokouhi"] + "codeowners": ["@dshokouhi"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 28e09cc7be957..85436f9617618 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -3,5 +3,6 @@ "name": "OctoPrint", "documentation": "https://www.home-assistant.io/integrations/octoprint", "after_dependencies": ["discovery"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/oem/manifest.json b/homeassistant/components/oem/manifest.json index 7ebacb9fa4e2e..29c2b1e7fa465 100644 --- a/homeassistant/components/oem/manifest.json +++ b/homeassistant/components/oem/manifest.json @@ -3,5 +3,6 @@ "name": "OpenEnergyMonitor WiFi Thermostat", "documentation": "https://www.home-assistant.io/integrations/oem", "requirements": ["oemthermostat==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ohmconnect/manifest.json b/homeassistant/components/ohmconnect/manifest.json index 3eb0d4758af19..08eaba422bb59 100644 --- a/homeassistant/components/ohmconnect/manifest.json +++ b/homeassistant/components/ohmconnect/manifest.json @@ -2,6 +2,7 @@ "domain": "ohmconnect", "name": "OhmConnect", "documentation": "https://www.home-assistant.io/integrations/ohmconnect", - "requirements": ["defusedxml==0.6.0"], - "codeowners": ["@robbiet480"] + "requirements": ["defusedxml==0.7.1"], + "codeowners": ["@robbiet480"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json index f61555495c325..2c9e40d830f86 100644 --- a/homeassistant/components/ombi/manifest.json +++ b/homeassistant/components/ombi/manifest.json @@ -3,5 +3,6 @@ "name": "Ombi", "documentation": "https://www.home-assistant.io/integrations/ombi/", "codeowners": ["@larssont"], - "requirements": ["pyombi==0.1.10"] + "requirements": ["pyombi==0.1.10"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ombi/services.yaml b/homeassistant/components/ombi/services.yaml index 6c7f5ced4893c..c6f154d073e0b 100644 --- a/homeassistant/components/ombi/services.yaml +++ b/homeassistant/components/ombi/services.yaml @@ -1,24 +1,47 @@ # Ombi services.yaml entries submit_movie_request: + name: Sumbit movie request description: Searches for a movie and requests the first result. fields: name: + name: Name description: Search parameter + required: true example: "beverly hills cop" + selector: + text: submit_tv_request: + name: Submit tv request description: Searches for a TV show and requests the first result. fields: name: + name: Name description: Search parameter + required: true example: "breaking bad" + selector: + text: season: - description: Which season(s) to request (first, latest or all) + name: Season + description: Which season(s) to request. example: "latest" + default: latest + selector: + select: + options: + - 'all' + - 'first' + - 'latest' submit_music_request: + name: Submit music request description: Searches for a music album and requests the first result. fields: name: + name: Name description: Search parameter + required: true example: "nevermind" + selector: + text: diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index e5a545e480688..8d2071dee7cac 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -1,5 +1,4 @@ """The Omnilogic integration.""" -import asyncio import logging from omnilogic import LoginException, OmniLogic, OmniLogicException @@ -11,18 +10,17 @@ from homeassistant.helpers import aiohttp_client from .common import OmniLogicUpdateCoordinator -from .const import CONF_SCAN_INTERVAL, COORDINATOR, DOMAIN, OMNI_API +from .const import ( + CONF_SCAN_INTERVAL, + COORDINATOR, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + OMNI_API, +) _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] - - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Omnilogic component.""" - hass.data.setdefault(DOMAIN, {}) - - return True +PLATFORMS = ["sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -32,9 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] - polling_interval = 6 - if CONF_SCAN_INTERVAL in conf: - polling_interval = conf[CONF_SCAN_INTERVAL] + polling_interval = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) session = aiohttp_client.async_get_clientsession(hass) @@ -54,33 +50,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass=hass, api=api, name="Omnilogic", + config_entry=entry, polling_interval=polling_interval, ) await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, OMNI_API: api, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 6f7ee6e5eb574..a103b8d112c47 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -3,8 +3,9 @@ from datetime import timedelta import logging -from omnilogic import OmniLogicException +from omnilogic import OmniLogic, OmniLogicException +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import ( @@ -30,12 +31,14 @@ class OmniLogicUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, - api: str, + api: OmniLogic, name: str, + config_entry: ConfigEntry, polling_interval: int, - ): + ) -> None: """Initialize the global Omnilogic data updater.""" self.api = api + self.config_entry = config_entry super().__init__( hass=hass, @@ -86,7 +89,7 @@ def __init__( name: str, item_id: tuple, icon: str, - ): + ) -> None: """Initialize the OmniLogic Entity.""" super().__init__(coordinator) @@ -103,9 +106,13 @@ def __init__( if bow_id is not None: unique_id = f"{unique_id}_{coordinator.data[bow_id]['systemId']}" - entity_friendly_name = ( - f"{entity_friendly_name}{coordinator.data[bow_id]['Name']} " - ) + + if kind != "Heaters": + entity_friendly_name = ( + f"{entity_friendly_name}{coordinator.data[bow_id]['Name']} " + ) + else: + entity_friendly_name = f"{entity_friendly_name}{coordinator.data[bow_id]['Operation']['VirtualHeater']['Name']} " unique_id = f"{unique_id}_{coordinator.data[item_id]['systemId']}_{kind}" @@ -155,3 +162,19 @@ def device_info(self): ATTR_MANUFACTURER: "Hayward", ATTR_MODEL: "OmniLogic", } + + +def check_guard(state_key, item, entity_setting): + """Validate that this entity passes the defined guard conditions defined at setup.""" + + if state_key not in item: + return True + + for guard_condition in entity_setting["guard_condition"]: + if guard_condition and all( + item.get(guard_key) == guard_value + for guard_key, guard_value in guard_condition.items() + ): + return True + + return False diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py index f8dffaeda44bb..d5239760fcc52 100644 --- a/homeassistant/components/omnilogic/config_flow.py +++ b/homeassistant/components/omnilogic/config_flow.py @@ -9,7 +9,7 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import CONF_SCAN_INTERVAL, DOMAIN +from .const import CONF_SCAN_INTERVAL, DEFAULT_PH_OFFSET, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -18,7 +18,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Omnilogic.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback @@ -30,7 +29,7 @@ async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} - config_entry = self.hass.config_entries.async_entries(DOMAIN) + config_entry = self._async_current_entries() if config_entry: return self.async_abort(reason="single_instance_allowed") @@ -88,8 +87,16 @@ async def async_step_init(self, user_input=None): { vol.Optional( CONF_SCAN_INTERVAL, - default=6, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), ): int, + vol.Optional( + "ph_offset", + default=self.config_entry.options.get( + "ph_offset", DEFAULT_PH_OFFSET + ), + ): vol.All(vol.Coerce(float)), } ), ) diff --git a/homeassistant/components/omnilogic/const.py b/homeassistant/components/omnilogic/const.py index a57ef2b062a92..41db7be506488 100644 --- a/homeassistant/components/omnilogic/const.py +++ b/homeassistant/components/omnilogic/const.py @@ -2,6 +2,8 @@ DOMAIN = "omnilogic" CONF_SCAN_INTERVAL = "polling_interval" +DEFAULT_SCAN_INTERVAL = 6 +DEFAULT_PH_OFFSET = 0 COORDINATOR = "coordinator" OMNI_API = "omni_api" ATTR_IDENTIFIERS = "identifiers" @@ -20,7 +22,7 @@ ALL_ITEM_KINDS = { "BOWS", "Filter", - "Heater", + "Heaters", "Chlorinator", "CSAD", "Lights", diff --git a/homeassistant/components/omnilogic/manifest.json b/homeassistant/components/omnilogic/manifest.json index 2b2a4a9fe3d31..ea2e951d08481 100644 --- a/homeassistant/components/omnilogic/manifest.json +++ b/homeassistant/components/omnilogic/manifest.json @@ -3,6 +3,7 @@ "name": "Hayward Omnilogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/omnilogic", - "requirements": ["omnilogic==0.4.3"], - "codeowners": ["@oliver84","@djtimca","@gentoosu"] + "requirements": ["omnilogic==0.4.5"], + "codeowners": ["@oliver84", "@djtimca", "@gentoosu"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index 25457224e9f00..beec071b192d0 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -9,8 +9,8 @@ VOLUME_LITERS, ) -from .common import OmniLogicEntity, OmniLogicUpdateCoordinator -from .const import COORDINATOR, DOMAIN, PUMP_TYPES +from .common import OmniLogicEntity, OmniLogicUpdateCoordinator, check_guard +from .const import COORDINATOR, DEFAULT_PH_OFFSET, DOMAIN, PUMP_TYPES async def async_setup_entry(hass, entry, async_add_entities): @@ -29,18 +29,7 @@ async def async_setup_entry(hass, entry, async_add_entities): for entity_setting in entity_settings: for state_key, entity_class in entity_setting["entity_classes"].items(): - if state_key not in item: - continue - - guard = False - for guard_condition in entity_setting["guard_condition"]: - if guard_condition and all( - item.get(guard_key) == guard_value - for guard_key, guard_value in guard_condition.items() - ): - guard = True - - if guard: + if check_guard(state_key, item, entity_setting): continue entity = entity_class( @@ -72,7 +61,7 @@ def __init__( unit: str, item_id: tuple, state_key: str, - ): + ) -> None: """Initialize Entities.""" super().__init__( coordinator=coordinator, @@ -136,13 +125,18 @@ class OmniLogicPumpSpeedSensor(OmnilogicSensor): def state(self): """Return the state for the pump speed sensor.""" - pump_type = PUMP_TYPES[self.coordinator.data[self._item_id]["Filter-Type"]] + pump_type = PUMP_TYPES[ + self.coordinator.data[self._item_id].get( + "Filter-Type", self.coordinator.data[self._item_id].get("Type", {}) + ) + ] pump_speed = self.coordinator.data[self._item_id][self._state_key] if pump_type == "VARIABLE": self._unit = PERCENTAGE state = pump_speed elif pump_type == "DUAL": + self._unit = None if pump_speed == 0: state = "off" elif pump_speed == self.coordinator.data[self._item_id].get( @@ -200,6 +194,12 @@ def state(self): if ph_state == 0: ph_state = None + else: + ph_state = float(ph_state) + float( + self.coordinator.config_entry.options.get( + "ph_offset", DEFAULT_PH_OFFSET + ) + ) return ph_state @@ -217,7 +217,7 @@ def __init__( device_class: str, icon: str, unit: str, - ): + ) -> None: """Initialize the sensor.""" super().__init__( coordinator=coordinator, @@ -234,7 +234,7 @@ def __init__( def state(self): """Return the state for the ORP sensor.""" - orp_state = self.coordinator.data[self._item_id][self._state_key] + orp_state = int(self.coordinator.data[self._item_id][self._state_key]) if orp_state == -1: orp_state = None diff --git a/homeassistant/components/omnilogic/services.yaml b/homeassistant/components/omnilogic/services.yaml new file mode 100644 index 0000000000000..b886fe7f7f789 --- /dev/null +++ b/homeassistant/components/omnilogic/services.yaml @@ -0,0 +1,17 @@ +set_pump_speed: + name: Set pump speed + description: Set the run speed of a variable speed pump. + target: + entity: + integration: omnilogic + domain: switch + fields: + speed: + name: Speed + description: Speed for the VSP between min and max speed. + required: true + example: 85 + selector: + number: + min: 0 + max: 100000 diff --git a/homeassistant/components/omnilogic/strings.json b/homeassistant/components/omnilogic/strings.json index c050a7945f1a9..9c55877b3b095 100644 --- a/homeassistant/components/omnilogic/strings.json +++ b/homeassistant/components/omnilogic/strings.json @@ -21,7 +21,8 @@ "step": { "init": { "data": { - "polling_interval": "Polling interval (in seconds)" + "polling_interval": "Polling interval (in seconds)", + "ph_offset": "pH offset (positive or negative)" } } } diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py new file mode 100644 index 0000000000000..771b02a24c1df --- /dev/null +++ b/homeassistant/components/omnilogic/switch.py @@ -0,0 +1,264 @@ +"""Platform for Omnilogic switch integration.""" +import time + +from omnilogic import OmniLogicException +import voluptuous as vol + +from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers import config_validation as cv, entity_platform + +from .common import OmniLogicEntity, OmniLogicUpdateCoordinator, check_guard +from .const import COORDINATOR, DOMAIN, PUMP_TYPES + +SERVICE_SET_SPEED = "set_pump_speed" +OMNILOGIC_SWITCH_OFF = 7 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the light platform.""" + + coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + entities = [] + + for item_id, item in coordinator.data.items(): + id_len = len(item_id) + item_kind = item_id[-2] + entity_settings = SWITCH_TYPES.get((id_len, item_kind)) + + if not entity_settings: + continue + + for entity_setting in entity_settings: + for state_key, entity_class in entity_setting["entity_classes"].items(): + if check_guard(state_key, item, entity_setting): + continue + + entity = entity_class( + coordinator=coordinator, + state_key=state_key, + name=entity_setting["name"], + kind=entity_setting["kind"], + item_id=item_id, + icon=entity_setting["icon"], + ) + + entities.append(entity) + + async_add_entities(entities) + + # register service + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_SET_SPEED, + {vol.Required("speed"): cv.positive_int}, + "async_set_speed", + ) + + +class OmniLogicSwitch(OmniLogicEntity, SwitchEntity): + """Define an Omnilogic Base Switch entity to be extended.""" + + def __init__( + self, + coordinator: OmniLogicUpdateCoordinator, + kind: str, + name: str, + icon: str, + item_id: tuple, + state_key: str, + ) -> None: + """Initialize Entities.""" + super().__init__( + coordinator=coordinator, + kind=kind, + name=name, + item_id=item_id, + icon=icon, + ) + + self._state_key = state_key + self._state = None + self._last_action = 0 + self._state_delay = 30 + + @property + def is_on(self): + """Return the on/off state of the switch.""" + state_int = 0 + + # The Omnilogic API has a significant delay in state reporting after calling for a + # change. This state delay will ensure that HA keeps an optimistic value of state + # during this period to improve the user experience and avoid confusion. + if self._last_action < (time.time() - self._state_delay): + state_int = int(self.coordinator.data[self._item_id][self._state_key]) + + if self._state == OMNILOGIC_SWITCH_OFF: + state_int = 0 + + self._state = state_int != 0 + + return self._state + + +class OmniLogicRelayControl(OmniLogicSwitch): + """Define the OmniLogic Relay entity.""" + + async def async_turn_on(self, **kwargs): + """Turn on the relay.""" + self._state = True + self._last_action = time.time() + self.async_write_ha_state() + + await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + 1, + ) + + async def async_turn_off(self, **kwargs): + """Turn off the relay.""" + self._state = False + self._last_action = time.time() + self.async_write_ha_state() + + await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + 0, + ) + + +class OmniLogicPumpControl(OmniLogicSwitch): + """Define the OmniLogic Pump Switch Entity.""" + + def __init__( + self, + coordinator: OmniLogicUpdateCoordinator, + kind: str, + name: str, + icon: str, + item_id: tuple, + state_key: str, + ) -> None: + """Initialize entities.""" + super().__init__( + coordinator=coordinator, + kind=kind, + name=name, + icon=icon, + item_id=item_id, + state_key=state_key, + ) + + self._max_speed = int(coordinator.data[item_id]["Max-Pump-Speed"]) + self._min_speed = int(coordinator.data[item_id]["Min-Pump-Speed"]) + + if "Filter-Type" in coordinator.data[item_id]: + self._pump_type = PUMP_TYPES[coordinator.data[item_id]["Filter-Type"]] + else: + self._pump_type = PUMP_TYPES[coordinator.data[item_id]["Type"]] + + self._last_speed = None + + async def async_turn_on(self, **kwargs): + """Turn on the pump.""" + self._state = True + self._last_action = time.time() + self.async_write_ha_state() + + on_value = 100 + + if self._pump_type != "SINGLE" and self._last_speed: + on_value = self._last_speed + + await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + on_value, + ) + + async def async_turn_off(self, **kwargs): + """Turn off the pump.""" + self._state = False + self._last_action = time.time() + self.async_write_ha_state() + + if self._pump_type != "SINGLE": + if "filterSpeed" in self.coordinator.data[self._item_id]: + self._last_speed = self.coordinator.data[self._item_id]["filterSpeed"] + else: + self._last_speed = self.coordinator.data[self._item_id]["pumpSpeed"] + + await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + 0, + ) + + async def async_set_speed(self, speed): + """Set the switch speed.""" + + if self._pump_type != "SINGLE": + if self._min_speed <= speed <= self._max_speed: + success = await self.coordinator.api.set_relay_valve( + int(self._item_id[1]), + int(self._item_id[3]), + int(self._item_id[-1]), + speed, + ) + + if success: + self.async_write_ha_state() + + else: + raise OmniLogicException( + "Cannot set speed. Speed is outside pump range." + ) + + else: + raise OmniLogicException("Cannot set speed on a non-variable speed pump.") + + +SWITCH_TYPES = { + (4, "Relays"): [ + { + "entity_classes": {"switchState": OmniLogicRelayControl}, + "name": "", + "kind": "relay", + "icon": None, + "guard_condition": [], + }, + ], + (6, "Relays"): [ + { + "entity_classes": {"switchState": OmniLogicRelayControl}, + "name": "", + "kind": "relay", + "icon": None, + "guard_condition": [], + } + ], + (6, "Pumps"): [ + { + "entity_classes": {"pumpState": OmniLogicPumpControl}, + "name": "", + "kind": "pump", + "icon": None, + "guard_condition": [], + } + ], + (6, "Filter"): [ + { + "entity_classes": {"filterState": OmniLogicPumpControl}, + "name": "", + "kind": "pump", + "icon": None, + "guard_condition": [], + } + ], +} diff --git a/homeassistant/components/omnilogic/translations/ca.json b/homeassistant/components/omnilogic/translations/ca.json index b460e47f2b8a3..7425fbcead6c4 100644 --- a/homeassistant/components/omnilogic/translations/ca.json +++ b/homeassistant/components/omnilogic/translations/ca.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "Compensaci\u00f3 de pH (positiu o negatiu)", "polling_interval": "Interval d'escaneig (segons)" } } diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json index 85de80d3dfab8..612793ca05955 100644 --- a/homeassistant/components/omnilogic/translations/de.json +++ b/homeassistant/components/omnilogic/translations/de.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH-Offset (positiv oder negativ)", "polling_interval": "Abfrageintervall (in Sekunden)" } } diff --git a/homeassistant/components/omnilogic/translations/en.json b/homeassistant/components/omnilogic/translations/en.json index e46253a922dba..809f8a0ec2846 100644 --- a/homeassistant/components/omnilogic/translations/en.json +++ b/homeassistant/components/omnilogic/translations/en.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH offset (positive or negative)", "polling_interval": "Polling interval (in seconds)" } } diff --git a/homeassistant/components/omnilogic/translations/es.json b/homeassistant/components/omnilogic/translations/es.json index 7e054159bf924..54aef5f189294 100644 --- a/homeassistant/components/omnilogic/translations/es.json +++ b/homeassistant/components/omnilogic/translations/es.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "Desplazamiento del pH (positivo o negativo)", "polling_interval": "Intervalo de sondeo (en segundos)" } } diff --git a/homeassistant/components/omnilogic/translations/et.json b/homeassistant/components/omnilogic/translations/et.json index c9a06f011170e..d9803ffd35213 100644 --- a/homeassistant/components/omnilogic/translations/et.json +++ b/homeassistant/components/omnilogic/translations/et.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH nihe (positiivne v\u00f5i negatiivne)", "polling_interval": "P\u00e4ringute intervall (sekundites)" } } diff --git a/homeassistant/components/omnilogic/translations/fr.json b/homeassistant/components/omnilogic/translations/fr.json index ab73af0d27a0a..4a8b293aebf97 100644 --- a/homeassistant/components/omnilogic/translations/fr.json +++ b/homeassistant/components/omnilogic/translations/fr.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "D\u00e9calage pH (positif ou n\u00e9gatif)", "polling_interval": "Intervalle d'interrogation (en secondes)" } } diff --git a/homeassistant/components/omnilogic/translations/it.json b/homeassistant/components/omnilogic/translations/it.json index ceff0e81258d4..28a9a1a04252a 100644 --- a/homeassistant/components/omnilogic/translations/it.json +++ b/homeassistant/components/omnilogic/translations/it.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "Scostamento del pH (positivo o negativo)", "polling_interval": "Intervallo di scansione (in secondi)" } } diff --git a/homeassistant/components/omnilogic/translations/nl.json b/homeassistant/components/omnilogic/translations/nl.json index 5189795ec9c04..b94599f93f4c9 100644 --- a/homeassistant/components/omnilogic/translations/nl.json +++ b/homeassistant/components/omnilogic/translations/nl.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH afwijking (positief of negatief)", "polling_interval": "Polling-interval (in seconden)" } } diff --git a/homeassistant/components/omnilogic/translations/no.json b/homeassistant/components/omnilogic/translations/no.json index 96c072082e1b9..15b44be91a835 100644 --- a/homeassistant/components/omnilogic/translations/no.json +++ b/homeassistant/components/omnilogic/translations/no.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "pH-forskyvning (positiv eller negativ)", "polling_interval": "Avstemningsintervall (i sekunder)" } } diff --git a/homeassistant/components/omnilogic/translations/pl.json b/homeassistant/components/omnilogic/translations/pl.json index 5fafc9637604e..f0fadfbfd6a28 100644 --- a/homeassistant/components/omnilogic/translations/pl.json +++ b/homeassistant/components/omnilogic/translations/pl.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "przesuni\u0119cie pH (dodatnie lub ujemne)", "polling_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w sekundach)" } } diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json index 5b00efefa1a20..51111556fb342 100644 --- a/homeassistant/components/omnilogic/translations/ru.json +++ b/homeassistant/components/omnilogic/translations/ru.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "\u0421\u043c\u0435\u0449\u0435\u043d\u0438\u0435 pH (\u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0438\u043b\u0438 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435)", "polling_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" } } diff --git a/homeassistant/components/omnilogic/translations/zh-Hant.json b/homeassistant/components/omnilogic/translations/zh-Hant.json index c2c39e00d68bd..89e49de710af9 100644 --- a/homeassistant/components/omnilogic/translations/zh-Hant.json +++ b/homeassistant/components/omnilogic/translations/zh-Hant.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "ph_offset": "ph \u504f\u5dee\u503c\uff08\u6b63\u6216\u8ca0\uff09", "polling_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u79d2\uff09" } } diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 06c9946b5c9b6..fe65d82f626f9 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -2,17 +2,8 @@ "domain": "onboarding", "name": "Home Assistant Onboarding", "documentation": "https://www.home-assistant.io/integrations/onboarding", - "after_dependencies": [ - "hassio" - ], - "dependencies": [ - "analytics", - "auth", - "http", - "person" - ], - "codeowners": [ - "@home-assistant/core" - ], + "after_dependencies": ["hassio"], + "dependencies": ["analytics", "auth", "http", "person"], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index 4dac83815ba8e..2b8b2cc22b7e7 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -1,5 +1,4 @@ """The Ondilo ICO integration.""" -import asyncio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -12,13 +11,6 @@ PLATFORMS = ["sensor"] -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Ondilo ICO component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ondilo ICO from a config entry.""" @@ -33,26 +25,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/ondilo_ico/api.py b/homeassistant/components/ondilo_ico/api.py index e753f8d6dcb68..f698dcc693e08 100644 --- a/homeassistant/components/ondilo_ico/api.py +++ b/homeassistant/components/ondilo_ico/api.py @@ -18,7 +18,7 @@ def __init__( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ): + ) -> None: """Initialize Ondilo ICO Auth.""" self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/ondilo_ico/config_flow.py b/homeassistant/components/ondilo_ico/config_flow.py index 74c668a3d2c8e..503f393630365 100644 --- a/homeassistant/components/ondilo_ico/config_flow.py +++ b/homeassistant/components/ondilo_ico/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Ondilo ICO.""" import logging -from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -14,7 +13,6 @@ class OAuth2FlowHandler( """Config flow to handle Ondilo ICO OAuth2 authentication.""" DOMAIN = DOMAIN - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" diff --git a/homeassistant/components/ondilo_ico/manifest.json b/homeassistant/components/ondilo_ico/manifest.json index ee1afd315d649..4c3ee64779a35 100644 --- a/homeassistant/components/ondilo_ico/manifest.json +++ b/homeassistant/components/ondilo_ico/manifest.json @@ -3,13 +3,8 @@ "name": "Ondilo ICO", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ondilo_ico", - "requirements": [ - "ondilo==0.2.0" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@JeromeHXP" - ] -} \ No newline at end of file + "requirements": ["ondilo==0.2.0"], + "dependencies": ["http"], + "codeowners": ["@JeromeHXP"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/ondilo_ico/oauth_impl.py b/homeassistant/components/ondilo_ico/oauth_impl.py index d6072cd6f6fc3..e1c6e6fdb9071 100644 --- a/homeassistant/components/ondilo_ico/oauth_impl.py +++ b/homeassistant/components/ondilo_ico/oauth_impl.py @@ -15,7 +15,7 @@ class OndiloOauth2Implementation(LocalOAuth2Implementation): """Local implementation of OAuth2 specific to Ondilo to hard code client id and secret and return a proper name.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Just init default class with default values.""" super().__init__( hass, diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 3af2bb7c32612..2428862cb316a 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -89,7 +89,7 @@ class OndiloICO(CoordinatorEntity, SensorEntity): def __init__( self, coordinator: DataUpdateCoordinator, poolidx: int, sensor_idx: int - ): + ) -> None: """Initialize sensor entity with data from coordinator.""" super().__init__(coordinator) diff --git a/homeassistant/components/ondilo_ico/translations/ca.json b/homeassistant/components/ondilo_ico/translations/ca.json index 77453bda39892..195d3d59262a9 100644 --- a/homeassistant/components/ondilo_ico/translations/ca.json +++ b/homeassistant/components/ondilo_ico/translations/ca.json @@ -12,6 +12,5 @@ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/cs.json b/homeassistant/components/ondilo_ico/translations/cs.json index bcb8849839caa..2a54a82f41b42 100644 --- a/homeassistant/components/ondilo_ico/translations/cs.json +++ b/homeassistant/components/ondilo_ico/translations/cs.json @@ -12,6 +12,5 @@ "title": "Vyberte metodu ov\u011b\u0159en\u00ed" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/de.json b/homeassistant/components/ondilo_ico/translations/de.json index ad11cefde66ec..5bab6ed132bf5 100644 --- a/homeassistant/components/ondilo_ico/translations/de.json +++ b/homeassistant/components/ondilo_ico/translations/de.json @@ -12,6 +12,5 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/en.json b/homeassistant/components/ondilo_ico/translations/en.json index c88a152ef81fc..e3849fc17a3aa 100644 --- a/homeassistant/components/ondilo_ico/translations/en.json +++ b/homeassistant/components/ondilo_ico/translations/en.json @@ -12,6 +12,5 @@ "title": "Pick Authentication Method" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/es.json b/homeassistant/components/ondilo_ico/translations/es.json index 2394c610796ea..db8d744d17620 100644 --- a/homeassistant/components/ondilo_ico/translations/es.json +++ b/homeassistant/components/ondilo_ico/translations/es.json @@ -12,6 +12,5 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/et.json b/homeassistant/components/ondilo_ico/translations/et.json index 132e9849cf104..c7d46e7e9426d 100644 --- a/homeassistant/components/ondilo_ico/translations/et.json +++ b/homeassistant/components/ondilo_ico/translations/et.json @@ -12,6 +12,5 @@ "title": "Vali tuvastusmeetod" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/fr.json b/homeassistant/components/ondilo_ico/translations/fr.json index c05fc0caaa622..540d3e1e6c295 100644 --- a/homeassistant/components/ondilo_ico/translations/fr.json +++ b/homeassistant/components/ondilo_ico/translations/fr.json @@ -12,6 +12,5 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/id.json b/homeassistant/components/ondilo_ico/translations/id.json index 1227a6d6689dc..876fe2f8c39c0 100644 --- a/homeassistant/components/ondilo_ico/translations/id.json +++ b/homeassistant/components/ondilo_ico/translations/id.json @@ -12,6 +12,5 @@ "title": "Pilih Metode Autentikasi" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/it.json b/homeassistant/components/ondilo_ico/translations/it.json index cd75684a4372a..42536508716c7 100644 --- a/homeassistant/components/ondilo_ico/translations/it.json +++ b/homeassistant/components/ondilo_ico/translations/it.json @@ -12,6 +12,5 @@ "title": "Scegli il metodo di autenticazione" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ko.json b/homeassistant/components/ondilo_ico/translations/ko.json index 88f3d678171bd..fa000ea1c06d1 100644 --- a/homeassistant/components/ondilo_ico/translations/ko.json +++ b/homeassistant/components/ondilo_ico/translations/ko.json @@ -12,6 +12,5 @@ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/nl.json b/homeassistant/components/ondilo_ico/translations/nl.json index 8a91dff086f64..0613d559fceaf 100644 --- a/homeassistant/components/ondilo_ico/translations/nl.json +++ b/homeassistant/components/ondilo_ico/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." }, "create_entry": { "default": "Succesvol geauthenticeerd" @@ -12,6 +12,5 @@ "title": "Kies een authenticatie methode" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/no.json b/homeassistant/components/ondilo_ico/translations/no.json index 4a06b93d045de..a8f6ce4f9a3f3 100644 --- a/homeassistant/components/ondilo_ico/translations/no.json +++ b/homeassistant/components/ondilo_ico/translations/no.json @@ -12,6 +12,5 @@ "title": "Velg godkjenningsmetode" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/pl.json b/homeassistant/components/ondilo_ico/translations/pl.json index f3aa08a250f83..8c75c11dd7c4c 100644 --- a/homeassistant/components/ondilo_ico/translations/pl.json +++ b/homeassistant/components/ondilo_ico/translations/pl.json @@ -12,6 +12,5 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ru.json b/homeassistant/components/ondilo_ico/translations/ru.json index 56bb2d342b757..10a412c8a6415 100644 --- a/homeassistant/components/ondilo_ico/translations/ru.json +++ b/homeassistant/components/ondilo_ico/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." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." @@ -12,6 +12,5 @@ "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" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/tr.json b/homeassistant/components/ondilo_ico/translations/tr.json index 9672275736570..6a0084d0a966c 100644 --- a/homeassistant/components/ondilo_ico/translations/tr.json +++ b/homeassistant/components/ondilo_ico/translations/tr.json @@ -5,6 +5,5 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/uk.json b/homeassistant/components/ondilo_ico/translations/uk.json index 31e5834b027aa..b7e2a5601bf48 100644 --- a/homeassistant/components/ondilo_ico/translations/uk.json +++ b/homeassistant/components/ondilo_ico/translations/uk.json @@ -12,6 +12,5 @@ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/zh-Hant.json b/homeassistant/components/ondilo_ico/translations/zh-Hant.json index ea1902b329553..b740fd3e063c9 100644 --- a/homeassistant/components/ondilo_ico/translations/zh-Hant.json +++ b/homeassistant/components/ondilo_ico/translations/zh-Hant.json @@ -12,6 +12,5 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index e5a214ce8a4a3..a27e1a49ab1f9 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -3,9 +3,9 @@ import logging from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, PLATFORMS from .onewirehub import CannotConnect, OneWireHub @@ -13,12 +13,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Set up 1-Wire integrations.""" - return True - - -async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up a 1-Wire proxy for a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -28,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): except CannotConnect as exc: raise ConfigEntryNotReady() from exc - hass.data[DOMAIN][config_entry.unique_id] = onewirehub + hass.data[DOMAIN][config_entry.entry_id] = onewirehub async def cleanup_registry() -> None: # Get registries @@ -70,16 +65,11 @@ async def start_platforms() -> None: return True -async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: - hass.data[DOMAIN].pop(config_entry.unique_id) + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 86b584c998c9b..9671a787c41eb 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -1,14 +1,21 @@ """Support for 1-Wire binary sensors.""" +from __future__ import annotations + import os from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_TYPE_OWSERVER, DOMAIN, SENSOR_TYPE_SENSED -from .onewire_entities import OneWireProxyEntity +from .model import DeviceComponentDescription +from .onewire_entities import OneWireBaseEntity, OneWireProxyEntity from .onewirehub import OneWireHub -DEVICE_BINARY_SENSORS = { +DEVICE_BINARY_SENSORS: dict[str, list[DeviceComponentDescription]] = { # Family : { path, sensor_type } "12": [ { @@ -77,19 +84,26 @@ } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up 1-Wire platform.""" # Only OWServer implementation works with binary sensors if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: - onewirehub = hass.data[DOMAIN][config_entry.unique_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job(get_entities, onewirehub) async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub): +def get_entities(onewirehub: OneWireHub) -> list[OneWireBaseEntity]: """Get a list of entities.""" - entities = [] + if not onewirehub.devices: + return [] + + entities: list[OneWireBaseEntity] = [] for device in onewirehub.devices: family = device["family"] @@ -98,7 +112,7 @@ def get_entities(onewirehub: OneWireHub): if family not in DEVICE_BINARY_SENSORS: continue - device_info = { + device_info: DeviceInfo = { "identifiers": {(DOMAIN, device_id)}, "manufacturer": "Maxim Integrated", "model": device_type, @@ -126,6 +140,6 @@ class OneWireProxyBinarySensor(OneWireProxyEntity, BinarySensorEntity): """Implementation of a 1-Wire binary sensor.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if sensor is on.""" - return self._state + return bool(self._state) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index fbb1d5debefeb..468aa6b9acf78 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -1,13 +1,17 @@ """Config flow for 1-Wire component.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol -from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_MOUNT_DIR, - CONF_TYPE_OWFS, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, DEFAULT_OWSERVER_HOST, @@ -33,7 +37,9 @@ ) -async def validate_input_owserver(hass: HomeAssistantType, data): +async def validate_input_owserver( + hass: HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA_OWSERVER with values provided by the user. @@ -50,19 +56,9 @@ async def validate_input_owserver(hass: HomeAssistantType, data): return {"title": host} -def is_duplicate_owserver_entry(hass: HomeAssistantType, user_input): - """Check existing entries for matching host and port.""" - for config_entry in hass.config_entries.async_entries(DOMAIN): - if ( - config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER - and config_entry.data[CONF_HOST] == user_input[CONF_HOST] - and config_entry.data[CONF_PORT] == user_input[CONF_PORT] - ): - return True - return False - - -async def validate_input_mount_dir(hass: HomeAssistantType, data): +async def validate_input_mount_dir( + hass: HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA_MOUNTDIR with values provided by the user. @@ -82,18 +78,19 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): """Handle 1-Wire config flow.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL - def __init__(self): + def __init__(self) -> None: """Initialize 1-Wire config flow.""" - self.onewire_config = {} + self.onewire_config: dict[str, Any] = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle 1-Wire config flow start. Let user manually input configuration. """ - errors = {} + errors: dict[str, str] = {} if user_input is not None: self.onewire_config.update(user_input) if CONF_TYPE_OWSERVER == user_input[CONF_TYPE]: @@ -107,13 +104,20 @@ async def async_step_user(self, user_input=None): errors=errors, ) - async def async_step_owserver(self, user_input=None): + async def async_step_owserver( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle OWServer configuration.""" errors = {} if user_input: # Prevent duplicate entries - if is_duplicate_owserver_entry(self.hass, user_input): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match( + { + CONF_TYPE: CONF_TYPE_OWSERVER, + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + ) self.onewire_config.update(user_input) @@ -132,7 +136,9 @@ async def async_step_owserver(self, user_input=None): errors=errors, ) - async def async_step_mount_dir(self, user_input=None): + async def async_step_mount_dir( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle SysBus configuration.""" errors = {} if user_input: @@ -159,7 +165,7 @@ async def async_step_mount_dir(self, user_input=None): errors=errors, ) - async def async_step_import(self, platform_config): + async def async_step_import(self, platform_config: dict[str, Any]) -> FlowResult: """Handle import configuration from YAML.""" # OWServer if platform_config[CONF_TYPE] == CONF_TYPE_OWSERVER: @@ -167,20 +173,6 @@ async def async_step_import(self, platform_config): platform_config[CONF_PORT] = DEFAULT_OWSERVER_PORT return await self.async_step_owserver(platform_config) - # OWFS - if platform_config[CONF_TYPE] == CONF_TYPE_OWFS: # pragma: no cover - # This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated. - # https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices - await self.async_set_unique_id( - f"{CONF_TYPE_OWFS}:{platform_config[CONF_MOUNT_DIR]}" - ) - self._abort_if_unique_id_configured( - updates=platform_config, reload_on_update=True - ) - return self.async_create_entry( - title=platform_config[CONF_MOUNT_DIR], data=platform_config - ) - # SysBus if CONF_MOUNT_DIR not in platform_config: platform_config[CONF_MOUNT_DIR] = DEFAULT_SYSBUS_MOUNT_DIR diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 54b18f7c9053a..d2c712c26c5a1 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -1,4 +1,6 @@ """Constants for 1-Wire component.""" +from __future__ import annotations + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -20,7 +22,6 @@ CONF_MOUNT_DIR = "mount_dir" CONF_NAMES = "names" -CONF_TYPE_OWFS = "OWFS" CONF_TYPE_OWSERVER = "OWServer" CONF_TYPE_SYSBUS = "SysBus" @@ -45,7 +46,7 @@ SWITCH_TYPE_LATCH = "latch" SWITCH_TYPE_PIO = "pio" -SENSOR_TYPES = { +SENSOR_TYPES: dict[str, list[str | None]] = { # SensorType: [ Unit, DeviceClass ] SENSOR_TYPE_TEMPERATURE: [TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE], SENSOR_TYPE_HUMIDITY: [PERCENTAGE, DEVICE_CLASS_HUMIDITY], diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 47ab6ad24046f..f48236c7f37ea 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/onewire", "config_flow": true, "requirements": ["pyownet==0.10.0.post1", "pi1wire==0.1.0"], - "codeowners": ["@garbled1", "@epenet"] + "codeowners": ["@garbled1", "@epenet"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/onewire/model.py b/homeassistant/components/onewire/model.py new file mode 100644 index 0000000000000..8dc841f16ba31 --- /dev/null +++ b/homeassistant/components/onewire/model.py @@ -0,0 +1,21 @@ +"""Type definitions for 1-Wire integration.""" +from __future__ import annotations + +from typing import TypedDict + + +class DeviceComponentDescription(TypedDict, total=False): + """Device component description class.""" + + path: str + name: str + type: str + default_disabled: bool + + +class OWServerDeviceDescription(TypedDict): + """OWServer device description class.""" + + path: str + family: str + type: str diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index 10c2b0c24a77b..b60d06739e83c 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -6,7 +6,8 @@ from pyownet import protocol -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.typing import StateType from .const import ( SENSOR_TYPE_COUNT, @@ -15,6 +16,7 @@ SWITCH_TYPE_LATCH, SWITCH_TYPE_PIO, ) +from .model import DeviceComponentDescription _LOGGER = logging.getLogger(__name__) @@ -24,14 +26,14 @@ class OneWireBaseEntity(Entity): def __init__( self, - name, - device_file, + name: str, + device_file: str, entity_type: str, - entity_name: str = None, - device_info=None, - default_disabled: bool = False, - unique_id: str = None, - ): + entity_name: str, + device_info: DeviceInfo, + default_disabled: bool, + unique_id: str, + ) -> None: """Initialize the entity.""" self._name = f"{name} {entity_name or entity_type.capitalize()}" self._device_file = device_file @@ -39,10 +41,10 @@ def __init__( self._device_class = SENSOR_TYPES[entity_type][1] self._unit_of_measurement = SENSOR_TYPES[entity_type][0] self._device_info = device_info - self._state = None - self._value_raw = None + self._state: StateType = None + self._value_raw: float | None = None self._default_disabled = default_disabled - self._unique_id = unique_id or device_file + self._unique_id = unique_id @property def name(self) -> str | None: @@ -65,7 +67,7 @@ def unique_id(self) -> str | None: return self._unique_id @property - def device_info(self) -> dict[str, Any] | None: + def device_info(self) -> DeviceInfo | None: """Return device specific attributes.""" return self._device_info @@ -82,11 +84,11 @@ def __init__( self, device_id: str, device_name: str, - device_info: dict[str, Any], + device_info: DeviceInfo, entity_path: str, - entity_specs: dict[str, Any], + entity_specs: DeviceComponentDescription, owproxy: protocol._Proxy, - ): + ) -> None: """Initialize the sensor.""" super().__init__( name=device_name, @@ -99,31 +101,30 @@ def __init__( ) self._owproxy = owproxy - def _read_value_ownet(self): + def _read_value_ownet(self) -> str: """Read a value from the owserver.""" - return self._owproxy.read(self._device_file).decode().lstrip() + read_bytes: bytes = self._owproxy.read(self._device_file) + return read_bytes.decode().lstrip() - def _write_value_ownet(self, value: bytes): + def _write_value_ownet(self, value: bytes) -> None: """Write a value to the owserver.""" - return self._owproxy.write(self._device_file, value) + self._owproxy.write(self._device_file, value) - def update(self): + def update(self) -> None: """Get the latest data from the device.""" - value = None try: self._value_raw = float(self._read_value_ownet()) except protocol.Error as exc: _LOGGER.error("Owserver failure in read(), got: %s", exc) + self._state = None else: if self._entity_type == SENSOR_TYPE_COUNT: - value = int(self._value_raw) + self._state = int(self._value_raw) elif self._entity_type in [ SENSOR_TYPE_SENSED, SWITCH_TYPE_LATCH, SWITCH_TYPE_PIO, ]: - value = int(self._value_raw) == 1 + self._state = int(self._value_raw) == 1 else: - value = round(self._value_raw, 1) - - self._state = value + self._state = round(self._value_raw, 1) diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 09a3235377dcf..26d085940553f 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -1,4 +1,6 @@ """Hub for communication with 1-Wire server or mount_dir.""" +from __future__ import annotations + import os from pi1wire import Pi1Wire @@ -6,10 +8,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS +from .model import OWServerDeviceDescription DEVICE_COUPLERS = { # Family : [branches] @@ -20,13 +23,13 @@ class OneWireHub: """Hub to communicate with SysBus or OWServer.""" - def __init__(self, hass: HomeAssistantType): + def __init__(self, hass: HomeAssistant) -> None: """Initialize.""" self.hass = hass - self.type: str = None - self.pi1proxy: Pi1Wire = None - self.owproxy: protocol._Proxy = None - self.devices = None + self.type: str | None = None + self.pi1proxy: Pi1Wire | None = None + self.owproxy: protocol._Proxy | None = None + self.devices: list | None = None async def connect(self, host: str, port: int) -> None: """Connect to the owserver host.""" @@ -54,10 +57,11 @@ async def initialize(self, config_entry: ConfigEntry) -> None: await self.connect(host, port) await self.discover_devices() - async def discover_devices(self): + async def discover_devices(self) -> None: """Discover all devices.""" if self.devices is None: if self.type == CONF_TYPE_SYSBUS: + assert self.pi1proxy self.devices = await self.hass.async_add_executor_job( self.pi1proxy.find_all_sensors ) @@ -65,11 +69,13 @@ async def discover_devices(self): self.devices = await self.hass.async_add_executor_job( self._discover_devices_owserver ) - return self.devices - def _discover_devices_owserver(self, path="/"): + def _discover_devices_owserver( + self, path: str = "/" + ) -> list[OWServerDeviceDescription]: """Discover all owserver devices.""" devices = [] + assert self.owproxy for device_path in self.owproxy.dir(path): device_family = self.owproxy.read(f"{device_path}family").decode() device_type = self.owproxy.read(f"{device_path}type").decode() diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 02af7a89ae3fa..3b63f551f984a 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -1,23 +1,27 @@ """Support for 1-Wire environment sensors.""" from __future__ import annotations -from glob import glob +import asyncio import logging import os +from types import MappingProxyType +from typing import Any -from pi1wire import InvalidCRCException, UnsupportResponseException +from pi1wire import InvalidCRCException, OneWireInterface, UnsupportResponseException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import StateType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType, StateType from .const import ( CONF_MOUNT_DIR, CONF_NAMES, - CONF_TYPE_OWFS, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, DEFAULT_OWSERVER_PORT, @@ -33,12 +37,13 @@ SENSOR_TYPE_VOLTAGE, SENSOR_TYPE_WETNESS, ) +from .model import DeviceComponentDescription from .onewire_entities import OneWireBaseEntity, OneWireProxyEntity from .onewirehub import OneWireHub _LOGGER = logging.getLogger(__name__) -DEVICE_SENSORS = { +DEVICE_SENSORS: dict[str, list[DeviceComponentDescription]] = { # Family : { SensorType: owfs path } "10": [ {"path": "temperature", "name": "Temperature", "type": SENSOR_TYPE_TEMPERATURE} @@ -146,7 +151,7 @@ # These can only be read by OWFS. Currently this driver only supports them # via owserver (network protocol) -HOBBYBOARD_EF = { +HOBBYBOARD_EF: dict[str, list[DeviceComponentDescription]] = { "HobbyBoards_EF": [ { "path": "humidity/humidity_corrected", @@ -190,7 +195,19 @@ # 7E sensors are special sensors by Embedded Data Systems -EDS_SENSORS = { +EDS_SENSORS: dict[str, list[DeviceComponentDescription]] = { + "EDS0066": [ + { + "path": "EDS0066/temperature", + "name": "Temperature", + "type": SENSOR_TYPE_TEMPERATURE, + }, + { + "path": "EDS0066/pressure", + "name": "Pressure", + "type": SENSOR_TYPE_PRESSURE, + }, + ], "EDS0068": [ { "path": "EDS0068/temperature", @@ -226,7 +243,7 @@ ) -def get_sensor_types(device_sub_type): +def get_sensor_types(device_sub_type: str) -> dict[str, Any]: """Return the proper info array for the device type.""" if "HobbyBoard" in device_sub_type: return HOBBYBOARD_EF @@ -235,16 +252,22 @@ def get_sensor_types(device_sub_type): return DEVICE_SENSORS -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: dict[str, Any], + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Old way of setting up 1-Wire platform.""" + _LOGGER.warning( + "Loading 1-Wire via platform setup is deprecated. " + "Please remove it from your configuration" + ) + if config.get(CONF_HOST): config[CONF_TYPE] = CONF_TYPE_OWSERVER elif config[CONF_MOUNT_DIR] == DEFAULT_SYSBUS_MOUNT_DIR: config[CONF_TYPE] = CONF_TYPE_SYSBUS - else: # pragma: no cover - # This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated. - # https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices - config[CONF_TYPE] = CONF_TYPE_OWFS hass.async_create_task( hass.config_entries.flow.async_init( @@ -253,18 +276,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up 1-Wire platform.""" - onewirehub = hass.data[DOMAIN][config_entry.unique_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job( get_entities, onewirehub, config_entry.data ) async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub, config): +def get_entities( + onewirehub: OneWireHub, config: MappingProxyType[str, Any] +) -> list[OneWireBaseEntity]: """Get a list of entities.""" - entities = [] + if not onewirehub.devices: + return [] + + entities: list[OneWireBaseEntity] = [] device_names = {} if CONF_NAMES in config and isinstance(config[CONF_NAMES], dict): device_names = config[CONF_NAMES] @@ -272,6 +304,7 @@ def get_entities(onewirehub: OneWireHub, config): conf_type = config[CONF_TYPE] # We have an owserver on a remote(or local) host/port if conf_type == CONF_TYPE_OWSERVER: + assert onewirehub.owproxy for device in onewirehub.devices: family = device["family"] device_type = device["type"] @@ -292,7 +325,7 @@ def get_entities(onewirehub: OneWireHub, config): device_id, ) continue - device_info = { + device_info: DeviceInfo = { "identifiers": {(DOMAIN, device_id)}, "manufacturer": "Maxim Integrated", "model": device_type, @@ -360,38 +393,6 @@ def get_entities(onewirehub: OneWireHub, config): "Check the mount_dir parameter if it's defined" ) - # We have an owfs mounted - else: # pragma: no cover - # This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated. - # https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices - base_dir = config[CONF_MOUNT_DIR] - _LOGGER.debug("Initializing using OWFS %s", base_dir) - _LOGGER.warning( - "The OWFS implementation of 1-Wire sensors is deprecated, " - "and should be migrated to OWServer (on localhost:4304). " - "If migration to OWServer is not feasible on your installation, " - "please raise an issue at https://github.com/home-assistant/core/issues/new" - "?title=Unable%20to%20migrate%20onewire%20from%20OWFS%20to%20OWServer", - ) - for family_file_path in glob(os.path.join(base_dir, "*", "family")): - with open(family_file_path) as family_file: - family = family_file.read() - if "EF" in family: - continue - if family in DEVICE_SENSORS: - for sensor_key, sensor_value in DEVICE_SENSORS[family].items(): - sensor_id = os.path.split(os.path.split(family_file_path)[0])[1] - device_file = os.path.join( - os.path.split(family_file_path)[0], sensor_value - ) - entities.append( - OneWireOWFSSensor( - device_names.get(sensor_id, sensor_id), - device_file, - sensor_key, - ) - ) - return entities @@ -416,9 +417,23 @@ def state(self) -> StateType: class OneWireDirectSensor(OneWireSensor): """Implementation of a 1-Wire sensor directly connected to RPI GPIO.""" - def __init__(self, name, device_file, device_info, owsensor): + def __init__( + self, + name: str, + device_file: str, + device_info: DeviceInfo, + owsensor: OneWireInterface, + ) -> None: """Initialize the sensor.""" - super().__init__(name, device_file, "temperature", "Temperature", device_info) + super().__init__( + name, + device_file, + "temperature", + "Temperature", + device_info, + False, + device_file, + ) self._owsensor = owsensor @property @@ -426,50 +441,35 @@ def state(self) -> StateType: """Return the state of the entity.""" return self._state - def update(self): + async def get_temperature(self) -> float: + """Get the latest data from the device.""" + attempts = 1 + while True: + try: + return await self.hass.async_add_executor_job( + self._owsensor.get_temperature + ) + except UnsupportResponseException as ex: + _LOGGER.debug( + "Cannot read from sensor %s (retry attempt %s): %s", + self._device_file, + attempts, + ex, + ) + await asyncio.sleep(0.2) + attempts += 1 + if attempts > 10: + raise + + async def async_update(self) -> None: """Get the latest data from the device.""" - value = None try: - self._value_raw = self._owsensor.get_temperature() - value = round(float(self._value_raw), 1) + self._value_raw = await self.get_temperature() + self._state = round(self._value_raw, 1) except ( FileNotFoundError, InvalidCRCException, UnsupportResponseException, ) as ex: _LOGGER.warning("Cannot read from sensor %s: %s", self._device_file, ex) - self._state = value - - -class OneWireOWFSSensor(OneWireSensor): # pragma: no cover - """Implementation of a 1-Wire sensor through owfs. - - This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated. - https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices - """ - - @property - def state(self) -> StateType: - """Return the state of the entity.""" - return self._state - - def _read_value_raw(self): - """Read the value as it is returned by the sensor.""" - with open(self._device_file) as ds_device_file: - lines = ds_device_file.readlines() - return lines - - def update(self): - """Get the latest data from the device.""" - value = None - try: - value_read = self._read_value_raw() - if len(value_read) == 1: - value = round(float(value_read[0]), 1) - self._value_raw = float(value_read[0]) - except ValueError: - _LOGGER.warning("Invalid value read from %s", self._device_file) - except FileNotFoundError: - _LOGGER.warning("Cannot read from sensor: %s", self._device_file) - - self._state = value + self._state = None diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index da1ed01a9809d..228c8f9d78bf3 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -1,16 +1,32 @@ """Support for 1-Wire environment switches.""" +from __future__ import annotations + import logging import os +from typing import Any from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_TYPE_OWSERVER, DOMAIN, SWITCH_TYPE_LATCH, SWITCH_TYPE_PIO -from .onewire_entities import OneWireProxyEntity +from .model import DeviceComponentDescription +from .onewire_entities import OneWireBaseEntity, OneWireProxyEntity from .onewirehub import OneWireHub -DEVICE_SWITCHES = { +DEVICE_SWITCHES: dict[str, list[DeviceComponentDescription]] = { # Family : { owfs path } + "05": [ + { + "path": "PIO", + "name": "PIO", + "type": SWITCH_TYPE_PIO, + "default_disabled": True, + }, + ], "12": [ { "path": "PIO.A", @@ -140,19 +156,26 @@ LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up 1-Wire platform.""" # Only OWServer implementation works with switches if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: - onewirehub = hass.data[DOMAIN][config_entry.unique_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job(get_entities, onewirehub) async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub): +def get_entities(onewirehub: OneWireHub) -> list[OneWireBaseEntity]: """Get a list of entities.""" - entities = [] + if not onewirehub.devices: + return [] + + entities: list[OneWireBaseEntity] = [] for device in onewirehub.devices: family = device["family"] @@ -162,7 +185,7 @@ def get_entities(onewirehub: OneWireHub): if family not in DEVICE_SWITCHES: continue - device_info = { + device_info: DeviceInfo = { "identifiers": {(DOMAIN, device_id)}, "manufacturer": "Maxim Integrated", "model": device_type, @@ -190,14 +213,14 @@ class OneWireProxySwitch(OneWireProxyEntity, SwitchEntity): """Implementation of a 1-Wire switch.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if sensor is on.""" - return self._state + return bool(self._state) - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self._write_value_ownet(b"1") - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self._write_value_ownet(b"0") diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index 9c606534a5b09..f9ee1b5e2c216 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index a1a7659bae50a..39c1686d03e61 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -3,5 +3,6 @@ "name": "Onkyo", "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": ["onkyo-eiscp==1.2.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 0eb39064db7cd..f90ccb1676099 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,6 +1,4 @@ """The ONVIF integration.""" -import asyncio - from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS @@ -88,12 +86,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if device.capabilities.events: platforms += ["binary_sensor", "sensor"] - for platform in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, platforms) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) + ) return True @@ -108,14 +105,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): platforms += ["binary_sensor", "sensor"] await device.events.async_stop() - return all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in platforms - ] - ) - ) + return await hass.config_entries.async_unload_platforms(entry, platforms) async def _get_snapshot_auth(device): diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 50390464df885..0e95d24ef7848 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -5,6 +5,7 @@ from haffmpeg.tools import IMAGE_JPEG, ImageFrame from onvif.exceptions import ONVIFError import voluptuous as vol +from yarl import URL from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG @@ -43,7 +44,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the ONVIF camera video stream.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() # Create PTZ service platform.async_register_entity_service( @@ -175,9 +176,10 @@ async def handle_async_mjpeg_stream(self, request): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" uri_no_auth = await self.device.async_get_stream_uri(self.profile) - self._stream_uri = uri_no_auth.replace( - "rtsp://", f"rtsp://{self.device.username}:{self.device.password}@", 1 - ) + url = URL(uri_no_auth) + url = url.with_user(self.device.username) + url = url.with_password(self.device.password) + self._stream_uri = str(url) async def async_perform_ptz( self, diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 0de9362795323..aeac90b301f87 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -76,7 +76,6 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a ONVIF config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @staticmethod @callback diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 826ff4b1a2982..e8428696bfce7 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -41,7 +41,7 @@ class ONVIFDevice: """Manages an ONVIF device.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None) -> None: """Initialize the device.""" self.hass: HomeAssistant = hass self.config_entry: ConfigEntry = config_entry diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 76b18d729a8f0..a45cc02c84b32 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -29,7 +29,9 @@ class EventManager: """ONVIF Event Manager.""" - def __init__(self, hass: HomeAssistant, device: ONVIFCamera, unique_id: str): + def __init__( + self, hass: HomeAssistant, device: ONVIFCamera, unique_id: str + ) -> None: """Initialize event manager.""" self.hass: HomeAssistant = hass self.device: ONVIFCamera = device diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 7329f629affa4..641497f52047c 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -9,5 +9,6 @@ ], "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index bed426e992444..ee5af2ae77ed1 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -1,34 +1,85 @@ ptz: + name: PTZ description: If your ONVIF camera supports PTZ, you will be able to pan, tilt or zoom your camera. + target: + entity: + integration: onvif + domain: camera fields: - entity_id: - description: "String or list of strings that point at entity_ids of cameras. Else targets all." - example: "camera.living_room_camera" tilt: - description: "Tilt direction. Allowed values: UP, DOWN" + name: Tilt + description: "Tilt direction." example: "UP" + selector: + select: + options: + - 'DOWN' + - 'UP' pan: - description: "Pan direction. Allowed values: RIGHT, LEFT" + name: Pan + description: "Pan direction." example: "RIGHT" + selector: + select: + options: + - 'LEFT' + - 'RIGHT' zoom: - description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT" + name: Zoom + description: "Zoom." example: "ZOOM_IN" + selector: + select: + options: + - 'ZOOM_IN' + - 'ZOOM_OUT' distance: - description: "Distance coefficient. Sets how much PTZ should be executed in one request. Allowed values: floating point numbers, 0 to 1" + name: Distance + description: "Distance coefficient. Sets how much PTZ should be executed in one request." default: 0.1 example: 0.1 + selector: + number: + min: 0 + max: 1 + step: 0.01 speed: - description: "Speed coefficient. Sets how fast PTZ will be executed. Allowed values: floating point numbers, 0 to 1" + name: Speed + description: "Speed coefficient. Sets how fast PTZ will be executed." default: 0.5 example: 0.5 + selector: + number: + min: 0 + max: 1 + step: 0.01 continuous_duration: + name: Continuous duration description: "Set ContinuousMove delay in seconds before stopping the move" default: 0.5 example: 0.5 + selector: + number: + min: 0 + max: 1 + step: 0.01 preset: + name: Preset description: "PTZ preset profile token. Sets the preset profile token which is executed with GotoPreset" example: "1" + default: "0" + selector: + text: move_mode: - description: "PTZ moving mode. One of ContinuousMove, RelativeMove, AbsoluteMove, GotoPreset, or Stop" + name: Move Mode + description: "PTZ moving mode." default: "RelativeMove" example: "ContinuousMove" + selector: + select: + options: + - 'AbsoluteMove' + - 'ContinuousMove' + - 'GotoPreset' + - 'RelativeMove' + - 'Stop' diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 4f75a639ee5d4..5b52990dde584 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ONVIF ya est\u00e1 en marcha.", "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifique la configuraci\u00f3n del perfil en su dispositivo.", "no_mac": "No se pudo configurar una identificaci\u00f3n \u00fanica para el dispositivo ONVIF.", - "onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Revise los registros para m\u00e1s informaci\u00f3n." + "onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Comprueba el registro para m\u00e1s informaci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index b21982fede835..9450b3e95699c 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_h264": "\u8a72\u88dd\u7f6e\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u88dd\u7f6e\u8a2d\u5b9a\u3002", "no_mac": "\u7121\u6cd5\u70ba ONVIF \u88dd\u7f6e\u8a2d\u5b9a\u552f\u4e00 ID\u3002", diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index e8ae2d240290a..bc33832bba13f 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -17,7 +17,7 @@ from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, ) -from homeassistant.const import CONF_API_KEY, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_REGION, HTTP_OK from homeassistant.core import split_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -41,8 +41,6 @@ "vn2", ] -CONF_REGION = "region" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/openalpr_cloud/manifest.json b/homeassistant/components/openalpr_cloud/manifest.json index dbb8253ff96d0..74b593bd1acfe 100644 --- a/homeassistant/components/openalpr_cloud/manifest.json +++ b/homeassistant/components/openalpr_cloud/manifest.json @@ -2,5 +2,6 @@ "domain": "openalpr_cloud", "name": "OpenALPR Cloud", "documentation": "https://www.home-assistant.io/integrations/openalpr_cloud", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index d098edba5b2f8..5e4b5298d1307 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -183,7 +183,6 @@ async def async_process_image(self, image): alpr = await asyncio.create_subprocess_exec( *self._cmd, - loop=self.hass.loop, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 29b9c3a07d801..8837d79369d3e 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -2,5 +2,6 @@ "domain": "openalpr_local", "name": "OpenALPR Local", "documentation": "https://www.home-assistant.io/integrations/openalpr_local", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index a0294a7aa49ce..d011c485d421c 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,6 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.20.2", "opencv-python-headless==4.3.0.36"], - "codeowners": [] + "requirements": ["numpy==1.20.3", "opencv-python-headless==4.4.0.42"], + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/openerz/manifest.json b/homeassistant/components/openerz/manifest.json index 9fa696a873a0e..b1e3b0597b5a2 100644 --- a/homeassistant/components/openerz/manifest.json +++ b/homeassistant/components/openerz/manifest.json @@ -3,5 +3,6 @@ "name": "Open ERZ", "documentation": "https://www.home-assistant.io/integrations/openerz", "codeowners": ["@misialq"], - "requirements": ["openerz-api==0.1.0"] + "requirements": ["openerz-api==0.1.0"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index 9cf38cbdd0d53..c4e5a5b7711f6 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -3,5 +3,6 @@ "name": "OpenEVSE", "documentation": "https://www.home-assistant.io/integrations/openevse", "requirements": ["openevsewifi==1.1.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index 60484aca77c73..43c45b6b66530 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -2,5 +2,6 @@ "domain": "openexchangerates", "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index 8bbf8c76c42c3..a14fb232eac57 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -2,8 +2,7 @@ "domain": "opengarage", "name": "OpenGarage", "documentation": "https://www.home-assistant.io/integrations/opengarage", - "codeowners": [ - "@danielhiversen" - ], - "requirements": ["open-garage==0.1.4"] + "codeowners": ["@danielhiversen"], + "requirements": ["open-garage==0.1.4"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openhardwaremonitor/manifest.json b/homeassistant/components/openhardwaremonitor/manifest.json index 242b00175d852..faf98c11a6df1 100644 --- a/homeassistant/components/openhardwaremonitor/manifest.json +++ b/homeassistant/components/openhardwaremonitor/manifest.json @@ -2,5 +2,6 @@ "domain": "openhardwaremonitor", "name": "Open Hardware Monitor", "documentation": "https://www.home-assistant.io/integrations/openhardwaremonitor", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 98fbf2d961a81..f45d6d31cef17 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -3,5 +3,6 @@ "name": "Linn / OpenHome", "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": ["openhomedevice==0.7.2"], - "codeowners": ["@bazwilliams"] + "codeowners": ["@bazwilliams"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 270eb22ebdaf0..7e333f7432b39 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -53,7 +53,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([entity]) openhome_data.add(device.Uuid()) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_INVOKE_PIN, diff --git a/homeassistant/components/openhome/services.yaml b/homeassistant/components/openhome/services.yaml index e8ae5fb55da62..29b07500c3f53 100644 --- a/homeassistant/components/openhome/services.yaml +++ b/homeassistant/components/openhome/services.yaml @@ -1,11 +1,19 @@ # Describes the format for available openhome services invoke_pin: + name: Invoke PIN description: Invoke a pin on the specified device. + target: + entity: + integration: openhome + domain: media_player fields: - entity_id: - description: The name of the openhome device to invoke the pin on - example: media_player.main_room pin: + name: PIN description: Which pin to invoke + required: true example: 4 + selector: + number: + min: 0 + max: 1000 diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index 780f5f59020d3..df750156d1de9 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -3,5 +3,6 @@ "name": "openSenseMap", "documentation": "https://www.home-assistant.io/integrations/opensensemap", "requirements": ["opensensemap-api==0.1.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index 17479b70de787..38877042d59a7 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -2,5 +2,6 @@ "domain": "opensky", "name": "OpenSky Network", "documentation": "https://www.home-assistant.io/integrations/opensky", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 8686997e7481a..e3ec9ddef130f 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,5 +1,4 @@ """Support for OpenTherm Gateway devices.""" -import asyncio from datetime import date, datetime import logging @@ -81,6 +80,8 @@ extra=vol.ALLOW_EXTRA, ) +PLATFORMS = [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR] + async def options_updated(hass, entry): """Handle options update.""" @@ -112,10 +113,7 @@ async def async_setup_entry(hass, config_entry): # Schedule directly on the loop to avoid blocking HA startup. hass.loop.create_task(gateway.connect_and_subscribe()) - for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, comp) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) register_services(hass) return True @@ -400,14 +398,10 @@ async def set_setback_temp(call): async def async_unload_entry(hass, entry): """Cleanup and disconnect from gateway.""" - await asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, COMP_BINARY_SENSOR), - hass.config_entries.async_forward_entry_unload(entry, COMP_CLIMATE), - hass.config_entries.async_forward_entry_unload(entry, COMP_SENSOR), - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] await gateway.cleanup() - return True + return unload_ok class OpenThermGatewayDevice: diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index aa764b7ae9e11..7c3bc8f8f6b53 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -31,7 +31,6 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """OpenTherm Gateway Config Flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @callback @@ -46,7 +45,7 @@ async def async_step_init(self, info=None): device = info[CONF_DEVICE] gw_id = cv.slugify(info.get(CONF_ID, name)) - entries = [e.data for e in self.hass.config_entries.async_entries(DOMAIN)] + entries = [e.data for e in self._async_current_entries()] if gw_id in [e[CONF_ID] for e in entries]: return self._show_form({"base": "id_exists"}) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index baa02dc3f4604..463a0aa105289 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": ["pyotgw==1.1b1"], "codeowners": ["@mvn23"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index 8a1bddc210015..fe3ecc157c500 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -1,13 +1,19 @@ # Describes the format for available opentherm_gw services reset_gateway: + name: Reset gateway description: Reset the OpenTherm Gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: set_central_heating_ovrd: + name: Set central heating override description: > Set the central heating override option on the gateway. When overriding the control setpoint (via a set_control_setpoint service call with a value other than 0), the gateway automatically enables the central heating override to start heating. @@ -16,49 +22,83 @@ set_central_heating_ovrd: You will only need this if you are writing your own software thermostat. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" ch_override: + name: Central heating override description: > The desired boolean value for the central heating override. + required: true example: "on" + selector: + boolean: set_clock: + name: Set clock description: Set the clock and day of the week on the connected thermostat. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" date: + name: Date description: Optional date from which the day of the week will be extracted. Defaults to today. example: "2018-10-23" + selector: + text: time: + name: Name description: Optional time in 24h format which will be provided to the thermostat. Defaults to the current time. example: "19:34" + selector: + text: set_control_setpoint: + name: Set control set point description: > Set the central heating control setpoint override on the gateway. You will only need this if you are writing your own software thermostat. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: temperature: + name: Temperature description: > The central heating setpoint to set on the gateway. Values between 0 and 90 are accepted, but not all boilers support this range. A value of 0 disables the central heating setpoint override. + required: true example: "37.5" + selector: + number: + min: 0 + max: 90 + step: 0.1 + unit_of_measurement: '°' set_hot_water_ovrd: + name: Set hot water override description: > Set the domestic hot water enable option on the gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: dhw_override: + name: Domestic hot water override description: > Control the domestic hot water enable option. If the boiler has been configured to let the room unit control when to keep a @@ -66,88 +106,187 @@ set_hot_water_ovrd: that. Value should be 0 or 1 to enable the override in off or on state, or "A" to disable the override. + required: true example: "1" + selector: + text: set_hot_water_setpoint: + name: Set hot water set point description: > Set the domestic hot water setpoint on the gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: temperature: + name: Temperature description: > The domestic hot water setpoint to set on the gateway. Not all boilers support this feature. Values between 0 and 90 are accepted, but not all boilers support this range. Check the values of the slave_dhw_min_setp and slave_dhw_max_setp sensors to see the supported range on your boiler. example: "60" + selector: + number: + min: 0 + max: 90 + step: 0.1 + unit_of_measurement: '°' set_gpio_mode: + name: Set gpio mode description: Change the function of the GPIO pins of the gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: id: - description: The ID of the GPIO pin. Either "A" or "B". + name: ID + description: The ID of the GPIO pin. + required: true example: "B" + selector: + select: + options: + - 'A' + - 'B' mode: + name: Mode description: > Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO "B". See https://www.home-assistant.io/integrations/opentherm_gw/#gpio-modes for an explanation of the values. + required: true example: "5" + selector: + number: + min: 0 + max: 7 set_led_mode: + name: Set LED mode description: Change the function of the LEDs of the gateway. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: id: - description: The ID of the LED. Possible values are "A" through "F". + name: ID + description: The ID of the LED. + required: true example: "C" + selector: + select: + options: + - 'A' + - 'B' + - 'C' + - 'D' + - 'E' + - 'F' mode: + name: Mode description: > The function to assign to the LED. One of "R", "X", "T", "B", "O", "F", "H", "W", "C", "E", "M" or "P". See https://www.home-assistant.io/integrations/opentherm_gw/#led-modes for an explanation of the values. + required: true example: "F" + selector: + select: + options: + - 'B' + - 'C' + - 'E' + - 'F' + - 'H' + - 'M' + - 'O' + - 'P' + - 'R' + - 'T' + - 'W' + - 'X' set_max_modulation: + name: Set max modulation description: > Override the maximum relative modulation level. You will only need this if you are writing your own software thermostat. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: level: + name: Level description: > The modulation level to provide to the gateway. - Values between 0 and 100 will set the modulation level. Provide a value of -1 to clear the override and forward the value from the thermostat again. + required: true example: "42" + selector: + number: + min: -1 + max: 100 set_outside_temperature: + name: Set outside temperature description: > Provide an outside temperature to the thermostat. If your thermostat is unable to display an outside temperature and does not support OTC (Outside Temperature Correction), this has no effect. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: temperature: + name: Temperature description: > The temperature to provide to the thermostat. Values between -40.0 and 64.0 will be accepted, but not all thermostats can display the full range. Any value above 64.0 will clear a previously configured value (suggestion: 99) + required: true example: "-2.3" + selector: + number: + min: -40 + max: 99 set_setback_temperature: + name: Set setback temperature description: Configure the setback temperature to be used with the GPIO away mode function. fields: gateway_id: + name: Gateway ID description: The gateway_id of the OpenTherm Gateway. + required: true example: "opentherm_gateway" + selector: + text: temperature: + name: Temperature description: The setback temperature to configure on the gateway. Values between 0.0 and 30.0 are accepted. + required: true example: "16.0" + selector: + number: + min: 0 + max: 30 + step: 0.1 diff --git a/homeassistant/components/opentherm_gw/translations/bg.json b/homeassistant/components/opentherm_gw/translations/bg.json index fbcaed52db593..3b120cebde9ac 100644 --- a/homeassistant/components/opentherm_gw/translations/bg.json +++ b/homeassistant/components/opentherm_gw/translations/bg.json @@ -19,8 +19,7 @@ "step": { "init": { "data": { - "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430", - "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442" + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430" }, "description": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/ca.json b/homeassistant/components/opentherm_gw/translations/ca.json index 3f38055fb8ca4..c4caf76d23392 100644 --- a/homeassistant/components/opentherm_gw/translations/ca.json +++ b/homeassistant/components/opentherm_gw/translations/ca.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Temperatura de la planta", - "precision": "Precisi\u00f3", "read_precision": "Llegeix precisi\u00f3", "set_precision": "Defineix precisi\u00f3", "temporary_override_mode": "Mode de sobreescriptura temporal" diff --git a/homeassistant/components/opentherm_gw/translations/cs.json b/homeassistant/components/opentherm_gw/translations/cs.json index 1b497fcf396c8..39d70d05cfcf5 100644 --- a/homeassistant/components/opentherm_gw/translations/cs.json +++ b/homeassistant/components/opentherm_gw/translations/cs.json @@ -20,8 +20,7 @@ "step": { "init": { "data": { - "floor_temperature": "Teplota podlahy", - "precision": "P\u0159esnost" + "floor_temperature": "Teplota podlahy" }, "description": "Mo\u017enosti br\u00e1ny OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/da.json b/homeassistant/components/opentherm_gw/translations/da.json index efc2262b41522..cca7cb347dbd1 100644 --- a/homeassistant/components/opentherm_gw/translations/da.json +++ b/homeassistant/components/opentherm_gw/translations/da.json @@ -19,8 +19,7 @@ "step": { "init": { "data": { - "floor_temperature": "Gulvtemperatur", - "precision": "Pr\u00e6cision" + "floor_temperature": "Gulvtemperatur" }, "description": "Indstillinger for OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/de.json b/homeassistant/components/opentherm_gw/translations/de.json index 36b7659294501..e7be781536672 100644 --- a/homeassistant/components/opentherm_gw/translations/de.json +++ b/homeassistant/components/opentherm_gw/translations/de.json @@ -21,7 +21,9 @@ "init": { "data": { "floor_temperature": "Boden-Temperatur", - "precision": "Genauigkeit" + "read_precision": "Pr\u00e4zision abfragen", + "set_precision": "Pr\u00e4zision einstellen", + "temporary_override_mode": "Tempor\u00e4rer Sollwert\u00fcbersteuerungsmodus" }, "description": "Optionen f\u00fcr das OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/en.json b/homeassistant/components/opentherm_gw/translations/en.json index a4e9eb664b223..c1e897c631b94 100644 --- a/homeassistant/components/opentherm_gw/translations/en.json +++ b/homeassistant/components/opentherm_gw/translations/en.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Floor Temperature", - "precision": "Precision", "read_precision": "Read Precision", "set_precision": "Set Precision", "temporary_override_mode": "Temporary Setpoint Override Mode" diff --git a/homeassistant/components/opentherm_gw/translations/es-419.json b/homeassistant/components/opentherm_gw/translations/es-419.json index 3b3a32987be6c..f9a6f60b463d3 100644 --- a/homeassistant/components/opentherm_gw/translations/es-419.json +++ b/homeassistant/components/opentherm_gw/translations/es-419.json @@ -20,7 +20,6 @@ "init": { "data": { "floor_temperature": "Temperatura del piso", - "precision": "Precisi\u00f3n", "read_precision": "Leer precisi\u00f3n", "set_precision": "Establecer precisi\u00f3n" }, diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index 7a85b685e891d..d780548a8fae4 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -21,9 +21,9 @@ "init": { "data": { "floor_temperature": "Temperatura del suelo", - "precision": "Precisi\u00f3n", "read_precision": "Leer precisi\u00f3n", - "set_precision": "Establecer precisi\u00f3n" + "set_precision": "Establecer precisi\u00f3n", + "temporary_override_mode": "Modo de anulaci\u00f3n temporal del punto de ajuste" }, "description": "Opciones para OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/et.json b/homeassistant/components/opentherm_gw/translations/et.json index 9aef362a6a0d0..67528b034c5cd 100644 --- a/homeassistant/components/opentherm_gw/translations/et.json +++ b/homeassistant/components/opentherm_gw/translations/et.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "P\u00f5randa temperatuur", - "precision": "T\u00e4psus", "read_precision": "Lugemi t\u00e4psus", "set_precision": "M\u00e4\u00e4ra lugemi t\u00e4psus", "temporary_override_mode": "Ajutine seadepunkti alistamine" diff --git a/homeassistant/components/opentherm_gw/translations/fr.json b/homeassistant/components/opentherm_gw/translations/fr.json index 7cc5b4ef84890..6b9bf0322447b 100644 --- a/homeassistant/components/opentherm_gw/translations/fr.json +++ b/homeassistant/components/opentherm_gw/translations/fr.json @@ -21,9 +21,9 @@ "init": { "data": { "floor_temperature": "Temp\u00e9rature du sol", - "precision": "Pr\u00e9cision", "read_precision": "Pr\u00e9cision de lecture", - "set_precision": "D\u00e9finir la pr\u00e9cision" + "set_precision": "D\u00e9finir la pr\u00e9cision", + "temporary_override_mode": "Mode de neutralisation du point de consigne temporaire" }, "description": "Options pour la passerelle OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index b8f51f4bb20c6..78ff8c886365f 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -21,7 +21,7 @@ "init": { "data": { "floor_temperature": "Padl\u00f3 h\u0151m\u00e9rs\u00e9klete", - "precision": "Pontoss\u00e1g" + "temporary_override_mode": "Ideiglenes be\u00e1ll\u00edt\u00e1s fel\u00fclb\u00edr\u00e1l\u00e1si m\u00f3dja" } } } diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json index 7c7624c3dfe07..f3677b9de3b14 100644 --- a/homeassistant/components/opentherm_gw/translations/id.json +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -21,7 +21,9 @@ "init": { "data": { "floor_temperature": "Suhu Lantai", - "precision": "Tingkat Presisi" + "read_precision": "Tingkat Presisi Baca", + "set_precision": "Atur Presisi", + "temporary_override_mode": "Mode Penimpaan Setpoint Sementara" }, "description": "Pilihan untuk Gateway OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/it.json b/homeassistant/components/opentherm_gw/translations/it.json index a082cd87586f3..4b3407cd95f0c 100644 --- a/homeassistant/components/opentherm_gw/translations/it.json +++ b/homeassistant/components/opentherm_gw/translations/it.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Temperatura del pavimento", - "precision": "Precisione", "read_precision": "Leggi la precisione", "set_precision": "Imposta la precisione", "temporary_override_mode": "Modalit\u00e0 di esclusione temporanea del setpoint" diff --git a/homeassistant/components/opentherm_gw/translations/ko.json b/homeassistant/components/opentherm_gw/translations/ko.json index 00f2902a4f3ca..178bf915a6b79 100644 --- a/homeassistant/components/opentherm_gw/translations/ko.json +++ b/homeassistant/components/opentherm_gw/translations/ko.json @@ -21,9 +21,9 @@ "init": { "data": { "floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", - "precision": "\uc815\ubc00\ub3c4", "read_precision": "\uc77d\uae30 \uc815\ubc00\ub3c4", - "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30" + "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30", + "temporary_override_mode": "\uc784\uc2dc \uc124\uc815\uac12 \uc7ac\uc815\uc758 \ubaa8\ub4dc" }, "description": "OpenTherm Gateway \uc635\uc158" } diff --git a/homeassistant/components/opentherm_gw/translations/lb.json b/homeassistant/components/opentherm_gw/translations/lb.json index 452ca69540f90..e90a6c2a310d3 100644 --- a/homeassistant/components/opentherm_gw/translations/lb.json +++ b/homeassistant/components/opentherm_gw/translations/lb.json @@ -20,8 +20,7 @@ "step": { "init": { "data": { - "floor_temperature": "Buedem Temperatur", - "precision": "Pr\u00e4zisioun" + "floor_temperature": "Buedem Temperatur" }, "description": "Optioune fir OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/lv.json b/homeassistant/components/opentherm_gw/translations/lv.json index 2c146e9d5630b..916fe4661a6f6 100644 --- a/homeassistant/components/opentherm_gw/translations/lv.json +++ b/homeassistant/components/opentherm_gw/translations/lv.json @@ -3,8 +3,7 @@ "step": { "init": { "data": { - "floor_temperature": "Gr\u012bdas temperat\u016bra", - "precision": "Precizit\u0101te" + "floor_temperature": "Gr\u012bdas temperat\u016bra" } } } diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json index bdd3337d05b50..025cdea128dde 100644 --- a/homeassistant/components/opentherm_gw/translations/nl.json +++ b/homeassistant/components/opentherm_gw/translations/nl.json @@ -21,9 +21,9 @@ "init": { "data": { "floor_temperature": "Vloertemperatuur", - "precision": "Precisie", "read_precision": "Lees Precisie", - "set_precision": "Precisie instellen" + "set_precision": "Precisie instellen", + "temporary_override_mode": "Tijdelijke setpoint-overschrijvingsmodus" }, "description": "Opties voor de OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index 07b7c77c5ccc4..9ef1aa0a5fbe2 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -21,9 +21,9 @@ "init": { "data": { "floor_temperature": "Etasje Temperatur", - "precision": "Presisjon", "read_precision": "Les presisjon", - "set_precision": "Angi presisjon" + "set_precision": "Angi presisjon", + "temporary_override_mode": "Midlertidig overstyringsmodus for settpunkt" }, "description": "Alternativer for OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/pl.json b/homeassistant/components/opentherm_gw/translations/pl.json index dc06752e40428..9e1c4b1363ddd 100644 --- a/homeassistant/components/opentherm_gw/translations/pl.json +++ b/homeassistant/components/opentherm_gw/translations/pl.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142", - "precision": "Precyzja", "read_precision": "Odczytaj precyzj\u0119", "set_precision": "Ustaw precyzj\u0119" }, diff --git a/homeassistant/components/opentherm_gw/translations/pt-BR.json b/homeassistant/components/opentherm_gw/translations/pt-BR.json deleted file mode 100644 index 96907fd4cdc31..0000000000000 --- a/homeassistant/components/opentherm_gw/translations/pt-BR.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "options": { - "step": { - "init": { - "data": { - "precision": "Precis\u00e3o" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/pt.json b/homeassistant/components/opentherm_gw/translations/pt.json index 85b6e6179632c..94a446dbe7138 100644 --- a/homeassistant/components/opentherm_gw/translations/pt.json +++ b/homeassistant/components/opentherm_gw/translations/pt.json @@ -12,14 +12,5 @@ } } } - }, - "options": { - "step": { - "init": { - "data": { - "precision": "Precis\u00e3o" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/ru.json b/homeassistant/components/opentherm_gw/translations/ru.json index 3b10be1166a90..9f2bfff07cfbe 100644 --- a/homeassistant/components/opentherm_gw/translations/ru.json +++ b/homeassistant/components/opentherm_gw/translations/ru.json @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", - "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c", "read_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0447\u0442\u0435\u043d\u0438\u044f", "set_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438", "temporary_override_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0430\u0432\u043a\u0438" diff --git a/homeassistant/components/opentherm_gw/translations/sl.json b/homeassistant/components/opentherm_gw/translations/sl.json index fb8ed5cac6dc2..a54ceb5031e27 100644 --- a/homeassistant/components/opentherm_gw/translations/sl.json +++ b/homeassistant/components/opentherm_gw/translations/sl.json @@ -19,8 +19,7 @@ "step": { "init": { "data": { - "floor_temperature": "Temperatura nadstropja", - "precision": "Natan\u010dnost" + "floor_temperature": "Temperatura nadstropja" }, "description": "Mo\u017enosti za prehod OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/translations/sv.json b/homeassistant/components/opentherm_gw/translations/sv.json index 6f21273b67ce5..01aa96564ac57 100644 --- a/homeassistant/components/opentherm_gw/translations/sv.json +++ b/homeassistant/components/opentherm_gw/translations/sv.json @@ -19,8 +19,7 @@ "step": { "init": { "data": { - "floor_temperature": "Golvetemperatur", - "precision": "Precision" + "floor_temperature": "Golvetemperatur" }, "description": "Alternativ f\u00f6r OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/translations/uk.json b/homeassistant/components/opentherm_gw/translations/uk.json index af7699271136b..fdffbfec00e90 100644 --- a/homeassistant/components/opentherm_gw/translations/uk.json +++ b/homeassistant/components/opentherm_gw/translations/uk.json @@ -20,8 +20,7 @@ "step": { "init": { "data": { - "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u0456\u0434\u043b\u043e\u0433\u0438", - "precision": "\u0422\u043e\u0447\u043d\u0456\u0441\u0442\u044c" + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u0456\u0434\u043b\u043e\u0433\u0438" }, "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0443 Opentherm" } diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index 8273eb1de983c..7b8466aa424b5 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728" }, @@ -21,7 +21,6 @@ "init": { "data": { "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", - "precision": "\u6e96\u78ba\u5ea6", "read_precision": "\u8b80\u53d6\u7cbe\u6e96\u5ea6", "set_precision": "\u8a2d\u5b9a\u7cbe\u6e96\u5ea6", "temporary_override_mode": "\u81e8\u6642 Setpoint \u8986\u84cb\u6a21\u5f0f" diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index aeefe435845d9..e1af166a3c296 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -69,10 +69,7 @@ async def async_setup_entry(hass, config_entry): LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @_verify_domain_control async def update_data(service): @@ -107,13 +104,8 @@ async def update_protection_data(service): async def async_unload_entry(hass, config_entry): """Unload an OpenUV config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 172b0ed3c443a..2ed6b56d9142b 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -28,7 +28,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle an OpenUV config flow.""" VERSION = 2 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def _show_form(self, errors=None): """Show the form to the user.""" diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index f55ca587679b7..81e38d251f124 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", "requirements": ["pyopenuv==1.0.9"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openuv/services.yaml b/homeassistant/components/openuv/services.yaml index ea353e848929b..e4886dfa7d82f 100644 --- a/homeassistant/components/openuv/services.yaml +++ b/homeassistant/components/openuv/services.yaml @@ -1,9 +1,12 @@ # Describes the format for available OpenUV services update_data: + name: Update data description: Request new data from OpenUV. Consumes two API calls. update_uv_index_data: + name: Update UV index data description: Request new UV index data from OpenUV. update_protection_data: + name: Update protection data description: Request new protection window data from OpenUV. diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index f6d47d1dcae01..49846a0ad0ae5 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -1,5 +1,4 @@ """The openweathermap component.""" -import asyncio import logging from pyowm import OWM @@ -31,12 +30,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the OpenWeatherMap component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up OpenWeatherMap as config entry.""" name = config_entry.data[CONF_NAME] @@ -61,10 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) update_listener = config_entry.add_update_listener(async_update_options) hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] = update_listener @@ -101,13 +91,8 @@ async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: update_listener = hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] diff --git a/homeassistant/components/openweathermap/abstract_owm_sensor.py b/homeassistant/components/openweathermap/abstract_owm_sensor.py index 30a21a057f0fb..ea12123b707cc 100644 --- a/homeassistant/components/openweathermap/abstract_owm_sensor.py +++ b/homeassistant/components/openweathermap/abstract_owm_sensor.py @@ -3,7 +3,15 @@ from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT +from .const import ( + ATTRIBUTION, + DEFAULT_NAME, + DOMAIN, + MANUFACTURER, + SENSOR_DEVICE_CLASS, + SENSOR_NAME, + SENSOR_UNIT, +) class AbstractOpenWeatherMapSensor(SensorEntity): @@ -36,6 +44,17 @@ def unique_id(self): """Return a unique_id for this entity.""" return self._unique_id + @property + def device_info(self): + """Return the device info.""" + split_unique_id = self._unique_id.split("-") + return { + "identifiers": {(DOMAIN, f"{split_unique_id[0]}-{split_unique_id[1]}")}, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + @property def should_poll(self): """Return the polling requirement of the entity.""" diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 7be4fe795aca1..507f1b6f72127 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -30,7 +30,6 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for OpenWeatherMap.""" VERSION = CONFIG_FLOW_VERSION - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 36d38ff4688f9..c1ca96188d8eb 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -42,6 +42,7 @@ DEFAULT_NAME = "OpenWeatherMap" DEFAULT_LANGUAGE = "en" ATTRIBUTION = "Data provided by OpenWeatherMap" +MANUFACTURER = "OpenWeather" CONF_LANGUAGE = "language" CONFIG_FLOW_VERSION = 2 ENTRY_NAME = "name" @@ -110,6 +111,7 @@ ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + ATTR_API_CLOUDS, ] LANGUAGES = [ "af", @@ -245,7 +247,11 @@ SENSOR_NAME: "Precipitation probability", SENSOR_UNIT: PERCENTAGE, }, - ATTR_FORECAST_PRESSURE: {SENSOR_NAME: "Pressure"}, + ATTR_FORECAST_PRESSURE: { + SENSOR_NAME: "Pressure", + SENSOR_UNIT: PRESSURE_HPA, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + }, ATTR_FORECAST_TEMP: { SENSOR_NAME: "Temperature", SENSOR_UNIT: TEMP_CELSIUS, @@ -265,4 +271,5 @@ SENSOR_NAME: "Wind speed", SENSOR_UNIT: SPEED_METERS_PER_SECOND, }, + ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: PERCENTAGE}, } diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index 27cda9fb26dcd..0b0114328acb6 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openweathermap", "requirements": ["pyowm==3.2.0"], - "codeowners": ["@fabaff", "@freekode", "@nzapponi"] + "codeowners": ["@fabaff", "@freekode", "@nzapponi"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index cac601b71d32c..da0305f633d4e 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -17,6 +17,7 @@ "mode": "Modus", "name": "Name der Integration" }, + "description": "Richten Sie die OpenWeatherMap-Integration ein. Zum Generieren des API-Schl\u00fcssels gehen Sie auf https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json index 108d4575e5580..cafa9c0fdd07b 100644 --- a/homeassistant/components/openweathermap/translations/sv.json +++ b/homeassistant/components/openweathermap/translations/sv.json @@ -8,8 +8,6 @@ "data": { "api_key": "OpenWeatherMap API-nyckel", "language": "Spr\u00e5k", - "latitude": "Latitud", - "longitude": "Longitud", "mode": "L\u00e4ge", "name": "Integrationens namn" }, diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 7908beb61d689..ffd3e4b726999 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,6 +1,7 @@ """Support for the OpenWeatherMap (OWM) service.""" from homeassistant.components.weather import WeatherEntity -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS +from homeassistant.util.pressure import convert as pressure_convert from .const import ( ATTR_API_CONDITION, @@ -11,9 +12,11 @@ ATTR_API_WIND_BEARING, ATTR_API_WIND_SPEED, ATTRIBUTION, + DEFAULT_NAME, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + MANUFACTURER, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -54,6 +57,16 @@ def unique_id(self): """Return a unique_id for this entity.""" return self._unique_id + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self._unique_id)}, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + @property def should_poll(self): """Return the polling requirement of the entity.""" @@ -82,7 +95,12 @@ def temperature_unit(self): @property def pressure(self): """Return the pressure.""" - return self._weather_coordinator.data[ATTR_API_PRESSURE] + pressure = self._weather_coordinator.data[ATTR_API_PRESSURE] + # OpenWeatherMap returns pressure in hPA, so convert to + # inHg if we aren't using metric. + if not self.hass.config.units.is_metric and pressure: + return pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG) + return pressure @property def humidity(self): diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 20cc71da72573..4518e3b6bda0e 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -167,6 +167,7 @@ def _convert_forecast(self, entry): ATTR_FORECAST_CONDITION: self._get_condition( entry.weather_code, entry.reference_time("unix") ), + ATTR_API_CLOUDS: entry.clouds, } temperature_dict = entry.temperature("celsius") diff --git a/homeassistant/components/opnsense/manifest.json b/homeassistant/components/opnsense/manifest.json index 129ca0108a5a7..ed390278969cf 100644 --- a/homeassistant/components/opnsense/manifest.json +++ b/homeassistant/components/opnsense/manifest.json @@ -3,5 +3,6 @@ "name": "OPNSense", "documentation": "https://www.home-assistant.io/integrations/opnsense", "requirements": ["pyopnsense==0.2.0"], - "codeowners": ["@mtreinish"] + "codeowners": ["@mtreinish"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/opple/manifest.json b/homeassistant/components/opple/manifest.json index bb6596c47ef96..1f0360e265a36 100644 --- a/homeassistant/components/opple/manifest.json +++ b/homeassistant/components/opple/manifest.json @@ -3,5 +3,6 @@ "name": "Opple", "documentation": "https://www.home-assistant.io/integrations/opple", "requirements": ["pyoppleio==1.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 904ff29cb1d09..7d96756a8d1b0 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "Orange Pi GPIO", "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": ["OPi.GPIO==0.4.0"], - "codeowners": ["@pascallj"] + "codeowners": ["@pascallj"], + "iot_class": "local_push" } diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json index 1be40a72d1c55..0d023a96ad5b8 100644 --- a/homeassistant/components/oru/manifest.json +++ b/homeassistant/components/oru/manifest.json @@ -3,5 +3,6 @@ "name": "Orange and Rockland Utility (ORU)", "documentation": "https://www.home-assistant.io/integrations/oru", "codeowners": ["@bvlaicu"], - "requirements": ["oru==0.1.11"] + "requirements": ["oru==0.1.11"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/orvibo/manifest.json b/homeassistant/components/orvibo/manifest.json index 83b5d644898ff..94c7391b6492c 100644 --- a/homeassistant/components/orvibo/manifest.json +++ b/homeassistant/components/orvibo/manifest.json @@ -3,5 +3,6 @@ "name": "Orvibo", "documentation": "https://www.home-assistant.io/integrations/orvibo", "requirements": ["orvibo==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index 80cfeff6e12bd..0596d4073eba5 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -3,5 +3,6 @@ "name": "Osramlightify", "documentation": "https://www.home-assistant.io/integrations/osramlightify", "requirements": ["lightify==1.0.7.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index cfd84eb206939..9b8b4527b2c4a 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/otp", "requirements": ["pyotp==2.3.0"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 98ed42ea10e42..18414db7292e3 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -3,17 +3,18 @@ from datetime import datetime, timedelta import logging -from typing import Any import aiohttp import async_timeout from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -24,13 +25,10 @@ _LOGGER = logging.getLogger(__name__) - -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Set up the OVO Energy components.""" - return True +PLATFORMS = ["sensor"] -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OVO Energy from a config entry.""" client = OVOEnergy() @@ -44,12 +42,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool raise ConfigEntryNotReady from exception if not authenticated: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - return False + raise ConfigEntryAuthFailed async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" @@ -61,12 +54,7 @@ async def async_update_data() -> OVODailyUsage: except aiohttp.ClientError as exception: raise UpdateFailed(exception) from exception if not authenticated: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - raise UpdateFailed("Not authenticated with OVO Energy") + raise ConfigEntryAuthFailed("Not authenticated with OVO Energy") return await client.get_daily_usage(datetime.utcnow().strftime("%Y-%m")) coordinator = DataUpdateCoordinator( @@ -89,21 +77,19 @@ async def async_update_data() -> OVODailyUsage: await coordinator.async_config_entry_first_refresh() # Setup components - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigType) -> bool: """Unload OVO Energy config entry.""" # Unload sensors - await hass.config_entries.async_forward_entry_unload(entry, "sensor") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) del hass.data[DOMAIN][entry.entry_id] - return True + return unload_ok class OVOEnergyEntity(CoordinatorEntity): @@ -150,7 +136,7 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity): """Defines a OVO Energy device entity.""" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this OVO Energy instance.""" return { "identifiers": {(DOMAIN, self._client.account_id)}, diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index f65b8007ecb3e..f4dcc5301d7f9 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -3,7 +3,6 @@ from ovoenergy.ovoenergy import OVOEnergy import voluptuous as vol -from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -19,7 +18,6 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a OVO Energy config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize the flow.""" @@ -74,18 +72,15 @@ async def async_step_reauth(self, user_input): errors["base"] = "connection_error" else: if authenticated: - await self.async_set_unique_id(self.username) - - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, - data={ - CONF_USERNAME: self.username, - CONF_PASSWORD: user_input[CONF_PASSWORD], - }, - ) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.username) + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + return self.async_abort(reason="reauth_successful") errors["base"] = "authorization_error" diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 6ec03eb19a5a1..ba559ffb41d4d 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -3,6 +3,7 @@ "name": "OVO Energy", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ovo_energy", - "requirements": ["ovoenergy==1.1.11"], - "codeowners": ["@timmo001"] + "requirements": ["ovoenergy==1.1.12"], + "codeowners": ["@timmo001"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index d03f7c49f96aa..7615a7011d3cf 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import OVOEnergyDeviceEntity @@ -17,7 +17,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up OVO Energy sensor based on a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ @@ -80,7 +80,7 @@ def unit_of_measurement(self) -> str: class OVOEnergyLastElectricityReading(OVOEnergySensor): """Defines a OVO Energy last reading sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, client: OVOEnergy): + def __init__(self, coordinator: DataUpdateCoordinator, client: OVOEnergy) -> None: """Initialize OVO Energy sensor.""" super().__init__( @@ -115,7 +115,7 @@ def extra_state_attributes(self) -> object: class OVOEnergyLastGasReading(OVOEnergySensor): """Defines a OVO Energy last reading sensor.""" - def __init__(self, coordinator: DataUpdateCoordinator, client: OVOEnergy): + def __init__(self, coordinator: DataUpdateCoordinator, client: OVOEnergy) -> None: """Initialize OVO Energy sensor.""" super().__init__( @@ -152,7 +152,7 @@ class OVOEnergyLastElectricityCost(OVOEnergySensor): def __init__( self, coordinator: DataUpdateCoordinator, client: OVOEnergy, currency: str - ): + ) -> None: """Initialize OVO Energy sensor.""" super().__init__( coordinator, @@ -188,7 +188,7 @@ class OVOEnergyLastGasCost(OVOEnergySensor): def __init__( self, coordinator: DataUpdateCoordinator, client: OVOEnergy, currency: str - ): + ) -> None: """Initialize OVO Energy sensor.""" super().__init__( coordinator, diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json index df19d4898f297..87605e2b3c4e9 100644 --- a/homeassistant/components/ovo_energy/strings.json +++ b/homeassistant/components/ovo_energy/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/homeassistant/components/ovo_energy/translations/ca.json b/homeassistant/components/ovo_energy/translations/ca.json index 3cc971434a4a2..f8552caa86bc3 100644 --- a/homeassistant/components/ovo_energy/translations/ca.json +++ b/homeassistant/components/ovo_energy/translations/ca.json @@ -5,7 +5,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index a86f39a614cf2..6fccec143332f 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -19,6 +19,7 @@ "password": "Passwort", "username": "Benutzername" }, + "description": "Richte eine OVO Energy-Instanz ein, um auf deinen Energieverbrauch zuzugreifen.", "title": "Ovo Energy Account hinzuf\u00fcgen" } } diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json index 7b3160af97e6f..3539d91220db8 100644 --- a/homeassistant/components/ovo_energy/translations/en.json +++ b/homeassistant/components/ovo_energy/translations/en.json @@ -5,7 +5,7 @@ "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/et.json b/homeassistant/components/ovo_energy/translations/et.json index b91f360159a39..ab33d27f337cc 100644 --- a/homeassistant/components/ovo_energy/translations/et.json +++ b/homeassistant/components/ovo_energy/translations/et.json @@ -5,7 +5,7 @@ "cannot_connect": "\u00dchendus nurjus", "invalid_auth": "Tuvastamise viga" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/it.json b/homeassistant/components/ovo_energy/translations/it.json index 9f4c2c68935fb..0f8dd492d0221 100644 --- a/homeassistant/components/ovo_energy/translations/it.json +++ b/homeassistant/components/ovo_energy/translations/it.json @@ -5,7 +5,7 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/nl.json b/homeassistant/components/ovo_energy/translations/nl.json index d598e17d93b59..245575d8666c8 100644 --- a/homeassistant/components/ovo_energy/translations/nl.json +++ b/homeassistant/components/ovo_energy/translations/nl.json @@ -5,7 +5,7 @@ "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json index 97bf9fc498f16..64b989de78b7f 100644 --- a/homeassistant/components/ovo_energy/translations/no.json +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -5,7 +5,7 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json index 5767f3f7cf2af..c426d1404177e 100644 --- a/homeassistant/components/ovo_energy/translations/pl.json +++ b/homeassistant/components/ovo_energy/translations/pl.json @@ -5,7 +5,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json index 89eb632102f58..d0a2ffb798de4 100644 --- a/homeassistant/components/ovo_energy/translations/ru.json +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -5,7 +5,7 @@ "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." }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/zh-Hant.json b/homeassistant/components/ovo_energy/translations/zh-Hant.json index 43f456f757400..0b1c218d94c38 100644 --- a/homeassistant/components/ovo_energy/translations/zh-Hant.json +++ b/homeassistant/components/ovo_energy/translations/zh-Hant.json @@ -5,7 +5,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "OVO Energy\uff1a{username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index d3091d7d02713..d51566718d60e 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -32,6 +32,7 @@ CONF_REGION_MAPPING = "region_mapping" CONF_EVENTS_ONLY = "events_only" BEACON_DEV_ID = "beacon" +PLATFORMS = ["device_tracker"] DEFAULT_OWNTRACKS_TOPIC = "owntracks/#" @@ -101,9 +102,7 @@ async def async_setup_entry(hass, entry): DOMAIN, "OwnTracks", webhook_id, handle_webhook ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "device_tracker") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.data[DOMAIN]["unsub"] = hass.helpers.dispatcher.async_dispatcher_connect( DOMAIN, async_handle_message @@ -115,10 +114,10 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload an OwnTracks config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) - await hass.config_entries.async_forward_entry_unload(entry, "device_tracker") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN]["unsub"]() - return True + return unload_ok async def async_remove_entry(hass, entry): diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 0fcca8953c70f..9e83e5b4ec46b 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -6,5 +6,6 @@ "requirements": ["PyNaCl==1.3.0"], "dependencies": ["webhook"], "after_dependencies": ["mqtt", "cloud"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/owntracks/translations/ru.json b/homeassistant/components/owntracks/translations/ru.json index bc38694d2a8ca..09fdba7726683 100644 --- a/homeassistant/components/owntracks/translations/ru.json +++ b/homeassistant/components/owntracks/translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index ace71e4af81c2..7890129dd9176 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -22,7 +22,7 @@ from homeassistant.components import mqtt from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.config_entries import ENTRY_STATE_LOADED, ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -56,14 +56,9 @@ DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" -async def async_setup(hass: HomeAssistant, config: dict): - """Initialize basic config of ozw component.""" - hass.data[DOMAIN] = {} - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # noqa: C901 """Set up ozw from a config entry.""" + hass.data.setdefault(DOMAIN, {}) ozw_data = hass.data[DOMAIN][entry.entry_id] = {} ozw_data[DATA_UNSUBSCRIBE] = [] @@ -97,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): else: mqtt_entries = hass.config_entries.async_entries("mqtt") - if not mqtt_entries or mqtt_entries[0].state != ENTRY_STATE_LOADED: + if not mqtt_entries or mqtt_entries[0].state is not ConfigEntryState.LOADED: _LOGGER.error("MQTT integration is not set up") return False @@ -105,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @callback def send_message(topic, payload): - if mqtt_entry.state != ENTRY_STATE_LOADED: + if mqtt_entry.state is not ConfigEntryState.LOADED: _LOGGER.error("MQTT integration is not set up") return @@ -306,14 +301,7 @@ async def async_stop_mqtt_client(event=None): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" # cleanup platforms - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not unload_ok: return False diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 2546a2e0aff66..cc07d738488ec 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -24,7 +24,6 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for ozw.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Set up flow instance.""" @@ -99,7 +98,7 @@ def _async_use_mqtt_integration(self): mqtt_entries = self.hass.config_entries.async_entries("mqtt") if ( not mqtt_entries - or mqtt_entries[0].state != config_entries.ENTRY_STATE_LOADED + or mqtt_entries[0].state is not config_entries.ConfigEntryState.LOADED ): return self.async_abort(reason="mqtt_required") return self._async_create_entry_from_vars() diff --git a/homeassistant/components/ozw/lock.py b/homeassistant/components/ozw/lock.py index 68acb3f96913a..9cadb2862f148 100644 --- a/homeassistant/components/ozw/lock.py +++ b/homeassistant/components/ozw/lock.py @@ -37,7 +37,7 @@ def async_add_lock(value): async_dispatcher_connect(hass, f"{DOMAIN}_new_{LOCK_DOMAIN}", async_add_lock) ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_USERCODE, diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index a1409fd79a81e..e2adce13339cb 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -3,15 +3,8 @@ "name": "OpenZWave (beta)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ozw", - "requirements": [ - "python-openzwave-mqtt[mqtt-client]==1.4.0" - ], - "after_dependencies": [ - "mqtt" - ], - "codeowners": [ - "@cgarwood", - "@marcelveldt", - "@MartinHjelmare" - ] + "requirements": ["python-openzwave-mqtt[mqtt-client]==1.4.0"], + "after_dependencies": ["mqtt"], + "codeowners": ["@cgarwood", "@marcelveldt", "@MartinHjelmare"], + "iot_class": "local_push" } diff --git a/homeassistant/components/ozw/services.yaml b/homeassistant/components/ozw/services.yaml index 641c086f52446..2919aceceb66c 100644 --- a/homeassistant/components/ozw/services.yaml +++ b/homeassistant/components/ozw/services.yaml @@ -1,58 +1,126 @@ # Describes the format for available Z-Wave services add_node: + name: Add node description: Add a new node to the Z-Wave network. fields: secure: + name: Secure description: Add the new node with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. + default: false + selector: + boolean: instance_id: - description: (Optional) The OZW Instance/Controller to use, defaults to 1. + name: Instance ID + description: The OZW Instance/Controller to use. + default: 1 + selector: + number: + min: 1 + max: 255 remove_node: + name: Remove node description: Remove a node from the Z-Wave network. Will set the controller into exclusion mode. fields: instance_id: - description: (Optional) The OZW Instance/Controller to use, defaults to 1. + name: Instance ID + description: The OZW Instance/Controller to use. + default: 1 + selector: + number: + min: 1 + max: 255 cancel_command: + name: Cancel command description: Cancel a pending add or remove node command. fields: instance_id: - description: (Optional) The OZW Instance/Controller to use, defaults to 1. + name: Instance ID + description: The OZW Instance/Controller to use. + default: 1 + 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 example: 10 + selector: + number: + min: 1 + max: 255 parameter: - description: Parameter number to set (integer). + name: Parameter + description: Parameter number to set. + required: true example: 8 + 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 example: 50268673 + selector: + text: instance_id: - description: (Optional) The OZW Instance/Controller to use, defaults to 1. + name: Instance ID + description: The OZW Instance/Controller to use. + default: 1 + selector: + number: + min: 1 + max: 255 clear_usercode: + name: Clear usercode description: Clear a usercode from lock. + target: + entity: + integration: ozw + domain: lock fields: - entity_id: - description: Lock entity_id. - example: lock.front_door_locked code_slot: + name: Code slot description: Code slot to clear code from. + required: true example: 1 + selector: + number: + min: 1 + max: 255 set_usercode: + name: Set usercode description: Set a usercode to lock. + target: + entity: + integration: ozw + domain: lock fields: - entity_id: - description: Lock entity_id. - example: lock.front_door_locked code_slot: + name: Code slot description: Code slot to set the code. + required: true example: 1 + selector: + number: + min: 1 + max: 255 usercode: + name: Usercode description: Code to set. + required: true example: 1234 + selector: + text: diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index 37ab2ea9c9e5f..9651b75386dff 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -4,7 +4,7 @@ "addon_info_failed": "\u53d6\u5f97 OpenZWave \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", "addon_set_config_failed": "OpenZWave a\u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" diff --git a/homeassistant/components/panasonic_bluray/manifest.json b/homeassistant/components/panasonic_bluray/manifest.json index c7e50c1c91a46..a9d6a4ebf76cb 100644 --- a/homeassistant/components/panasonic_bluray/manifest.json +++ b/homeassistant/components/panasonic_bluray/manifest.json @@ -3,5 +3,6 @@ "name": "Panasonic Blu-Ray Player", "documentation": "https://www.home-assistant.io/integrations/panasonic_bluray", "requirements": ["panacotta==0.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 67cf07dc43338..8f0a0e89d4581 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -1,5 +1,4 @@ """The Panasonic Viera integration.""" -import asyncio from functools import partial import logging from urllib.request import URLError @@ -104,25 +103,16 @@ async def async_setup_entry(hass, config_entry): data={**config, ATTR_DEVICE_INFO: device_info}, ) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) - if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 50a030b91dadf..93c33deb4dc10 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -29,7 +29,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Panasonic Viera.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the Panasonic Viera config flow.""" diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 7b9a3d7d4e0bc..fe365f85f2cea 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "requirements": ["panasonic_viera==0.3.6"], "codeowners": [], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/panasonic_viera/translations/zh-Hant.json b/homeassistant/components/panasonic_viera/translations/zh-Hant.json index 1b39556f45124..5b3e5ada9720f 100644 --- a/homeassistant/components/panasonic_viera/translations/zh-Hant.json +++ b/homeassistant/components/panasonic_viera/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/pandora/manifest.json b/homeassistant/components/pandora/manifest.json index 9ecb5b4b29d2c..45f87b36ec16f 100644 --- a/homeassistant/components/pandora/manifest.json +++ b/homeassistant/components/pandora/manifest.json @@ -3,5 +3,6 @@ "name": "Pandora", "documentation": "https://www.home-assistant.io/integrations/pandora", "requirements": ["pexpect==4.6.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json index 81802af1084a3..2e685a8625c51 100644 --- a/homeassistant/components/pcal9535a/manifest.json +++ b/homeassistant/components/pcal9535a/manifest.json @@ -3,5 +3,6 @@ "name": "PCAL9535A I/O Expander", "documentation": "https://www.home-assistant.io/integrations/pcal9535a", "requirements": ["pcal9535a==0.7"], - "codeowners": ["@Shulyaka"] + "codeowners": ["@Shulyaka"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pencom/manifest.json b/homeassistant/components/pencom/manifest.json index 0637c18b64749..e8b44173fe91d 100644 --- a/homeassistant/components/pencom/manifest.json +++ b/homeassistant/components/pencom/manifest.json @@ -3,5 +3,6 @@ "name": "Pencom", "documentation": "https://www.home-assistant.io/integrations/pencom", "requirements": ["pencompy==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 05d52cf7830e3..071261e7b2387 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -2,8 +2,9 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Mapping, MutableMapping import logging -from typing import Any, Mapping, MutableMapping +from typing import Any import voluptuous as vol diff --git a/homeassistant/components/persistent_notification/manifest.json b/homeassistant/components/persistent_notification/manifest.json index ff3ef06d97c89..c21e8150d8a81 100644 --- a/homeassistant/components/persistent_notification/manifest.json +++ b/homeassistant/components/persistent_notification/manifest.json @@ -3,5 +3,6 @@ "name": "Persistent Notification", "documentation": "https://www.home-assistant.io/integrations/persistent_notification", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/persistent_notification/services.yaml b/homeassistant/components/persistent_notification/services.yaml index 7917a06a3d790..5695a3c3b8207 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -1,26 +1,47 @@ create: + name: Create description: Show a notification in the frontend. fields: message: + name: Message description: Message body of the notification. [Templates accepted] + required: true example: Please check your configuration.yaml. + selector: + text: title: - description: Optional title for your notification. [Optional, Templates accepted] + name: Title + description: Optional title for your notification. [Templates accepted] example: Test notification + selector: + text: notification_id: - description: Target ID of the notification, will replace a notification with the same ID. [Optional] + name: Notification ID + description: Target ID of the notification, will replace a notification with the same ID. example: 1234 + selector: + text: dismiss: + name: Dismiss description: Remove a notification from the frontend. fields: notification_id: - description: Target ID of the notification, which should be removed. [Required] + name: Notification ID + description: Target ID of the notification, which should be removed. + required: true example: 1234 + selector: + text: mark_read: + name: Mark read description: Mark a notification read. fields: notification_id: - description: Target ID of the notification, which should be mark read. [Required] + name: Notification ID + description: Target ID of the notification, which should be mark read. + required: true example: 1234 + selector: + text: diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 1eb9d4eda7a40..7641a75e9c6c4 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -49,7 +49,7 @@ from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -168,7 +168,7 @@ def __init__( logger: logging.Logger, id_manager: collection.IDManager, yaml_collection: collection.YamlCollection, - ): + ) -> None: """Initialize a person storage collection.""" super().__init__(store, logger, id_manager) self.yaml_collection = yaml_collection @@ -259,7 +259,7 @@ async def _validate_user_id(self, user_id): raise ValueError("User already taken") -async def filter_yaml_data(hass: HomeAssistantType, persons: list[dict]) -> list[dict]: +async def filter_yaml_data(hass: HomeAssistant, persons: list[dict]) -> list[dict]: """Validate YAML data that we can't validate via schema.""" filtered = [] person_invalid_user = [] @@ -293,7 +293,7 @@ async def filter_yaml_data(hass: HomeAssistantType, persons: list[dict]) -> list return filtered -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistant, config: ConfigType): """Set up the person component.""" entity_component = EntityComponent(_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() @@ -514,7 +514,7 @@ def _parse_source_state(self, state): @websocket_api.websocket_command({vol.Required(CONF_TYPE): "person/list"}) def ws_list_person( - hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg ): """List persons.""" yaml, storage = hass.data[DOMAIN] diff --git a/homeassistant/components/person/group.py b/homeassistant/components/person/group.py index 07ec2cfe985ed..9bd2c991678c3 100644 --- a/homeassistant/components/person/group.py +++ b/homeassistant/components/person/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_HOME, STATE_NOT_HOME -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_HOME}, STATE_NOT_HOME) diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index 7aec7df7c9a69..09b74bf34eba0 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["image"], "after_dependencies": ["device_tracker"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/person/services.yaml b/homeassistant/components/person/services.yaml index 0af934f56b8d2..265c6049563df 100644 --- a/homeassistant/components/person/services.yaml +++ b/homeassistant/components/person/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload the person configuration. diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index b585451cdb076..f21337f512ed9 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -18,7 +18,6 @@ ) from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -28,12 +27,6 @@ LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Philips TV component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Philips TV from a config entry.""" @@ -47,26 +40,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi) await coordinator.async_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) @@ -76,7 +60,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): class PluggableAction: """A pluggable action handler.""" - def __init__(self, update: Callable[[], None]): + def __init__(self, update: Callable[[], None]) -> None: """Initialize.""" self._update = update self._actions: dict[Any, AutomationActionType] = {} @@ -101,7 +85,7 @@ def _remove(): return _remove - async def async_run(self, hass: HomeAssistantType, context: Context | None = None): + async def async_run(self, hass: HomeAssistant, context: Context | None = None): """Run all turn on triggers.""" for job, variables in self._actions.values(): hass.async_run_hass_job(job, variables, context) diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 8f0bcd161fcb8..84303e6ca92b6 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -39,7 +39,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Philips TV.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self) -> None: """Initialize flow.""" @@ -50,9 +49,7 @@ def __init__(self) -> None: async def async_step_import(self, conf: dict) -> dict: """Import a configuration from config.yaml.""" - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == conf[CONF_HOST]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: conf[CONF_HOST]}) return await self.async_step_user( { diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 36e01d8f3c8a6..9d1c4dbd04d0b 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,11 +2,8 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": [ - "ha-philipsjs==2.7.0" - ], - "codeowners": [ - "@elupus" - ], - "config_flow": true -} \ No newline at end of file + "requirements": ["ha-philipsjs==2.7.3"], + "codeowners": ["@elupus"], + "config_flow": true, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 7376d34e308af..61aa97a66b122 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -43,9 +43,8 @@ STATE_OFF, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator @@ -104,7 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities, ): @@ -129,7 +128,7 @@ def __init__( coordinator: PhilipsTVDataUpdateCoordinator, system: dict[str, Any], unique_id: str, - ): + ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api self._coordinator = coordinator diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index f4d34904f1b53..98ecab96fd485 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -37,7 +37,7 @@ def __init__( coordinator: PhilipsTVDataUpdateCoordinator, system: SystemType, unique_id: str, - ): + ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api self._coordinator = coordinator diff --git a/homeassistant/components/philips_js/translations/de.json b/homeassistant/components/philips_js/translations/de.json index 6288e9fb5c4ba..552d7aca07aa1 100644 --- a/homeassistant/components/philips_js/translations/de.json +++ b/homeassistant/components/philips_js/translations/de.json @@ -14,7 +14,8 @@ "data": { "pin": "PIN-Code" }, - "description": "Gib die auf deinem Fernseher angezeigten PIN ein" + "description": "Gib die auf deinem Fernseher angezeigten PIN ein", + "title": "Paaren" }, "user": { "data": { diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index c8d34e9ea9d6e..5cd00abc216b8 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, "error": { + "cannot_connect": "No se pudo conectar", "invalid_pin": "PIN no v\u00e1lido", - "pairing_failure": "No se ha podido emparejar: {error_id}" + "pairing_failure": "No se ha podido emparejar: {error_id}", + "unknown": "Error inesperado" }, "step": { "pair": { + "data": { + "pin": "C\u00f3digo PIN" + }, "description": "Introduzca el PIN que se muestra en el televisor", "title": "Par" }, diff --git a/homeassistant/components/philips_js/translations/id.json b/homeassistant/components/philips_js/translations/id.json index 633cfdd633e69..b9a1b948a9156 100644 --- a/homeassistant/components/philips_js/translations/id.json +++ b/homeassistant/components/philips_js/translations/id.json @@ -10,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "pair": { + "data": { + "pin": "Kode PIN" + }, + "description": "Masukkan PIN yang ditampilkan di TV Anda", + "title": "Pasangkan" + }, "user": { "data": { "api_version": "Versi API", diff --git a/homeassistant/components/neato/translations/pt-BR.json b/homeassistant/components/philips_js/translations/ro.json similarity index 61% rename from homeassistant/components/neato/translations/pt-BR.json rename to homeassistant/components/philips_js/translations/ro.json index 932b4b8a72e0a..aea8efa9d0d83 100644 --- a/homeassistant/components/neato/translations/pt-BR.json +++ b/homeassistant/components/philips_js/translations/ro.json @@ -1,9 +1,9 @@ { "config": { "step": { - "user": { + "pair": { "data": { - "username": "Usu\u00e1rio" + "pin": "Cod PIN" } } } diff --git a/homeassistant/components/philips_js/translations/sv.json b/homeassistant/components/philips_js/translations/sv.json new file mode 100644 index 0000000000000..418a59f0bdc7e --- /dev/null +++ b/homeassistant/components/philips_js/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_pin": "Ogiltig PIN-kod" + }, + "step": { + "pair": { + "data": { + "pin": "PIN-kod" + }, + "title": "Para ihop" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/zh-Hant.json b/homeassistant/components/philips_js/translations/zh-Hant.json index 7ae9c8893d557..de7f02b7a2137 100644 --- a/homeassistant/components/philips_js/translations/zh-Hant.json +++ b/homeassistant/components/philips_js/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/pi4ioe5v9xxxx/manifest.json b/homeassistant/components/pi4ioe5v9xxxx/manifest.json index f399c52859df0..4e12fcd009cae 100644 --- a/homeassistant/components/pi4ioe5v9xxxx/manifest.json +++ b/homeassistant/components/pi4ioe5v9xxxx/manifest.json @@ -1,7 +1,8 @@ { - "domain": "pi4ioe5v9xxxx", - "name": "pi4ioe5v9xxxx IO Expander", - "documentation": "https://www.home-assistant.io/integrations/pi4ioe5v9xxxx", - "requirements": ["pi4ioe5v9xxxx==0.0.2"], - "codeowners": ["@antonverburg"] + "domain": "pi4ioe5v9xxxx", + "name": "pi4ioe5v9xxxx IO Expander", + "documentation": "https://www.home-assistant.io/integrations/pi4ioe5v9xxxx", + "requirements": ["pi4ioe5v9xxxx==0.0.2"], + "codeowners": ["@antonverburg"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index bc486a0c9014e..34fdc9978c1c3 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1,5 +1,4 @@ """The pi_hole component.""" -import asyncio import logging from hole import Hole @@ -53,7 +52,10 @@ ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [PI_HOLE_SCHEMA]))}, + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [PI_HOLE_SCHEMA]))}, + ), extra=vol.ALLOW_EXTRA, ) @@ -126,23 +128,15 @@ async def async_update_data(): DATA_KEY_COORDINATOR: coordinator, } - for platform in _async_platforms(entry): - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, _async_platforms(entry)) return True async def async_unload_entry(hass, entry): """Unload Pi-hole entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in _async_platforms(entry) - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, _async_platforms(entry) ) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 60d53c4f904b8..68f0ecbbb2c1f 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -33,7 +33,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Pi-hole config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the config flow.""" diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index efe90bbf7e8b7..a96cae8b22b20 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/pi_hole", "requirements": ["hole==0.5.1"], "codeowners": ["@fabaff", "@johnluetke", "@shenxn"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/pi_hole/services.yaml b/homeassistant/components/pi_hole/services.yaml index fb9a5c17a132d..1b5da9f0d4fe7 100644 --- a/homeassistant/components/pi_hole/services.yaml +++ b/homeassistant/components/pi_hole/services.yaml @@ -1,9 +1,15 @@ disable: + name: Disable description: Disable configured Pi-hole(s) for an amount of time + target: + entity: + integration: pi_hole + domain: switch fields: - entity_id: - description: Target switch entity - example: switch.pi_hole duration: + name: Duration description: Time that the Pi-hole should be disabled for + required: true example: "00:00:15" + selector: + text: diff --git a/homeassistant/components/pi_hole/switch.py b/homeassistant/components/pi_hole/switch.py index 015bab8fe604c..955585243cfc2 100644 --- a/homeassistant/components/pi_hole/switch.py +++ b/homeassistant/components/pi_hole/switch.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(switches, True) # register service - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_DISABLE, { diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py new file mode 100644 index 0000000000000..055faadb78452 --- /dev/null +++ b/homeassistant/components/picnic/__init__.py @@ -0,0 +1,48 @@ +"""The Picnic integration.""" + +from python_picnic_api import PicnicAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant + +from .const import CONF_API, CONF_COORDINATOR, CONF_COUNTRY_CODE, DOMAIN +from .coordinator import PicnicUpdateCoordinator + +PLATFORMS = ["sensor"] + + +def create_picnic_client(entry: ConfigEntry): + """Create an instance of the PicnicAPI client.""" + return PicnicAPI( + auth_token=entry.data.get(CONF_ACCESS_TOKEN), + country_code=entry.data.get(CONF_COUNTRY_CODE), + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Picnic from a config entry.""" + picnic_client = await hass.async_add_executor_job(create_picnic_client, entry) + picnic_coordinator = PicnicUpdateCoordinator(hass, picnic_client, entry) + + # Fetch initial data so we have data when entities subscribe + await picnic_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + CONF_API: picnic_client, + CONF_COORDINATOR: picnic_coordinator, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + 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: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/picnic/config_flow.py b/homeassistant/components/picnic/config_flow.py new file mode 100644 index 0000000000000..09a1d52428333 --- /dev/null +++ b/homeassistant/components/picnic/config_flow.py @@ -0,0 +1,115 @@ +"""Config flow for Picnic integration.""" +from __future__ import annotations + +import logging + +from python_picnic_api import PicnicAPI +from python_picnic_api.session import PicnicAuthError +import requests +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME + +from .const import CONF_COUNTRY_CODE, COUNTRY_CODES, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_COUNTRY_CODE, default=COUNTRY_CODES[0]): vol.In( + COUNTRY_CODES + ), + } +) + + +class PicnicHub: + """Hub class to test user authentication.""" + + @staticmethod + def authenticate(username, password, country_code) -> tuple[str, dict]: + """Test if we can authenticate with the Picnic API.""" + picnic = PicnicAPI(username, password, country_code) + return picnic.session.auth_token, picnic.get_user() + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + hub = PicnicHub() + + try: + auth_token, user_data = await hass.async_add_executor_job( + hub.authenticate, + data[CONF_USERNAME], + data[CONF_PASSWORD], + data[CONF_COUNTRY_CODE], + ) + except requests.exceptions.ConnectionError as error: + raise CannotConnect from error + except PicnicAuthError as error: + raise InvalidAuth from error + + # Return the validation result + address = ( + f'{user_data["address"]["street"]} {user_data["address"]["house_number"]}' + + f'{user_data["address"]["house_number_ext"]}' + ) + return auth_token, { + "title": address, + "unique_id": user_data["user_id"], + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Picnic.""" + + VERSION = 1 + + 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: + auth_token, info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Set the unique id and abort if it already exists + await self.async_set_unique_id(info["unique_id"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=info["title"], + data={ + CONF_ACCESS_TOKEN: auth_token, + CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE], + }, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py new file mode 100644 index 0000000000000..18a625897322d --- /dev/null +++ b/homeassistant/components/picnic/const.py @@ -0,0 +1,118 @@ +"""Constants for the Picnic integration.""" +from homeassistant.const import CURRENCY_EURO, DEVICE_CLASS_TIMESTAMP + +DOMAIN = "picnic" + +CONF_API = "api" +CONF_COORDINATOR = "coordinator" +CONF_COUNTRY_CODE = "country_code" + +COUNTRY_CODES = ["NL", "DE", "BE"] +ATTRIBUTION = "Data provided by Picnic" +ADDRESS = "address" +CART_DATA = "cart_data" +SLOT_DATA = "slot_data" +LAST_ORDER_DATA = "last_order_data" + +SENSOR_CART_ITEMS_COUNT = "cart_items_count" +SENSOR_CART_TOTAL_PRICE = "cart_total_price" +SENSOR_SELECTED_SLOT_START = "selected_slot_start" +SENSOR_SELECTED_SLOT_END = "selected_slot_end" +SENSOR_SELECTED_SLOT_MAX_ORDER_TIME = "selected_slot_max_order_time" +SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE = "selected_slot_min_order_value" +SENSOR_LAST_ORDER_SLOT_START = "last_order_slot_start" +SENSOR_LAST_ORDER_SLOT_END = "last_order_slot_end" +SENSOR_LAST_ORDER_STATUS = "last_order_status" +SENSOR_LAST_ORDER_ETA_START = "last_order_eta_start" +SENSOR_LAST_ORDER_ETA_END = "last_order_eta_end" +SENSOR_LAST_ORDER_DELIVERY_TIME = "last_order_delivery_time" +SENSOR_LAST_ORDER_TOTAL_PRICE = "last_order_total_price" + +SENSOR_TYPES = { + SENSOR_CART_ITEMS_COUNT: { + "icon": "mdi:format-list-numbered", + "data_type": CART_DATA, + "state": lambda cart: cart.get("total_count", 0), + }, + SENSOR_CART_TOTAL_PRICE: { + "unit": CURRENCY_EURO, + "icon": "mdi:currency-eur", + "default_enabled": True, + "data_type": CART_DATA, + "state": lambda cart: cart.get("total_price", 0) / 100, + }, + SENSOR_SELECTED_SLOT_START: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-start", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot.get("window_start"), + }, + SENSOR_SELECTED_SLOT_END: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-end", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot.get("window_end"), + }, + SENSOR_SELECTED_SLOT_MAX_ORDER_TIME: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:clock-alert-outline", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot.get("cut_off_time"), + }, + SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE: { + "unit": CURRENCY_EURO, + "icon": "mdi:currency-eur", + "default_enabled": True, + "data_type": SLOT_DATA, + "state": lambda slot: slot["minimum_order_value"] / 100 + if slot.get("minimum_order_value") + else None, + }, + SENSOR_LAST_ORDER_SLOT_START: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-start", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("slot", {}).get("window_start"), + }, + SENSOR_LAST_ORDER_SLOT_END: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:calendar-end", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("slot", {}).get("window_end"), + }, + SENSOR_LAST_ORDER_STATUS: { + "icon": "mdi:list-status", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("status"), + }, + SENSOR_LAST_ORDER_ETA_START: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:clock-start", + "default_enabled": True, + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("eta", {}).get("start"), + }, + SENSOR_LAST_ORDER_ETA_END: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:clock-end", + "default_enabled": True, + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("eta", {}).get("end"), + }, + SENSOR_LAST_ORDER_DELIVERY_TIME: { + "class": DEVICE_CLASS_TIMESTAMP, + "icon": "mdi:timeline-clock", + "default_enabled": True, + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("delivery_time", {}).get("start"), + }, + SENSOR_LAST_ORDER_TOTAL_PRICE: { + "unit": CURRENCY_EURO, + "icon": "mdi:cash-marker", + "data_type": LAST_ORDER_DATA, + "state": lambda last_order: last_order.get("total_price", 0) / 100, + }, +} diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py new file mode 100644 index 0000000000000..bcd4e79a09895 --- /dev/null +++ b/homeassistant/components/picnic/coordinator.py @@ -0,0 +1,151 @@ +"""Coordinator to fetch data from the Picnic API.""" +import copy +from datetime import timedelta +import logging + +import async_timeout +from python_picnic_api import PicnicAPI +from python_picnic_api.session import PicnicAuthError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ADDRESS, CART_DATA, LAST_ORDER_DATA, SLOT_DATA + + +class PicnicUpdateCoordinator(DataUpdateCoordinator): + """The coordinator to fetch data from the Picnic API at a set interval.""" + + def __init__( + self, + hass: HomeAssistant, + picnic_api_client: PicnicAPI, + config_entry: ConfigEntry, + ) -> None: + """Initialize the coordinator with the given Picnic API client.""" + self.picnic_api_client = picnic_api_client + self.config_entry = config_entry + self._user_address = None + + logger = logging.getLogger(__name__) + super().__init__( + hass, + logger, + name="Picnic coordinator", + update_interval=timedelta(minutes=30), + ) + + async def _async_update_data(self) -> dict: + """Fetch data from API endpoint.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + data = await self.hass.async_add_executor_job(self.fetch_data) + + # Update the auth token in the config entry if applicable + self._update_auth_token() + + # Return the fetched data + return data + except ValueError as error: + raise UpdateFailed(f"API response was malformed: {error}") from error + except PicnicAuthError as error: + raise ConfigEntryAuthFailed from error + + def fetch_data(self): + """Fetch the data from the Picnic API and return a flat dict with only needed sensor data.""" + # Fetch from the API and pre-process the data + cart = self.picnic_api_client.get_cart() + last_order = self._get_last_order() + + if not cart or not last_order: + raise UpdateFailed("API response doesn't contain expected data.") + + slot_data = self._get_slot_data(cart) + + return { + ADDRESS: self._get_address(), + CART_DATA: cart, + SLOT_DATA: slot_data, + LAST_ORDER_DATA: last_order, + } + + def _get_address(self): + """Get the address that identifies the Picnic service.""" + if self._user_address is None: + address = self.picnic_api_client.get_user()["address"] + self._user_address = f'{address["street"]} {address["house_number"]}{address["house_number_ext"]}' + + return self._user_address + + @staticmethod + def _get_slot_data(cart: dict) -> dict: + """Get the selected slot, if it's explicitly selected.""" + selected_slot = cart.get("selected_slot", {}) + available_slots = cart.get("delivery_slots", []) + + if selected_slot.get("state") == "EXPLICIT": + slot_data = filter( + lambda slot: slot.get("slot_id") == selected_slot.get("slot_id"), + available_slots, + ) + if slot_data: + return next(slot_data) + + return {} + + def _get_last_order(self) -> dict: + """Get data of the last order from the list of deliveries.""" + # Get the deliveries + deliveries = self.picnic_api_client.get_deliveries(summary=True) + if not deliveries: + return {} + + # Determine the last order + last_order = copy.deepcopy(deliveries[0]) + + # Get the position details if the order is not delivered yet + delivery_position = {} + if not last_order.get("delivery_time"): + try: + delivery_position = self.picnic_api_client.get_delivery_position( + last_order["delivery_id"] + ) + except ValueError: + # No information yet can mean an empty response + pass + + # Determine the ETA, if available, the one from the delivery position API is more precise + # but it's only available shortly before the actual delivery. + last_order["eta"] = delivery_position.get( + "eta_window", last_order.get("eta2", {}) + ) + + # Determine the total price by adding up the total price of all sub-orders + total_price = 0 + for order in last_order.get("orders", []): + total_price += order.get("total_price", 0) + + # Sanitise the object + last_order["total_price"] = total_price + last_order.setdefault("delivery_time", {}) + if "eta2" in last_order: + del last_order["eta2"] + + # Make a copy because some references are local + return last_order + + @callback + def _update_auth_token(self): + """Set the updated authentication token.""" + updated_token = self.picnic_api_client.session.auth_token + if self.config_entry.data.get(CONF_ACCESS_TOKEN) != updated_token: + # Create an updated data dict + data = {**self.config_entry.data, CONF_ACCESS_TOKEN: updated_token} + + # Update the config entry + self.hass.config_entries.async_update_entry(self.config_entry, data=data) diff --git a/homeassistant/components/picnic/manifest.json b/homeassistant/components/picnic/manifest.json new file mode 100644 index 0000000000000..757f2ef24ad8b --- /dev/null +++ b/homeassistant/components/picnic/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "picnic", + "name": "Picnic", + "config_flow": true, + "iot_class": "cloud_polling", + "documentation": "https://www.home-assistant.io/integrations/picnic", + "requirements": ["python-picnic-api==1.1.0"], + "codeowners": ["@corneyl"] +} \ No newline at end of file diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py new file mode 100644 index 0000000000000..3a4d3582f9cda --- /dev/null +++ b/homeassistant/components/picnic/sensor.py @@ -0,0 +1,114 @@ +"""Definition of Picnic sensors.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ADDRESS, ATTRIBUTION, CONF_COORDINATOR, DOMAIN, SENSOR_TYPES + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Set up Picnic sensor entries.""" + picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR] + + # Add an entity for each sensor type + async_add_entities( + PicnicSensor(picnic_coordinator, config_entry, sensor_type, props) + for sensor_type, props in SENSOR_TYPES.items() + ) + + return True + + +class PicnicSensor(CoordinatorEntity): + """The CoordinatorEntity subclass representing Picnic sensors.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator[Any], + config_entry: ConfigEntry, + sensor_type, + properties, + ): + """Init a Picnic sensor.""" + super().__init__(coordinator) + + self.sensor_type = sensor_type + self.properties = properties + self.entity_id = f"sensor.picnic_{sensor_type}" + self._service_unique_id = config_entry.unique_id + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit this state is expressed in.""" + return self.properties.get("unit") + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return f"{self._service_unique_id}.{self.sensor_type}" + + @property + def name(self) -> str | None: + """Return the name of the entity.""" + return self._to_capitalized_name(self.sensor_type) + + @property + def state(self) -> StateType: + """Return the state of the entity.""" + data_set = ( + self.coordinator.data.get(self.properties["data_type"], {}) + if self.coordinator.data is not None + else {} + ) + return self.properties["state"](data_set) + + @property + def device_class(self) -> str | None: + """Return the class of this device, from component DEVICE_CLASSES.""" + return self.properties.get("class") + + @property + def icon(self) -> str | None: + """Return the icon to use in the frontend, if any.""" + return self.properties["icon"] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.last_update_success and self.state is not None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self.properties.get("default_enabled", False) + + @property + def extra_state_attributes(self): + """Return the sensor specific state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._service_unique_id)}, + "manufacturer": "Picnic", + "model": self._service_unique_id, + "name": f"Picnic: {self.coordinator.data[ADDRESS]}", + "entry_type": "service", + } + + @staticmethod + def _to_capitalized_name(name: str) -> str: + return name.replace("_", " ").capitalize() diff --git a/homeassistant/components/picnic/strings.json b/homeassistant/components/picnic/strings.json new file mode 100644 index 0000000000000..d43a91fbb0cb6 --- /dev/null +++ b/homeassistant/components/picnic/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Picnic", + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "country_code": "Country code" + } + } + }, + "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/picnic/translations/ca.json b/homeassistant/components/picnic/translations/ca.json new file mode 100644 index 0000000000000..c81d180aef006 --- /dev/null +++ b/homeassistant/components/picnic/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "country_code": "Codi de pa\u00eds", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/cs.json b/homeassistant/components/picnic/translations/cs.json new file mode 100644 index 0000000000000..dc27752e93594 --- /dev/null +++ b/homeassistant/components/picnic/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/de.json b/homeassistant/components/picnic/translations/de.json new file mode 100644 index 0000000000000..1a11e00664cef --- /dev/null +++ b/homeassistant/components/picnic/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "country_code": "L\u00e4ndercode", + "password": "Passwort", + "username": "Benutzername" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/en.json b/homeassistant/components/picnic/translations/en.json new file mode 100644 index 0000000000000..c7097df12a961 --- /dev/null +++ b/homeassistant/components/picnic/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": { + "country_code": "Country code", + "password": "Password", + "username": "Username" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json new file mode 100644 index 0000000000000..848f72e62d6d2 --- /dev/null +++ b/homeassistant/components/picnic/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "country_code": "C\u00f3digo del pa\u00eds", + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/et.json b/homeassistant/components/picnic/translations/et.json new file mode 100644 index 0000000000000..11fc0f1fe881e --- /dev/null +++ b/homeassistant/components/picnic/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "T\u00f5rge \u00fchendamisel", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "country_code": "Riigi kood", + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/fr.json b/homeassistant/components/picnic/translations/fr.json new file mode 100644 index 0000000000000..044b0a727711e --- /dev/null +++ b/homeassistant/components/picnic/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "country_code": "Code postal", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + }, + "title": "Pique-nique" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/it.json b/homeassistant/components/picnic/translations/it.json new file mode 100644 index 0000000000000..e77faae817d10 --- /dev/null +++ b/homeassistant/components/picnic/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "country_code": "Prefisso internazionale", + "password": "Password", + "username": "Nome utente" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/nl.json b/homeassistant/components/picnic/translations/nl.json new file mode 100644 index 0000000000000..210eebdf35717 --- /dev/null +++ b/homeassistant/components/picnic/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": { + "country_code": "Landcode", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/no.json b/homeassistant/components/picnic/translations/no.json new file mode 100644 index 0000000000000..45e3bcbb5487b --- /dev/null +++ b/homeassistant/components/picnic/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": { + "country_code": "Landskode", + "password": "Passord", + "username": "Brukernavn" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/pl.json b/homeassistant/components/picnic/translations/pl.json new file mode 100644 index 0000000000000..c278f29d13cf5 --- /dev/null +++ b/homeassistant/components/picnic/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "country_code": "Kod kraju", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/ru.json b/homeassistant/components/picnic/translations/ru.json new file mode 100644 index 0000000000000..e754faf8a0e96 --- /dev/null +++ b/homeassistant/components/picnic/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": { + "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/zh-Hant.json b/homeassistant/components/picnic/translations/zh-Hant.json new file mode 100644 index 0000000000000..2f72809d4fe91 --- /dev/null +++ b/homeassistant/components/picnic/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": { + "country_code": "\u570b\u78bc", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "title": "Picnic" +} \ No newline at end of file diff --git a/homeassistant/components/picotts/manifest.json b/homeassistant/components/picotts/manifest.json index 6f7a80be970db..cba95eb75b6aa 100644 --- a/homeassistant/components/picotts/manifest.json +++ b/homeassistant/components/picotts/manifest.json @@ -2,5 +2,6 @@ "domain": "picotts", "name": "Pico TTS", "documentation": "https://www.home-assistant.io/integrations/picotts", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/piglow/manifest.json b/homeassistant/components/piglow/manifest.json index 14d25b1dc92a7..f4b869aacf861 100644 --- a/homeassistant/components/piglow/manifest.json +++ b/homeassistant/components/piglow/manifest.json @@ -3,5 +3,6 @@ "name": "Piglow", "documentation": "https://www.home-assistant.io/integrations/piglow", "requirements": ["piglow==1.2.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pilight/manifest.json b/homeassistant/components/pilight/manifest.json index 8afafcd68b31b..e7173df21d9f2 100644 --- a/homeassistant/components/pilight/manifest.json +++ b/homeassistant/components/pilight/manifest.json @@ -3,5 +3,6 @@ "name": "Pilight", "documentation": "https://www.home-assistant.io/integrations/pilight", "requirements": ["pilight==0.1.1"], - "codeowners": ["@trekky12"] + "codeowners": ["@trekky12"], + "iot_class": "local_push" } diff --git a/homeassistant/components/pilight/services.yaml b/homeassistant/components/pilight/services.yaml index 9faa8908efbcd..6dc052043bf97 100644 --- a/homeassistant/components/pilight/services.yaml +++ b/homeassistant/components/pilight/services.yaml @@ -1,6 +1,11 @@ send: + name: Send description: Send RF code to Pilight device fields: protocol: + name: Protocol description: "Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports" + required: true example: "lirc" + selector: + object: diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 256023263bab6..d7d812d371ddd 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -54,18 +54,18 @@ def __init__(self, ip_address, dev_id, hass, config, privileged): def ping(self): """Send an ICMP echo request and return True if success.""" - pinger = subprocess.Popen( + with subprocess.Popen( self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL - ) - try: - pinger.communicate(timeout=1 + PING_TIMEOUT) - return pinger.returncode == 0 - except subprocess.TimeoutExpired: - kill_subprocess(pinger) - return False + ) as pinger: + try: + pinger.communicate(timeout=1 + PING_TIMEOUT) + return pinger.returncode == 0 + except subprocess.TimeoutExpired: + kill_subprocess(pinger) + return False - except subprocess.CalledProcessError: - return False + except subprocess.CalledProcessError: + return False def update(self) -> bool: """Update device state by sending one or more ping messages.""" @@ -141,9 +141,10 @@ async def _async_update_interval(now): try: await async_update(now) finally: - async_track_point_in_utc_time( - hass, _async_update_interval, util.dt.utcnow() + interval - ) + if not hass.is_stopping: + async_track_point_in_utc_time( + hass, _async_update_interval, util.dt.utcnow() + interval + ) await _async_update_interval(None) return True diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index 0995478760866..639a30a4fa02c 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], "requirements": ["icmplib==2.1.1"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/ping/services.yaml b/homeassistant/components/ping/services.yaml index e2da0c286271f..1f7e523e6851d 100644 --- a/homeassistant/components/ping/services.yaml +++ b/homeassistant/components/ping/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all ping entities. diff --git a/homeassistant/components/pioneer/manifest.json b/homeassistant/components/pioneer/manifest.json index 524f276441475..d19ecfb1f3615 100644 --- a/homeassistant/components/pioneer/manifest.json +++ b/homeassistant/components/pioneer/manifest.json @@ -2,5 +2,6 @@ "domain": "pioneer", "name": "Pioneer", "documentation": "https://www.home-assistant.io/integrations/pioneer", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/pjlink/manifest.json b/homeassistant/components/pjlink/manifest.json index 6b2dd94c0bd7b..ea07cc5d85a0b 100644 --- a/homeassistant/components/pjlink/manifest.json +++ b/homeassistant/components/pjlink/manifest.json @@ -3,5 +3,6 @@ "name": "PJLink", "documentation": "https://www.home-assistant.io/integrations/pjlink", "requirements": ["pypjlink2==1.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 2ec6028f9f94c..d73b997398a09 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -1,6 +1,5 @@ """Support for Plaato devices.""" -import asyncio from datetime import timedelta import logging @@ -84,15 +83,9 @@ ) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Plaato component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Configure based on config entry.""" - + hass.data.setdefault(DOMAIN, {}) use_webhook = entry.data[CONF_USE_WEBHOOK] if use_webhook: @@ -100,11 +93,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): else: await async_setup_coordinator(hass, entry) - for platform in PLATFORMS: - if entry.options.get(platform, True): - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms( + entry, [platform for platform in PLATFORMS if entry.options.get(platform, True)] + ) return True @@ -183,14 +174,7 @@ async def async_unload_coordinator(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_platforms(hass: HomeAssistant, entry: ConfigEntry, platforms): """Unload platforms.""" - unloaded = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in platforms - ] - ) - ) + unloaded = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unloaded: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 2cb1f4ce3261b..79a4657c31205 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -31,7 +31,6 @@ class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handles a Plaato config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize.""" @@ -172,7 +171,7 @@ def async_get_options_flow(config_entry): class PlaatoOptionsFlowHandler(config_entries.OptionsFlow): """Handle Plaato options.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize domain options flow.""" super().__init__() diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index e3291e5a229d2..99453f21d459a 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@JohNan"], - "requirements": ["pyplaato==0.0.15"] + "requirements": ["pyplaato==0.0.15"], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index 9a092ef4fa623..a95359abeaf7b 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -16,8 +16,10 @@ "step": { "api_method": { "data": { + "token": "F\u00fcgen Sie hier das Auth Token ein", "use_webhook": "Webhook verwenden" }, + "description": "Um die API abfragen zu k\u00f6nnen, wird ein `auth_token` ben\u00f6tigt, das durch folgende [diese](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) Anweisungen erhalten werden kann\n\n Ausgew\u00e4hltes Ger\u00e4t: **{Ger\u00e4tetyp}** \n\nWenn Sie lieber die eingebaute Webhook-Methode (nur Airlock) verwenden m\u00f6chten, setzen Sie bitte einen Haken und lassen Sie das Auth Token leer", "title": "API-Methode ausw\u00e4hlen" }, "user": { @@ -29,7 +31,8 @@ "title": "Plaato Webhook einrichten" }, "webhook": { - "description": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + "description": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url}).", + "title": "Zu verwendender Webhook" } } }, diff --git a/homeassistant/components/plaato/translations/nl.json b/homeassistant/components/plaato/translations/nl.json index d50763d0e1aca..23fae52b02090 100644 --- a/homeassistant/components/plaato/translations/nl.json +++ b/homeassistant/components/plaato/translations/nl.json @@ -19,7 +19,7 @@ "token": "Plak hier de verificatie-token", "use_webhook": "Webhook gebruiken" }, - "description": "Om de API te kunnenopvragen is een `auth_token` nodig, die kan worden verkregen door [deze] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructies te volgen\n\n Geselecteerd apparaat: **{device_type}** \n\nIndien u liever de ingebouwde webhook methode gebruikt (alleen Airlock) vink dan het vakje hieronder aan en laat Auth Token leeg", + "description": "Om de API te kunnen opvragen is een `auth_token` nodig, die kan worden verkregen door [deze] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructies te volgen\n\n Geselecteerd apparaat: **{device_type}** \n\nIndien u liever de ingebouwde webhook methode gebruikt (alleen Airlock) vink dan het vakje hieronder aan en laat Auth Token leeg", "title": "Selecteer API-methode" }, "user": { diff --git a/homeassistant/components/plaato/translations/ru.json b/homeassistant/components/plaato/translations/ru.json index 99e1bf94e0d9d..9ff1977dc53d3 100644 --- a/homeassistant/components/plaato/translations/ru.json +++ b/homeassistant/components/plaato/translations/ru.json @@ -31,7 +31,7 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Plaato" }, "webhook": { - "description": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\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]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", + "description": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "title": "Webhook" } } diff --git a/homeassistant/components/plant/group.py b/homeassistant/components/plant/group.py index 5d6edfa2b9ab3..90e894abb0f1d 100644 --- a/homeassistant/components/plant/group.py +++ b/homeassistant/components/plant/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OK, STATE_PROBLEM -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_PROBLEM}, STATE_OK) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 137c0524bac59..ffdf9aa555499 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,5 +1,4 @@ """Support to embed Plex.""" -import asyncio from functools import partial import logging @@ -15,15 +14,10 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY, SOURCE_REAUTH -from homeassistant.const import ( - CONF_SOURCE, - CONF_URL, - CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dev_reg, entity_registry as ent_reg from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer @@ -113,26 +107,17 @@ async def async_setup_entry(hass, entry): entry, data={**entry.data, PLEX_SERVER_CONFIG: new_server_data} ) except requests.exceptions.ConnectionError as error: - if entry.state != ENTRY_STATE_SETUP_RETRY: + if entry.state is not ConfigEntryState.SETUP_RETRY: _LOGGER.error( "Plex server (%s) could not be reached: [%s]", server_config[CONF_URL], error, ) raise ConfigEntryNotReady from error - except plexapi.exceptions.Unauthorized: - hass.async_create_task( - hass.config_entries.flow.async_init( - PLEX_DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error( - "Token not accepted, please reauthenticate Plex server '%s'", - entry.data[CONF_SERVER], - ) - return False + except plexapi.exceptions.Unauthorized as ex: + raise ConfigEntryAuthFailed( + f"Token not accepted, please reauthenticate Plex server '{entry.data[CONF_SERVER]}'" + ) from ex except ( plexapi.exceptions.BadRequest, plexapi.exceptions.NotFound, @@ -246,15 +231,11 @@ async def async_unload_entry(hass, entry): for unsub in dispatchers: unsub() - tasks = [ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - await asyncio.gather(*tasks) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) - return True + return unload_ok async def async_options_updated(hass, entry): diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index d1fa5684cf5f2..e18d72337ca02 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import http from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( @@ -25,7 +26,6 @@ from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.network import get_url from .const import ( AUTH_CALLBACK_NAME, @@ -52,6 +52,8 @@ from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer +HEADER_FRONTEND_BASE = "HA-Frontend-Base" + _LOGGER = logging.getLogger(__package__) @@ -80,7 +82,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Plex config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @callback @@ -286,7 +287,11 @@ async def async_step_integration_discovery(self, discovery_info): async def async_step_plex_website_auth(self): """Begin external auth flow on Plex website.""" self.hass.http.register_view(PlexAuthorizationCallbackView) - hass_url = get_url(self.hass) + if (req := http.current_request.get()) is None: + raise RuntimeError("No current request in context") + if (hass_url := req.headers.get(HEADER_FRONTEND_BASE)) is None: + raise RuntimeError("No header in request") + headers = {"Origin": hass_url} payload = { "X-Plex-Device-Name": X_PLEX_DEVICE_NAME, diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index e0e62d7150bf0..5d6ffd19550e1 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,10 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.5.1", - "plexauth==0.0.6", - "plexwebsocket==0.0.13" + "plexapi==4.5.1", + "plexauth==0.0.6", + "plexwebsocket==0.0.13" ], "dependencies": ["http"], - "codeowners": ["@jjlawren"] + "codeowners": ["@jjlawren"], + "iot_class": "local_push" } diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index f3f92880c44c6..e19d86e89ec46 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -52,7 +52,9 @@ class UnknownMediaType(BrowseError): _LOGGER = logging.getLogger(__name__) -def browse_media(entity, is_internal, media_content_type=None, media_content_id=None): +def browse_media( # noqa: C901 + entity, is_internal, media_content_type=None, media_content_id=None +): """Implement the websocket media browsing helper.""" def item_payload(item): diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index af1343095f0c0..2dc7b83b4396c 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -1,4 +1,6 @@ """Models to represent various Plex objects used in the integration.""" +import logging + from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -7,7 +9,15 @@ ) from homeassistant.util import dt as dt_util -LIVE_TV_SECTION = -4 +LIVE_TV_SECTION = "Live TV" +TRANSIENT_SECTION = "Preroll" +UNKNOWN_SECTION = "Unknown" +SPECIAL_SECTIONS = { + -2: TRANSIENT_SECTION, + -4: LIVE_TV_SECTION, +} + +_LOGGER = logging.getLogger(__name__) class PlexSession: @@ -66,8 +76,15 @@ def update_media(self, media): if media.duration: self.media_duration = int(media.duration / 1000) - if media.librarySectionID == LIVE_TV_SECTION: - self.media_library_title = "Live TV" + if media.librarySectionID in SPECIAL_SECTIONS: + self.media_library_title = SPECIAL_SECTIONS[media.librarySectionID] + elif media.librarySectionID < 1: + self.media_library_title = UNKNOWN_SECTION + _LOGGER.warning( + "Unknown library section ID (%s) for title '%s', please create an issue", + media.librarySectionID, + media.title, + ) else: self.media_library_title = ( media.section().title if media.librarySectionID is not None else "" @@ -115,7 +132,7 @@ def get_media_image_url(self, media): """Get the image URL from a media object.""" thumb_url = media.thumbUrl if media.type == "episode" and not self.plex_server.option_use_episode_art: - if media.librarySectionID == LIVE_TV_SECTION: + if SPECIAL_SECTIONS.get(media.librarySectionID) == LIVE_TV_SECTION: thumb_url = media.grandparentThumb else: thumb_url = media.url(media.grandparentThumb) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d4bd4b09ef26b..4dcdda044ebe6 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -316,7 +316,7 @@ def _fetch_platform_data(self): self.plextv_clients(), ) - async def _async_update_platforms(self): + async def _async_update_platforms(self): # noqa: C901 """Update the platform entities.""" _LOGGER.debug("Updating devices") diff --git a/homeassistant/components/plex/services.yaml b/homeassistant/components/plex/services.yaml index 366acb43a5bb0..782a4d17c189f 100644 --- a/homeassistant/components/plex/services.yaml +++ b/homeassistant/components/plex/services.yaml @@ -1,26 +1,21 @@ -play_on_sonos: - description: Play music hosted on a Plex server on a linked Sonos speaker. - fields: - entity_id: - description: Entity ID of a media_player from the Sonos integration. - example: "media_player.sonos_living_room" - media_content_id: - description: The ID of the content to play. See https://www.home-assistant.io/integrations/plex/#music for details. - example: >- - '{ "library_name": "Music", "artist_name": "Stevie Wonder" }' - media_content_type: - description: The type of content to play. Must be "music". - example: "music" - refresh_library: + name: Refresh library description: Refresh a Plex library to scan for new and updated media. fields: server_name: + name: Server name description: Name of a Plex server if multiple Plex servers configured. example: "My Plex Server" + selector: + text: library_name: + name: Library name description: Name of the Plex library to refresh. + required: true example: "TV Shows" + selector: + text: scan_for_clients: + name: Scan for clients description: Scan for available clients from the Plex server(s), local network, and plex.tv. diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 47a9a1e7d9c1d..d425cca246ebb 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -7,11 +7,6 @@ from .gateway import async_setup_entry_gw, async_unload_entry_gw -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Plugwise platform.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plugwise components from a config entry.""" if entry.data.get(CONF_HOST): diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index e17c85a79788d..d19c3b49920cc 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -78,7 +78,6 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Plugwise Smile.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the Plugwise config flow.""" @@ -114,9 +113,7 @@ async def async_step_user_gateway(self, user_input=None): user_input[CONF_HOST] = self.discovery_info[CONF_HOST] user_input[CONF_PORT] = self.discovery_info.get(CONF_PORT, DEFAULT_PORT) - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == user_input[CONF_HOST]: - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) try: api = await validate_gw_input(self.hass, user_input) diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 70a4a822431b7..41e3caacbffff 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -25,6 +25,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -135,10 +136,7 @@ async def async_update_data(): if single_master_thermostat is None: platforms = SENSOR_PLATFORMS - for platform in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, platforms) return True @@ -153,13 +151,8 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry_gw(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_GATEWAY - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + entry, PLATFORMS_GATEWAY ) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() @@ -197,7 +190,7 @@ def name(self): return self._name @property - def device_info(self) -> dict[str, any]: + def device_info(self) -> DeviceInfo: """Return the device information.""" device_information = { "identifiers": {(DOMAIN, self._dev_id)}, diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 998b84fe5d449..f81c240284621 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -5,5 +5,6 @@ "requirements": ["plugwise==0.8.5"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra"], "zeroconf": ["_plugwise._tcp.local."], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 2ed6721bab36d..e5a7ab5a6a9ed 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -37,6 +37,6 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" }, - "flow_title": "Smile: {name}" + "flow_title": "{name}" } } diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index ee580d2786ee5..c29fa230519a9 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -8,7 +8,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 685cd6fb9ae9f..97587131774f1 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -14,15 +14,18 @@ "data": { "flow_type": "Verbindungstyp" }, - "description": "Details" + "description": "Details", + "title": "Plugwise Typ" }, "user_gateway": { "data": { "host": "IP-Adresse", "password": "Smile ID", - "port": "Port" + "port": "Port", + "username": "Smile-Benutzername" }, - "description": "Bitte eingeben" + "description": "Bitte eingeben", + "title": "Stellen Sie eine Verbindung zu Smile her" } } }, @@ -31,7 +34,8 @@ "init": { "data": { "scan_interval": "Scanintervall (Sekunden)" - } + }, + "description": "Plugwise-Optionen einstellen" } } } diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 7f2f20e194745..3ee5551fd834c 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -8,7 +8,7 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 8ea77f78104a2..029b334f7c45b 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -8,7 +8,7 @@ "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu viga" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 18851d055a52a..316d733121bfa 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -8,7 +8,7 @@ "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index af77f6f15e14d..160cf182d3de2 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -8,7 +8,7 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "Glimlach: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index f8e128a0d270b..58ee9d4aed845 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -8,7 +8,7 @@ "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 0e6d3b16d4fce..5d0bdd0f4e6fe 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -8,7 +8,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index f027ebfc77247..c7b28d5b4159a 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -8,7 +8,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index 62e186ab61ac3..edd67dd41cb58 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -8,7 +8,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "Smile : {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index aeabe8634f859..ab370f5373179 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -17,14 +17,17 @@ _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, ) @@ -76,5 +79,5 @@ def cleanup(event): """Clean up resources.""" plum.cleanup() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) + entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)) return True diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index 40432810cc5fa..64c424ae74bf4 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from aiohttp import ContentTypeError from requests.exceptions import ConnectTimeout, HTTPError @@ -10,6 +9,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -35,9 +35,7 @@ def _show_form(self, errors=None): errors=errors or {}, ) - async def async_step_user( - self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: """Handle a flow initialized by the user or redirected to by import.""" if not user_input: return self._show_form() @@ -59,8 +57,6 @@ async def async_step_user( title=username, data={CONF_USERNAME: username, CONF_PASSWORD: password} ) - async def async_step_import( - self, import_config: ConfigType | None - ) -> dict[str, Any]: + async def async_step_import(self, import_config: ConfigType | None) -> FlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 90558eb252380..f358d81dfefd5 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from typing import Callable from plumlightpad import Plum @@ -16,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from .const import DOMAIN @@ -25,7 +24,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Plum Lightpad dimmer lights and glow rings.""" diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index ed9bb9c2eb447..366f770ca3b9e 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -2,12 +2,8 @@ "domain": "plum_lightpad", "name": "Plum Lightpad", "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", - "requirements": [ - "plumlightpad==0.0.11" - ], - "codeowners": [ - "@ColinHarrington", - "@prystupa" - ], - "config_flow": true + "requirements": ["plumlightpad==0.0.11"], + "codeowners": ["@ColinHarrington", "@prystupa"], + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index ad95609bd9f56..a2070daedd7c1 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -3,5 +3,6 @@ "name": "Pocket Casts", "documentation": "https://www.home-assistant.io/integrations/pocketcasts", "requirements": ["pycketcasts==1.0.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index e5c209004de14..45f58949e77df 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -13,14 +13,14 @@ CONF_TOKEN, CONF_WEBHOOK_ID, ) -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp from . import config_flow @@ -74,7 +74,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Point from a config entry.""" async def token_saver(token, **kwargs): @@ -107,7 +107,7 @@ async def token_saver(token, **kwargs): return True -async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, session): +async def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry, session): """Set up a webhook to handle binary sensor events.""" if CONF_WEBHOOK_ID not in entry.data: webhook_id = hass.components.webhook.async_generate_id() @@ -133,19 +133,17 @@ async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, sessi ) -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) session = hass.data[DOMAIN].pop(entry.entry_id) await session.remove_webhook() - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, platform) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - return True + return unload_ok async def handle_webhook(hass, webhook_id, request): @@ -165,7 +163,7 @@ async def handle_webhook(hass, webhook_id, request): class MinutPointClient: """Get the latest data and update the states.""" - def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry, session): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, session): """Initialize the Minut data object.""" self._known_devices = set() self._known_homes = set() @@ -309,7 +307,9 @@ def device_info(self): """Return a device description for device registry.""" device = self.device.device return { - "connections": {("mac", device["device_mac"])}, + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, device["device_mac"]) + }, "identifieres": device["device_id"], "manufacturer": "Minut", "model": f"Point v{device['hardware_version']}", diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index 1f3cf2a751d08..3b9ba84fab55b 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -40,12 +40,10 @@ def register_flow_implementation(hass, domain, client_id, client_secret): } -@config_entries.HANDLERS.register(DOMAIN) -class PointFlowHandler(config_entries.ConfigFlow): +class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize flow.""" @@ -53,7 +51,7 @@ def __init__(self): async def async_step_import(self, user_input=None): """Handle external yaml configuration.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") self.flow_impl = DOMAIN @@ -64,7 +62,7 @@ async def async_step_user(self, user_input=None): """Handle a flow start.""" flows = self.hass.data.get(DATA_FLOW_IMPL, {}) - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") if not flows: @@ -86,7 +84,7 @@ async def async_step_user(self, user_input=None): async def async_step_auth(self, user_input=None): """Create an entry for auth.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="external_setup") errors = {} @@ -125,7 +123,7 @@ async def _get_authorization_url(self): async def async_step_code(self, code=None): """Received code for authentication.""" - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="already_setup") if code is None: diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 899e5615b4061..fffb1b07f25e0 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pypoint==2.1.0"], "dependencies": ["webhook", "http"], "codeowners": ["@fredrike"], - "quality_scale": "gold" + "quality_scale": "gold", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/point/translations/bg.json b/homeassistant/components/point/translations/bg.json index 9a52ca8ad5201..4356554e7f83c 100644 --- a/homeassistant/components/point/translations/bg.json +++ b/homeassistant/components/point/translations/bg.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Point \u0430\u043a\u0430\u0443\u043d\u0442.", - "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", "external_setup": "Point \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043e\u0442 \u0434\u0440\u0443\u0433 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u043f\u0440\u043e\u0446\u0435\u0441.", "no_flows": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Point, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0441\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0442\u0435. [\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438\u0442\u0435](https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/ca.json b/homeassistant/components/point/translations/ca.json index 158c6addadea9..39269e3740dd6 100644 --- a/homeassistant/components/point/translations/ca.json +++ b/homeassistant/components/point/translations/ca.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", - "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "external_setup": "Point s'ha configurat correctament des d'un altre flux de dades.", "no_flows": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", diff --git a/homeassistant/components/point/translations/cs.json b/homeassistant/components/point/translations/cs.json index 6dedba8af11f6..d5e0e7c08b793 100644 --- a/homeassistant/components/point/translations/cs.json +++ b/homeassistant/components/point/translations/cs.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", - "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "external_setup": "Point \u00fasp\u011b\u0161n\u011b nastaveno jin\u00fdm zp\u016fsobem.", "no_flows": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", diff --git a/homeassistant/components/point/translations/da.json b/homeassistant/components/point/translations/da.json index 4530705a8b25c..80bdf84dedf32 100644 --- a/homeassistant/components/point/translations/da.json +++ b/homeassistant/components/point/translations/da.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Du kan kun konfigurere en enkelt Point konto.", - "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", "authorize_url_timeout": "Timeout ved generering af autoriseret url.", "external_setup": "Point er konfigureret med succes fra et andet flow.", "no_flows": "Du skal konfigurere Point f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index c36c7a44b51aa..41a8eb4344fac 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", - "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "external_setup": "Pointt erfolgreich von einem anderen Flow konfiguriert.", "no_flows": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", diff --git a/homeassistant/components/point/translations/en.json b/homeassistant/components/point/translations/en.json index 685a16cbbf5c8..c41dd93683fed 100644 --- a/homeassistant/components/point/translations/en.json +++ b/homeassistant/components/point/translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Already configured. Only a single configuration possible.", - "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize URL.", "external_setup": "Point successfully configured from another flow.", "no_flows": "The component is not configured. Please follow the documentation.", diff --git a/homeassistant/components/point/translations/es-419.json b/homeassistant/components/point/translations/es-419.json index 2b177e2682556..c6337dbd8ca3f 100644 --- a/homeassistant/components/point/translations/es-419.json +++ b/homeassistant/components/point/translations/es-419.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Solo puede configurar una cuenta Point.", - "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "external_setup": "Punto configurado con \u00e9xito desde otro flujo.", "no_flows": "Debe configurar Point antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index 51c334e794fef..c495a4fe3bd31 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "external_setup": "Point se ha configurado correctamente a partir de otro flujo.", "no_flows": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", diff --git a/homeassistant/components/point/translations/et.json b/homeassistant/components/point/translations/et.json index 7317e2cd3e366..7fa9a466e23a9 100644 --- a/homeassistant/components/point/translations/et.json +++ b/homeassistant/components/point/translations/et.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", - "authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.", "authorize_url_timeout": "Tuvastamise URL'i loomise ajal\u00f5pp.", "external_setup": "Point on teisest voost edukalt seadistatud.", "no_flows": "Osis pole seadistatud. Palun vaata dokumentatsiooni.", diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index ab9cd7af34e8b..0d05e0a5363be 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "external_setup": "Point correctement configur\u00e9 \u00e0 partir d\u2019un autre flux.", "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", diff --git a/homeassistant/components/point/translations/hu.json b/homeassistant/components/point/translations/hu.json index 7f4346a6ea9e6..1fc46d0c19b1c 100644 --- a/homeassistant/components/point/translations/hu.json +++ b/homeassistant/components/point/translations/hu.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." diff --git a/homeassistant/components/point/translations/id.json b/homeassistant/components/point/translations/id.json index 868321d74693f..854a67449a03c 100644 --- a/homeassistant/components/point/translations/id.json +++ b/homeassistant/components/point/translations/id.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", - "authorize_url_fail": "Kesalahan tidak dikenal terjadi ketika menghasilkan URL otorisasi.", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", "external_setup": "Point berhasil dikonfigurasi dari alur konfigurasi lainnya.", "no_flows": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", diff --git a/homeassistant/components/point/translations/it.json b/homeassistant/components/point/translations/it.json index 49eb2a760a408..76aae0f44037e 100644 --- a/homeassistant/components/point/translations/it.json +++ b/homeassistant/components/point/translations/it.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", - "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "external_setup": "Point configurato correttamente da un altro flusso.", "no_flows": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", diff --git a/homeassistant/components/point/translations/ko.json b/homeassistant/components/point/translations/ko.json index 5813dbba13726..c88c32906b8c8 100644 --- a/homeassistant/components/point/translations/ko.json +++ b/homeassistant/components/point/translations/ko.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "authorize_url_fail": "\uc778\uc99d URL\uc744 \uc0dd\uc131\ud558\ub294 \ub3d9\uc548 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "external_setup": "Point\uac00 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/point/translations/lb.json b/homeassistant/components/point/translations/lb.json index ccdc9db4fffcd..4fe8d86b796ae 100644 --- a/homeassistant/components/point/translations/lb.json +++ b/homeassistant/components/point/translations/lb.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech.", - "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "external_setup": "Point gouf vun engem anere Floss erfollegr\u00e4ich konfigur\u00e9iert.", "no_flows": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." diff --git a/homeassistant/components/point/translations/nl.json b/homeassistant/components/point/translations/nl.json index 8447ac6bbb2c3..37dae8481eb99 100644 --- a/homeassistant/components/point/translations/nl.json +++ b/homeassistant/components/point/translations/nl.json @@ -2,10 +2,9 @@ "config": { "abort": { "already_setup": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", - "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "external_setup": "Punt succesvol geconfigureerd vanuit een andere stroom.", - "no_flows": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_flows": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "create_entry": { diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json index a72a8083f6fb1..912c068340166 100644 --- a/homeassistant/components/point/translations/no.json +++ b/homeassistant/components/point/translations/no.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", - "authorize_url_fail": "Ukjent feil under generering av en autoriserings-URL.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "external_setup": "Punktet er konfigurert fra en annen flyt.", "no_flows": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", diff --git a/homeassistant/components/point/translations/pl.json b/homeassistant/components/point/translations/pl.json index 66b81d5675e63..56c65d52d1123 100644 --- a/homeassistant/components/point/translations/pl.json +++ b/homeassistant/components/point/translations/pl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", - "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "external_setup": "Punkt pomy\u015blnie skonfigurowany", "no_flows": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", diff --git a/homeassistant/components/point/translations/pt-BR.json b/homeassistant/components/point/translations/pt-BR.json index 5816f1a0bcd48..c9384d82c38ae 100644 --- a/homeassistant/components/point/translations/pt-BR.json +++ b/homeassistant/components/point/translations/pt-BR.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Voc\u00ea s\u00f3 pode configurar uma conta Point.", - "authorize_url_fail": "Erro desconhecido ao gerar URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Excedido tempo limite gerando a URL de autoriza\u00e7\u00e3o.", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", "no_flows": "Voc\u00ea precisa configurar o Point antes de ser capaz de autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es](https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/pt.json b/homeassistant/components/point/translations/pt.json index 401e10c256a09..3af925087622d 100644 --- a/homeassistant/components/point/translations/pt.json +++ b/homeassistant/components/point/translations/pt.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "S\u00f3 pode configurar uma \u00fanica conta Point.", - "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/).", diff --git a/homeassistant/components/point/translations/ru.json b/homeassistant/components/point/translations/ru.json index abb902408716b..1345877d54a03 100644 --- a/homeassistant/components/point/translations/ru.json +++ b/homeassistant/components/point/translations/ru.json @@ -2,10 +2,9 @@ "config": { "abort": { "already_setup": "\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.", - "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \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.", "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.", "external_setup": "Point \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", - "no_flows": "\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.", + "no_flows": "\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.", "unknown_authorize_url_generation": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \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." }, "create_entry": { diff --git a/homeassistant/components/point/translations/sl.json b/homeassistant/components/point/translations/sl.json index 3c928935ccee3..009ea51370e6a 100644 --- a/homeassistant/components/point/translations/sl.json +++ b/homeassistant/components/point/translations/sl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Nastavite lahko samo en ra\u010dun Point.", - "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", "external_setup": "To\u010dka uspe\u0161no konfigurirana iz drugega toka.", "no_flows": "Preden lahko preverite pristnost, morate konfigurirati Point. [Preberite navodila](https://www.home-assistant.io/components/point/).", diff --git a/homeassistant/components/point/translations/sv.json b/homeassistant/components/point/translations/sv.json index f7050c2c25579..51627c78f3d16 100644 --- a/homeassistant/components/point/translations/sv.json +++ b/homeassistant/components/point/translations/sv.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "Du kan endast konfigurera ett Point-konto.", - "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r f\u00f6rs\u00f6ker generera en url f\u00f6r auktorisering.", "authorize_url_timeout": "Timeout n\u00e4r genererar url f\u00f6r auktorisering.", "external_setup": "Point har lyckats med konfigurering ifr\u00e5n ett annat fl\u00f6de.", "no_flows": "Du beh\u00f6ver konfigurera Point innan de kan autentisera med den. [L\u00e4s instruktioner](https://www.home-assistant.io/components/point/)." diff --git a/homeassistant/components/point/translations/uk.json b/homeassistant/components/point/translations/uk.json index 6b66a39a29176..798f76e4f6b30 100644 --- a/homeassistant/components/point/translations/uk.json +++ b/homeassistant/components/point/translations/uk.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", - "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "external_setup": "Point \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.", "no_flows": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", diff --git a/homeassistant/components/point/translations/zh-Hans.json b/homeassistant/components/point/translations/zh-Hans.json index ecefc3b656c5d..09f66edf32398 100644 --- a/homeassistant/components/point/translations/zh-Hans.json +++ b/homeassistant/components/point/translations/zh-Hans.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\u60a8\u53ea\u80fd\u914d\u7f6e\u4e00\u4e2a Point \u5e10\u6237\u3002", - "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", "external_setup": "Point\u914d\u7f6e\u6210\u529f\u3002", "no_flows": "\u60a8\u9700\u8981\u5148\u914d\u7f6e Point\uff0c\u7136\u540e\u624d\u80fd\u5bf9\u5176\u8fdb\u884c\u6388\u6743\u3002 [\u8bf7\u9605\u8bfb\u8bf4\u660e](https://www.home-assistant.io/components/point/)\u3002" diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index 2bb1a8fc23901..3f9df05d69710 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", - "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", "no_flows": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index cfc2abb031642..89e340ee95ed5 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -1,5 +1,4 @@ """The PoolSense integration.""" -import asyncio from datetime import timedelta import logging @@ -25,13 +24,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the PoolSense component.""" - # Make sure coordinator is initialized. - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up PoolSense from a config entry.""" @@ -50,30 +42,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/poolsense/config_flow.py b/homeassistant/components/poolsense/config_flow.py index 653ba026ebf65..7ab9691d134a9 100644 --- a/homeassistant/components/poolsense/config_flow.py +++ b/homeassistant/components/poolsense/config_flow.py @@ -17,7 +17,6 @@ class PoolSenseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for PoolSense.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize PoolSense config flow.""" diff --git a/homeassistant/components/poolsense/manifest.json b/homeassistant/components/poolsense/manifest.json index 9eebadf2da066..697afd541063a 100644 --- a/homeassistant/components/poolsense/manifest.json +++ b/homeassistant/components/poolsense/manifest.json @@ -3,10 +3,7 @@ "name": "PoolSense", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/poolsense", - "requirements": [ - "poolsense==0.0.8" - ], - "codeowners": [ - "@haemishkyd" - ] + "requirements": ["poolsense==0.0.8"], + "codeowners": ["@haemishkyd"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index 6b64ec2eef1a4..dc569c2d9ad8d 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -12,7 +12,8 @@ "email": "E-Mail", "password": "Passwort" }, - "description": "M\u00f6chten Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chten Sie mit der Einrichtung beginnen?", + "title": "" } } } diff --git a/homeassistant/components/poolsense/translations/zh-Hant.json b/homeassistant/components/poolsense/translations/zh-Hant.json index 93a99ba1d317c..62ffca35e9fda 100644 --- a/homeassistant/components/poolsense/translations/zh-Hant.json +++ b/homeassistant/components/poolsense/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index ceec56aa05a94..0f63bf9798658 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -1,5 +1,4 @@ """The Tesla Powerwall integration.""" -import asyncio from datetime import timedelta import logging @@ -11,10 +10,10 @@ PowerwallUnreachableError, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -43,13 +42,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Tesla Powerwall component.""" - hass.data.setdefault(DOMAIN, {}) - - return True - - async def _migrate_old_unique_ids(hass, entry_id, powerwall_data): serial_numbers = powerwall_data[POWERWALL_API_SERIAL_NUMBERS] site_info = powerwall_data[POWERWALL_API_SITE_INFO] @@ -96,6 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_id = entry.entry_id + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(entry_id, {}) http_session = requests.Session() @@ -115,8 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except AccessDeniedError as err: _LOGGER.debug("Authentication failed", exc_info=err) http_session.close() - _async_start_reauth(hass, entry) - return False + raise ConfigEntryAuthFailed from err await _migrate_old_unique_ids(hass, entry_id, powerwall_data) @@ -130,13 +122,16 @@ async def async_update_data(): _LOGGER.debug("Updating data") try: return await _async_update_powerwall_data(hass, entry, power_wall) - except AccessDeniedError: + except AccessDeniedError as err: if password is None: - raise + raise ConfigEntryAuthFailed from err # If the session expired, relogin, and try again - await hass.async_add_executor_job(power_wall.login, "", password) - return await _async_update_powerwall_data(hass, entry, power_wall) + try: + await hass.async_add_executor_job(power_wall.login, "", password) + return await _async_update_powerwall_data(hass, entry, power_wall) + except AccessDeniedError as ex: + raise ConfigEntryAuthFailed from ex coordinator = DataUpdateCoordinator( hass, @@ -158,10 +153,7 @@ async def async_update_data(): await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -181,21 +173,10 @@ async def _async_update_powerwall_data( return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data -def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error("Password is no longer valid. Please reauthenticate") - - def _login_and_fetch_base_info(power_wall: Powerwall, password: str): """Login to the powerwall and fetch the base info.""" if password is not None: - power_wall.login("", password) + power_wall.login(password) power_wall.detect_and_pin_version() return call_base_info(power_wall) @@ -225,14 +206,7 @@ def _fetch_powerwall_data(power_wall): 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][POWERWALL_HTTP_SESSION].close() diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 579c916a15ada..420212a86ba50 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -12,7 +12,6 @@ from homeassistant import config_entries, core, exceptions from homeassistant.components.dhcp import IP_ADDRESS from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD -from homeassistant.core import callback from .const import DOMAIN @@ -22,7 +21,7 @@ def _login_and_fetch_site_info(power_wall: Powerwall, password: str): """Login to the powerwall and fetch the base info.""" if password is not None: - power_wall.login("", password) + power_wall.login(password) power_wall.detect_and_pin_version() return power_wall.get_site_info() @@ -53,18 +52,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Tesla Powerwall.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the powerwall flow.""" self.ip_address = None - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._async_ip_address_already_configured(dhcp_discovery[IP_ADDRESS]): - return self.async_abort(reason="already_configured") - - self.ip_address = dhcp_discovery[IP_ADDRESS] + self.ip_address = discovery_info[IP_ADDRESS] + self._async_abort_entries_match({CONF_IP_ADDRESS: self.ip_address}) + self.ip_address = discovery_info[IP_ADDRESS] self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} return await self.async_step_user() @@ -112,14 +109,6 @@ async def async_step_reauth(self, data): self.ip_address = data[CONF_IP_ADDRESS] return await self.async_step_user() - @callback - def _async_ip_address_already_configured(self, ip_address): - """See if we already have an entry matching the ip_address.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_IP_ADDRESS) == ip_address: - return True - return False - class WrongVersion(exceptions.HomeAssistantError): """Error to indicate the powerwall uses a software version we cannot interact with.""" diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index 6dd4558a98ccb..f338d5f981d37 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -12,6 +12,7 @@ ATTR_ENERGY_EXPORTED = "energy_exported_(in_kW)" ATTR_ENERGY_IMPORTED = "energy_imported_(in_kW)" ATTR_INSTANT_AVERAGE_VOLTAGE = "instant_average_voltage" +ATTR_INSTANT_TOTAL_CURRENT = "instant_total_current" ATTR_IS_ACTIVE = "is_active" STATUS_VERSION = "version" diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 40d0a6c50fe11..5cee6c1fd19cb 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,10 +3,17 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.3.5"], + "requirements": ["tesla-powerwall==0.3.10"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ - {"hostname":"1118431-*","macaddress":"88DA1A*"}, - {"hostname":"1118431-*","macaddress":"000145*"} - ] + { + "hostname": "1118431-*", + "macaddress": "88DA1A*" + }, + { + "hostname": "1118431-*", + "macaddress": "000145*" + } + ], + "iot_class": "local_polling" } diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 36f803e66d7a2..982952a4830d4 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -11,6 +11,7 @@ ATTR_ENERGY_IMPORTED, ATTR_FREQUENCY, ATTR_INSTANT_AVERAGE_VOLTAGE, + ATTR_INSTANT_TOTAL_CURRENT, ATTR_IS_ACTIVE, DOMAIN, ENERGY_KILO_WATT, @@ -144,6 +145,7 @@ def extra_state_attributes(self): ATTR_FREQUENCY: round(meter.frequency, 1), ATTR_ENERGY_EXPORTED: meter.get_energy_exported(), ATTR_ENERGY_IMPORTED: meter.get_energy_imported(), - ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.avarage_voltage, 1), + ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.average_voltage, 1), + ATTR_INSTANT_TOTAL_CURRENT: meter.get_instant_total_current(), ATTR_IS_ACTIVE: meter.is_active(), } diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index 5deacd6a8f9d1..e1b2f2dbd3bec 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "title": "Connect to the powerwall", diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json index 8016cd123710a..c8020069676a6 100644 --- a/homeassistant/components/powerwall/translations/ca.json +++ b/homeassistant/components/powerwall/translations/ca.json @@ -10,7 +10,7 @@ "unknown": "Error inesperat", "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index 0ccd42c812ba1..c916152637370 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "wrong_version": "Deine Powerwall verwendet eine Softwareversion, die nicht unterst\u00fctzt wird. Bitte ziehe ein Upgrade in Betracht oder melde dieses Problem, damit es behoben werden kann." }, "flow_title": "Tesla Powerwall ({ip_address})", "step": { diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index 06fc09804d92c..3be711d94c548 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -10,7 +10,7 @@ "unknown": "Unexpected error", "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 81e3edab38772..f2beb19d5dac9 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El powerwall ya est\u00e1 configurado" + "already_configured": "El powerwall ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado", "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Considera actualizar o informar de este problema para que pueda resolverse." }, @@ -12,7 +14,8 @@ "step": { "user": { "data": { - "ip_address": "Direcci\u00f3n IP" + "ip_address": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a" }, "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie del Backup Gateway y se puede encontrar en la aplicaci\u00f3n Telsa; o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.", "title": "Conectarse al powerwall" diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json index 8811b87031699..98eb25ca17af6 100644 --- a/homeassistant/components/powerwall/translations/et.json +++ b/homeassistant/components/powerwall/translations/et.json @@ -10,7 +10,7 @@ "unknown": "Ootamatu t\u00f5rge", "wrong_version": "Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud." }, - "flow_title": "Tesla Powerwall ( {ip_address} )", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json index 48cd7c04743df..85edd1656f8ab 100644 --- a/homeassistant/components/powerwall/translations/it.json +++ b/homeassistant/components/powerwall/translations/it.json @@ -10,7 +10,7 @@ "unknown": "Errore imprevisto", "wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Si prega di considerare l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json index 5149f39166938..87b78e719d0e2 100644 --- a/homeassistant/components/powerwall/translations/nl.json +++ b/homeassistant/components/powerwall/translations/nl.json @@ -10,7 +10,7 @@ "unknown": "Onverwachte fout", "wrong_version": "Uw powerwall gebruikt een softwareversie die niet wordt ondersteund. Overweeg om dit probleem te upgraden of te melden, zodat het kan worden opgelost." }, - "flow_title": "Tesla Powerwall ({ip_adres})", + "flow_title": "({ip_adres})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json index 00b77e1456611..6f45fb144f53b 100644 --- a/homeassistant/components/powerwall/translations/no.json +++ b/homeassistant/components/powerwall/translations/no.json @@ -10,7 +10,7 @@ "unknown": "Uventet feil", "wrong_version": "Powerwall bruker en programvareversjon som ikke st\u00f8ttes. Vennligst vurder \u00e5 oppgradere eller rapportere dette problemet, s\u00e5 det kan l\u00f8ses." }, - "flow_title": "", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index 272f28df3b99b..8d4f82fa14eab 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -10,7 +10,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d", "wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107." }, - "flow_title": "Tesla UPS ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/ru.json b/homeassistant/components/powerwall/translations/ru.json index f79b62c2c78dc..f8299a59445aa 100644 --- a/homeassistant/components/powerwall/translations/ru.json +++ b/homeassistant/components/powerwall/translations/ru.json @@ -10,7 +10,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "wrong_version": "\u0412\u0430\u0448 powerwall \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435, \u0447\u0442\u043e\u0431\u044b \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0435\u0448\u0438\u0442\u044c." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index 44e79e935cdf1..21a2a4f215953 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { @@ -10,7 +10,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4", "wrong_version": "\u4e0d\u652f\u63f4\u60a8\u6240\u4f7f\u7528\u7684 Powerwall \u7248\u672c\u3002\u8acb\u8003\u616e\u9032\u884c\u5347\u7d1a\u6216\u56de\u5831\u6b64\u554f\u984c\u3001\u4ee5\u671f\u554f\u984c\u53ef\u4ee5\u7372\u5f97\u89e3\u6c7a\u3002" }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index c8f6c9fd1a22a..e6bc68ba91895 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -3,7 +3,11 @@ import cProfile from datetime import timedelta import logging +import reprlib +import sys +import threading import time +import traceback from guppy import hpy import objgraph @@ -11,11 +15,11 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TYPE from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -24,6 +28,9 @@ SERVICE_START_LOG_OBJECTS = "start_log_objects" SERVICE_STOP_LOG_OBJECTS = "stop_log_objects" SERVICE_DUMP_LOG_OBJECTS = "dump_log_objects" +SERVICE_LOG_THREAD_FRAMES = "log_thread_frames" +SERVICE_LOG_EVENT_LOOP_SCHEDULED = "log_event_loop_scheduled" + SERVICES = ( SERVICE_START, @@ -31,27 +38,21 @@ SERVICE_START_LOG_OBJECTS, SERVICE_STOP_LOG_OBJECTS, SERVICE_DUMP_LOG_OBJECTS, + SERVICE_LOG_THREAD_FRAMES, + SERVICE_LOG_EVENT_LOOP_SCHEDULED, ) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) CONF_SECONDS = "seconds" -CONF_SCAN_INTERVAL = "scan_interval" -CONF_TYPE = "type" LOG_INTERVAL_SUB = "log_interval_subscription" _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the profiler component.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Profiler from a config entry.""" - lock = asyncio.Lock() domain_data = hass.data[DOMAIN] = {} @@ -99,6 +100,34 @@ def _dump_log_objects(call: ServiceCall): notification_id="profile_object_dump", ) + async def _async_dump_thread_frames(call: ServiceCall) -> None: + """Log all thread frames.""" + frames = sys._current_frames() # pylint: disable=protected-access + main_thread = threading.main_thread() + for thread in threading.enumerate(): + if thread == main_thread: + continue + _LOGGER.critical( + "Thread [%s]: %s", + thread.name, + "".join(traceback.format_stack(frames.get(thread.ident))).strip(), + ) + + async def _async_dump_scheduled(call: ServiceCall) -> None: + """Log all scheduled in the event loop.""" + arepr = reprlib.aRepr + original_maxstring = arepr.maxstring + original_maxother = arepr.maxother + arepr.maxstring = 300 + arepr.maxother = 300 + try: + for handle in hass.loop._scheduled: # pylint: disable=protected-access + if not handle.cancelled(): + _LOGGER.critical("Scheduled: %s", handle) + finally: + arepr.max_string = original_maxstring + arepr.max_other = original_maxother + async_register_admin_service( hass, DOMAIN, @@ -138,7 +167,6 @@ def _dump_log_objects(call: ServiceCall): DOMAIN, SERVICE_STOP_LOG_OBJECTS, _async_stop_log_objects, - schema=vol.Schema({}), ) async_register_admin_service( @@ -149,6 +177,20 @@ def _dump_log_objects(call: ServiceCall): schema=vol.Schema({vol.Required(CONF_TYPE): str}), ) + async_register_admin_service( + hass, + DOMAIN, + SERVICE_LOG_THREAD_FRAMES, + _async_dump_thread_frames, + ) + + async_register_admin_service( + hass, + DOMAIN, + SERVICE_LOG_EVENT_LOOP_SCHEDULED, + _async_dump_scheduled, + ) + return True diff --git a/homeassistant/components/profiler/config_flow.py b/homeassistant/components/profiler/config_flow.py index 259c300239cf2..b63246ce386eb 100644 --- a/homeassistant/components/profiler/config_flow.py +++ b/homeassistant/components/profiler/config_flow.py @@ -10,7 +10,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Profiler.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml index f0b04e9e00239..ff634e02ac54c 100644 --- a/homeassistant/components/profiler/services.yaml +++ b/homeassistant/components/profiler/services.yaml @@ -1,26 +1,62 @@ start: + name: Start description: Start the Profiler fields: seconds: + name: Seconds description: The number of seconds to run the profiler. example: 60.0 + default: 60.0 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds memory: + name: Memory description: Start the Memory Profiler fields: seconds: + name: Seconds description: The number of seconds to run the memory profiler. example: 60.0 + default: 60.0 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds start_log_objects: + name: Start log objects description: Start logging growth of objects in memory fields: scan_interval: + name: Scan interval description: The number of seconds between logging objects. example: 60.0 + default: 30.0 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds stop_log_objects: - description: Stop logging growth of objects in memory + name: Stop log objects + description: Stop logging growth of objects in memory. dump_log_objects: + name: Dump log objects description: Dump the repr of all matching objects to the log. fields: type: - description: The type of objects to dump to the log + name: Type + description: The type of objects to dump to the log. + required: true example: State + selector: + text: +log_thread_frames: + name: Log thread frames + description: Log the current frames for all threads. +log_event_loop_scheduled: + name: Log event loop scheduled + description: Log what is scheduled in the event loop. diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index 7597b2ff1a292..78ea16bb26c21 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -1,5 +1,4 @@ """Automation manager for boards manufactured by ProgettiHWSW Italy.""" -import asyncio from ProgettiHWSW.ProgettiHWSWAPI import ProgettiHWSWAPI from ProgettiHWSW.input import Input @@ -13,16 +12,9 @@ PLATFORMS = ["switch", "binary_sensor"] -async def async_setup(hass, config): - """Set up the ProgettiHWSW Automation component.""" - hass.data[DOMAIN] = {} - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up ProgettiHWSW Automation from a config entry.""" - + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = ProgettiHWSWAPI( f'{entry.data["host"]}:{entry.data["port"]}' ) @@ -30,24 +22,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Check board validation again to load new values to API. await hass.data[DOMAIN][entry.entry_id].check_board() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py index dca29668a8495..89d5916a3fd93 100644 --- a/homeassistant/components/progettihwsw/config_flow.py +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -15,17 +15,6 @@ async def validate_input(hass: core.HomeAssistant, data): """Validate the user host input.""" - confs = hass.config_entries.async_entries(DOMAIN) - same_entries = [ - True - for entry in confs - if entry.data.get("host") == data["host"] - and entry.data.get("port") == data["port"] - ] - - if same_entries: - raise ExistingEntry - api_instance = ProgettiHWSWAPI(f'{data["host"]}:{data["port"]}') is_valid = await api_instance.check_board() @@ -44,7 +33,6 @@ class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for ProgettiHWSW Automation.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize class variables.""" @@ -81,13 +69,14 @@ async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: + self._async_abort_entries_match( + {"host": user_input["host"], "port": user_input["port"]} + ) try: info = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" - except ExistingEntry: - return self.async_abort(reason="already_configured") except Exception: # pylint: disable=broad-except errors["base"] = "unknown" else: diff --git a/homeassistant/components/progettihwsw/manifest.json b/homeassistant/components/progettihwsw/manifest.json index 15987837fb5b0..d1dbb30f2fcb2 100644 --- a/homeassistant/components/progettihwsw/manifest.json +++ b/homeassistant/components/progettihwsw/manifest.json @@ -2,11 +2,8 @@ "domain": "progettihwsw", "name": "ProgettiHWSW Automation", "documentation": "https://www.home-assistant.io/integrations/progettihwsw", - "codeowners": [ - "@ardaseremet" - ], - "requirements": [ - "progettihwsw==0.1.1" - ], - "config_flow": true -} \ No newline at end of file + "codeowners": ["@ardaseremet"], + "requirements": ["progettihwsw==0.1.1"], + "config_flow": true, + "iot_class": "local_polling" +} diff --git a/homeassistant/components/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json index 0f773e03c1ded..0d9823587a216 100644 --- a/homeassistant/components/progettihwsw/translations/de.json +++ b/homeassistant/components/progettihwsw/translations/de.json @@ -26,13 +26,15 @@ "relay_7": "Relais 7", "relay_8": "Relais 8", "relay_9": "Relais 9" - } + }, + "title": "Relais einrichten" }, "user": { "data": { "host": "Host", "port": "Port" - } + }, + "title": "Board einrichten" } } } diff --git a/homeassistant/components/progettihwsw/translations/zh-Hant.json b/homeassistant/components/progettihwsw/translations/zh-Hant.json index 815ee581e69a3..040c3dff1d77b 100644 --- a/homeassistant/components/progettihwsw/translations/zh-Hant.json +++ b/homeassistant/components/progettihwsw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/proliphix/manifest.json b/homeassistant/components/proliphix/manifest.json index eb0b6e1b85764..e5f2fc056dc3a 100644 --- a/homeassistant/components/proliphix/manifest.json +++ b/homeassistant/components/proliphix/manifest.json @@ -3,5 +3,6 @@ "name": "Proliphix", "documentation": "https://www.home-assistant.io/integrations/proliphix", "requirements": ["proliphix==0.4.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index 9b4df619fb5af..9315bf308b7a8 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/prometheus", "requirements": ["prometheus_client==0.7.1"], "dependencies": ["http"], - "codeowners": ["@knyar"] + "codeowners": ["@knyar"], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/prowl/manifest.json b/homeassistant/components/prowl/manifest.json index 10bb7f8948e10..223d6f28865a7 100644 --- a/homeassistant/components/prowl/manifest.json +++ b/homeassistant/components/prowl/manifest.json @@ -2,5 +2,6 @@ "domain": "prowl", "name": "Prowl", "documentation": "https://www.home-assistant.io/integrations/prowl", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/proximity/manifest.json b/homeassistant/components/proximity/manifest.json index a93da5f72d01c..edc1f15254143 100644 --- a/homeassistant/components/proximity/manifest.json +++ b/homeassistant/components/proximity/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/proximity", "dependencies": ["device_tracker", "zone"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "calculated" } diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index a149c8b6034fd..5777bb3054c4e 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -5,7 +5,8 @@ from proxmoxer import ProxmoxAPI from proxmoxer.backends.https import AuthenticationError from proxmoxer.core import ResourceException -from requests.exceptions import SSLError +import requests.exceptions +from requests.exceptions import ConnectTimeout, SSLError import voluptuous as vol from homeassistant.const import ( @@ -31,7 +32,7 @@ CONF_VMS = "vms" CONF_CONTAINERS = "containers" -COORDINATOR = "coordinator" +COORDINATORS = "coordinators" API_DATA = "api_data" DEFAULT_PORT = 8006 @@ -90,6 +91,7 @@ async def async_setup(hass: HomeAssistant, config: dict): def build_client() -> ProxmoxAPI: """Build the Proxmox client connection.""" hass.data[PROXMOX_CLIENTS] = {} + for entry in config[DOMAIN]: host = entry[CONF_HOST] port = entry[CONF_PORT] @@ -98,6 +100,8 @@ def build_client() -> ProxmoxAPI: password = entry[CONF_PASSWORD] verify_ssl = entry[CONF_VERIFY_SSL] + hass.data[PROXMOX_CLIENTS][host] = None + try: # Construct an API client with the given data for the given host proxmox_client = ProxmoxClient( @@ -111,91 +115,100 @@ def build_client() -> ProxmoxAPI: continue except SSLError: _LOGGER.error( - 'Unable to verify proxmox server SSL. Try using "verify_ssl: false"' + "Unable to verify proxmox server SSL. " + 'Try using "verify_ssl: false" for proxmox instance %s:%d', + host, + port, ) continue + except ConnectTimeout: + _LOGGER.warning("Connection to host %s timed out during setup", host) + continue + + hass.data[PROXMOX_CLIENTS][host] = proxmox_client - return proxmox_client + await hass.async_add_executor_job(build_client) - proxmox_client = await hass.async_add_executor_job(build_client) + coordinators = hass.data[DOMAIN][COORDINATORS] = {} - async def async_update_data() -> dict: - """Fetch data from API endpoint.""" + # Create a coordinator for each vm/container + for host_config in config[DOMAIN]: + host_name = host_config["host"] + coordinators[host_name] = {} + + proxmox_client = hass.data[PROXMOX_CLIENTS][host_name] + + # Skip invalid hosts + if proxmox_client is None: + continue proxmox = proxmox_client.get_api_client() - def poll_api() -> dict: - data = {} + for node_config in host_config["nodes"]: + node_name = node_config["node"] + node_coordinators = coordinators[host_name][node_name] = {} - for host_config in config[DOMAIN]: - host_name = host_config["host"] + for vm_id in node_config["vms"]: + coordinator = create_coordinator_container_vm( + hass, proxmox, host_name, node_name, vm_id, TYPE_VM + ) - data[host_name] = {} + # Fetch initial data + await coordinator.async_refresh() - for node_config in host_config["nodes"]: - node_name = node_config["node"] - data[host_name][node_name] = {} + node_coordinators[vm_id] = coordinator - for vm_id in node_config["vms"]: - data[host_name][node_name][vm_id] = {} + for container_id in node_config["containers"]: + coordinator = create_coordinator_container_vm( + hass, proxmox, host_name, node_name, container_id, TYPE_CONTAINER + ) - vm_status = call_api_container_vm( - proxmox, node_name, vm_id, TYPE_VM - ) + # Fetch initial data + await coordinator.async_refresh() - if vm_status is None: - _LOGGER.warning("Vm/Container %s unable to be found", vm_id) - data[host_name][node_name][vm_id] = None - continue + node_coordinators[container_id] = coordinator - data[host_name][node_name][vm_id] = parse_api_container_vm( - vm_status - ) + for component in PLATFORMS: + await hass.async_create_task( + hass.helpers.discovery.async_load_platform( + component, DOMAIN, {"config": config}, config + ) + ) - for container_id in node_config["containers"]: - data[host_name][node_name][container_id] = {} + return True - container_status = call_api_container_vm( - proxmox, node_name, container_id, TYPE_CONTAINER - ) - if container_status is None: - _LOGGER.error( - "Vm/Container %s unable to be found", container_id - ) - data[host_name][node_name][container_id] = None - continue +def create_coordinator_container_vm( + hass, proxmox, host_name, node_name, vm_id, vm_type +): + """Create and return a DataUpdateCoordinator for a vm/container.""" - data[host_name][node_name][ - container_id - ] = parse_api_container_vm(container_status) + async def async_update_data(): + """Call the api and handle the response.""" - return data + def poll_api(): + """Call the api.""" + vm_status = call_api_container_vm(proxmox, node_name, vm_id, vm_type) + return vm_status - return await hass.async_add_executor_job(poll_api) + vm_status = await hass.async_add_executor_job(poll_api) - coordinator = DataUpdateCoordinator( + if vm_status is None: + _LOGGER.warning( + "Vm/Container %s unable to be found in node %s", vm_id, node_name + ) + return None + + return parse_api_container_vm(vm_status) + + return DataUpdateCoordinator( hass, _LOGGER, - name="proxmox_coordinator", + name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}", update_method=async_update_data, update_interval=timedelta(seconds=UPDATE_INTERVAL), ) - hass.data[DOMAIN][COORDINATOR] = coordinator - - # Fetch initial data - await coordinator.async_config_entry_first_refresh() - - for platform in PLATFORMS: - await hass.async_create_task( - hass.helpers.discovery.async_load_platform( - platform, DOMAIN, {"config": config}, config - ) - ) - - return True - def parse_api_container_vm(status): """Get the container or vm api data and return it formatted in a dictionary. @@ -216,7 +229,7 @@ def call_api_container_vm(proxmox, node_name, vm_id, machine_type): status = proxmox.nodes(node_name).qemu(vm_id).status.current.get() elif machine_type == TYPE_CONTAINER: status = proxmox.nodes(node_name).lxc(vm_id).status.current.get() - except ResourceException: + except (ResourceException, requests.exceptions.ConnectionError): return None return status diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 1151c2ec33299..fedb513e5b4e1 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -1,8 +1,9 @@ """Binary sensor to read Proxmox VE data.""" -from homeassistant.const import STATE_OFF, STATE_ON + +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import COORDINATOR, DOMAIN, ProxmoxEntity +from . import COORDINATORS, DOMAIN, PROXMOX_CLIENTS, ProxmoxEntity async def async_setup_platform(hass, config, add_entities, discovery_info=None): @@ -10,41 +11,45 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return - coordinator = hass.data[DOMAIN][COORDINATOR] - sensors = [] for host_config in discovery_info["config"][DOMAIN]: host_name = host_config["host"] + host_name_coordinators = hass.data[DOMAIN][COORDINATORS][host_name] + + if hass.data[PROXMOX_CLIENTS][host_name] is None: + continue for node_config in host_config["nodes"]: node_name = node_config["node"] for vm_id in node_config["vms"]: - coordinator_data = coordinator.data[host_name][node_name][vm_id] + coordinator = host_name_coordinators[node_name][vm_id] + coordinator_data = coordinator.data # unfound vm case if coordinator_data is None: continue vm_name = coordinator_data["name"] - vm_status = create_binary_sensor( + vm_sensor = create_binary_sensor( coordinator, host_name, node_name, vm_id, vm_name ) - sensors.append(vm_status) + sensors.append(vm_sensor) for container_id in node_config["containers"]: - coordinator_data = coordinator.data[host_name][node_name][container_id] + coordinator = host_name_coordinators[node_name][container_id] + coordinator_data = coordinator.data # unfound container case if coordinator_data is None: continue container_name = coordinator_data["name"] - container_status = create_binary_sensor( + container_sensor = create_binary_sensor( coordinator, host_name, node_name, container_id, container_name ) - sensors.append(container_status) + sensors.append(container_sensor) add_entities(sensors) @@ -62,7 +67,7 @@ def create_binary_sensor(coordinator, host_name, node_name, vm_id, name): ) -class ProxmoxBinarySensor(ProxmoxEntity): +class ProxmoxBinarySensor(ProxmoxEntity, BinarySensorEntity): """A binary sensor for reading Proxmox VE data.""" def __init__( @@ -80,12 +85,18 @@ def __init__( coordinator, unique_id, name, icon, host_name, node_name, vm_id ) - self._state = None - @property - def state(self): + def is_on(self): """Return the state of the binary sensor.""" - data = self.coordinator.data[self._host_name][self._node_name][self._vm_id] - if data["status"] == "running": - return STATE_ON - return STATE_OFF + data = self.coordinator.data + + if data is None: + return None + + return data["status"] == "running" + + @property + def available(self): + """Return sensor availability.""" + + return super().available and self.coordinator.data is not None diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index a47ce0a28eea3..bfea03e890210 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -2,6 +2,7 @@ "domain": "proxmoxve", "name": "Proxmox VE", "documentation": "https://www.home-assistant.io/integrations/proxmoxve", - "codeowners": ["@k4ds3", "@jhollowe"], - "requirements": ["proxmoxer==1.1.1"] + "codeowners": ["@k4ds3", "@jhollowe", "@Corbeno"], + "requirements": ["proxmoxer==1.1.1"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 11d271be543a0..bf5eafa7bbbe0 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -18,15 +18,21 @@ CONF_REGION, CONF_TOKEN, ) -from homeassistant.core import split_entity_id +from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import location from homeassistant.util.json import load_json, save_json from .config_flow import PlayStation4FlowHandler # noqa: F401 -from .const import ATTR_MEDIA_IMAGE_URL, COMMANDS, DOMAIN, GAMES_FILE, PS4_DATA +from .const import ( + ATTR_MEDIA_IMAGE_URL, + COMMANDS, + COUNTRYCODE_NAMES, + DOMAIN, + GAMES_FILE, + PS4_DATA, +) _LOGGER = logging.getLogger(__name__) @@ -39,6 +45,8 @@ } ) +PLATFORMS = ["media_player"] + class PS4Data: """Init Data Class.""" @@ -60,18 +68,15 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, entry): """Set up PS4 from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "media_player") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry): """Unload a PS4 config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, "media_player") - return True + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_migrate_entry(hass, entry): @@ -93,7 +98,7 @@ async def async_migrate_entry(hass, entry): hass.helpers.aiohttp_client.async_get_clientsession() ) if loc: - country = loc.country_name + country = COUNTRYCODE_NAMES.get(loc.country_code) if country in COUNTRIES: for device in data["devices"]: device[CONF_REGION] = country @@ -157,7 +162,7 @@ def format_unique_id(creds, mac_address): return f"{mac_address}_{suffix}" -def load_games(hass: HomeAssistantType, unique_id: str) -> dict: +def load_games(hass: HomeAssistant, unique_id: str) -> dict: """Load games for sources.""" g_file = hass.config.path(GAMES_FILE.format(unique_id)) try: @@ -176,7 +181,7 @@ def load_games(hass: HomeAssistantType, unique_id: str) -> dict: return games -def save_games(hass: HomeAssistantType, games: dict, unique_id: str): +def save_games(hass: HomeAssistant, games: dict, unique_id: str): """Save games to file.""" g_file = hass.config.path(GAMES_FILE.format(unique_id)) try: @@ -185,7 +190,7 @@ def save_games(hass: HomeAssistantType, games: dict, unique_id: str): _LOGGER.error("Could not save game list, %s", error) -def _reformat_data(hass: HomeAssistantType, games: dict, unique_id: str) -> dict: +def _reformat_data(hass: HomeAssistant, games: dict, unique_id: str) -> dict: """Reformat data to correct format.""" data_reformatted = False @@ -208,7 +213,7 @@ def _reformat_data(hass: HomeAssistantType, games: dict, unique_id: str) -> dict return games -def service_handle(hass: HomeAssistantType): +def service_handle(hass: HomeAssistant): """Handle for services.""" async def async_service_command(call): diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index a494b96abf17d..7424e0f5e1abc 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -17,7 +17,13 @@ ) from homeassistant.util import location -from .const import CONFIG_ENTRY_VERSION, DEFAULT_ALIAS, DEFAULT_NAME, DOMAIN +from .const import ( + CONFIG_ENTRY_VERSION, + COUNTRYCODE_NAMES, + DEFAULT_ALIAS, + DEFAULT_NAME, + DOMAIN, +) CONF_MODE = "Config Mode" CONF_AUTO = "Auto Discover" @@ -31,12 +37,10 @@ PIN_LENGTH = 8 -@config_entries.HANDLERS.register(DOMAIN) -class PlayStation4FlowHandler(config_entries.ConfigFlow): +class PlayStation4FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a PlayStation 4 config flow.""" VERSION = CONFIG_ENTRY_VERSION - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize the config flow.""" @@ -120,7 +124,7 @@ async def async_step_link(self, user_input=None): self.device_list = [device["host-ip"] for device in devices] # Check that devices found aren't configured per account. - entries = self.hass.config_entries.async_entries(DOMAIN) + entries = self._async_current_entries() if entries: # Retrieve device data from all entries if creds match. conf_devices = [ @@ -180,7 +184,7 @@ async def async_step_link(self, user_input=None): self.hass.helpers.aiohttp_client.async_get_clientsession() ) if self.location: - country = self.location.country_name + country = COUNTRYCODE_NAMES.get(self.location.country_code) if country in COUNTRIES: default_region = country diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index 0974286ebe824..f2d284daa7967 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -12,3 +12,71 @@ # Deprecated used for logger/backwards compatibility from 0.89 REGIONS = ["R1", "R2", "R3", "R4", "R5"] + +COUNTRYCODE_NAMES = { + "AE": "United Arab Emirates", + "AR": "Argentina", + "AT": "Austria", + "AU": "Australia", + "BE": "Belgium", + "BG": "Bulgaria", + "BH": "Bahrain", + "BR": "Brazil", + "CA": "Canada", + "CH": "Switzerland", + "CL": "Chile", + "CO": "Columbia", + "CR": "Costa Rica", + "CY": "Cyprus", + "CZ": "Czech Republic", + "DE": "Germany", + "DK": "Denmark", + "EC": "Ecuador", + "ES": "Spain", + "FI": "Finland", + "FR": "France", + "GB": "United Kingdom", + "GR": "Greece", + "GT": "Guatemala", + "HK": "Hong Kong", + "HN": "Honduras", + "HR": "Croatia", + "HU": "Hungary", + "ID": "Indonesia", + "IE": "Ireland", + "IL": "Israel", + "IN": "India", + "IS": "Iceland", + "IT": "Italy", + "JP": "Japan", + "KW": "Kuwait", + "LB": "Lebanon", + "LU": "Luxembourg", + "MT": "Malta", + "MX": "Mexico", + "MY": "Maylasia", + "NI": "Nicaragua", + "NL": "Nederland", + "NO": "Norway", + "NZ": "New Zealand", + "OM": "Oman", + "PA": "Panama", + "PE": "Peru", + "PL": "Poland", + "PT": "Portugal", + "QA": "Qatar", + "RO": "Romania", + "RU": "Russia", + "SA": "Saudi Arabia", + "SE": "Sweden", + "SG": "Singapore", + "SI": "Slovenia", + "SK": "Slovakia", + "SV": "El Salvador", + "TH": "Thailand", + "TR": "Turkey", + "TW": "Taiwan", + "UA": "Ukraine", + "US": "United States", + "ZA": "South Africa", +} diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 500c243b8c9e5..609b749774411 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": ["pyps4-2ndscreen==1.2.0"], - "codeowners": ["@ktnrg45"] + "codeowners": ["@ktnrg45"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ps4/services.yaml b/homeassistant/components/ps4/services.yaml index e1af6543a6507..fe7641357bf1d 100644 --- a/homeassistant/components/ps4/services.yaml +++ b/homeassistant/components/ps4/services.yaml @@ -1,9 +1,30 @@ send_command: + name: Send command description: Emulate button press for PlayStation 4. fields: entity_id: - description: Name(s) of entities to send command. + name: Entity + description: Name of entity to send command. + required: true example: "media_player.playstation_4" + selector: + entity: + integration: ps4 + domain: media_player command: + name: Command description: Button to press. + required: true example: "ps" + selector: + select: + options: + - "back" + - "down" + - "enter" + - "left" + - "option" + - "ps_hold" + - "ps" + - "right" + - "up" diff --git a/homeassistant/components/ps4/translations/ru.json b/homeassistant/components/ps4/translations/ru.json index d39907d0179c4..ea42e260ec821 100644 --- a/homeassistant/components/ps4/translations/ru.json +++ b/homeassistant/components/ps4/translations/ru.json @@ -4,8 +4,8 @@ "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.", "credential_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", - "port_987_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 987. \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](https://www.home-assistant.io/components/ps4/).", - "port_997_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 997. \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](https://www.home-assistant.io/components/ps4/)." + "port_987_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 987. \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](https://www.home-assistant.io/components/ps4/).", + "port_997_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 997. \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](https://www.home-assistant.io/components/ps4/)." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -25,7 +25,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**. \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/components/ps4/) \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.", + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**. \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](https://www.home-assistant.io/components/ps4/) \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.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/ps4/translations/zh-Hant.json b/homeassistant/components/ps4/translations/zh-Hant.json index 77bfa7bfdb11d..4475700481a2a 100644 --- a/homeassistant/components/ps4/translations/zh-Hant.json +++ b/homeassistant/components/ps4/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", diff --git a/homeassistant/components/pulseaudio_loopback/manifest.json b/homeassistant/components/pulseaudio_loopback/manifest.json index bc38d8c2594d6..4d7bfbf1e2984 100644 --- a/homeassistant/components/pulseaudio_loopback/manifest.json +++ b/homeassistant/components/pulseaudio_loopback/manifest.json @@ -3,5 +3,6 @@ "name": "PulseAudio Loopback", "documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback", "requirements": ["pulsectl==20.2.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/push/manifest.json b/homeassistant/components/push/manifest.json index c4a419bcfd3af..bafae78c23b5c 100644 --- a/homeassistant/components/push/manifest.json +++ b/homeassistant/components/push/manifest.json @@ -3,5 +3,6 @@ "name": "Push", "documentation": "https://www.home-assistant.io/integrations/push", "dependencies": ["webhook"], - "codeowners": ["@dgomes"] + "codeowners": ["@dgomes"], + "iot_class": "local_push" } diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index 1453f9ffe7355..34356e74a5648 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -3,5 +3,6 @@ "name": "Pushbullet", "documentation": "https://www.home-assistant.io/integrations/pushbullet", "requirements": ["pushbullet.py==0.11.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 222e7a22fdf41..56bfac0185919 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -3,5 +3,6 @@ "name": "Pushover", "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": ["pushover_complete==1.1.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 952a399157ca4..3f599ac2d8a89 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -75,6 +75,7 @@ def send_message(self, message="", **kwargs): if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): # try to open it as a normal file. try: + # pylint: disable=consider-using-with file_handle = open(data[ATTR_ATTACHMENT], "rb") # Replace the attachment identifier with file object. image = file_handle diff --git a/homeassistant/components/pushsafer/manifest.json b/homeassistant/components/pushsafer/manifest.json index 8932de99b5d60..a38f6f45f04d5 100644 --- a/homeassistant/components/pushsafer/manifest.json +++ b/homeassistant/components/pushsafer/manifest.json @@ -2,5 +2,6 @@ "domain": "pushsafer", "name": "Pushsafer", "documentation": "https://www.home-assistant.io/integrations/pushsafer", - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index 93f9b45c62ada..af40cf7eca457 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -3,5 +3,6 @@ "name": "PVOutput", "documentation": "https://www.home-assistant.io/integrations/pvoutput", "after_dependencies": ["rest"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 5930da52313a2..2ab8f387bda8d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORM, TARIFFS +from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORMS, TARIFFS UI_CONFIG_SCHEMA = vol.Schema( { @@ -15,7 +15,8 @@ } ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.ensure_list(UI_CONFIG_SCHEMA)}, extra=vol.ALLOW_EXTRA + vol.All(cv.deprecated(DOMAIN), {DOMAIN: cv.ensure_list(UI_CONFIG_SCHEMA)}), + extra=vol.ALLOW_EXTRA, ) @@ -44,13 +45,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Set up pvpc hourly pricing from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, PLATFORM) - ) - + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py index 10591e5b82c25..971a13acc2f33 100644 --- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -11,7 +11,6 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=_DOMAIN_NAME): """Handle a config flow for `pvpc_hourly_pricing` to select the tariff.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/pvpc_hourly_pricing/const.py b/homeassistant/components/pvpc_hourly_pricing/const.py index d75ad9fe35c0f..9e11bc57d6dbe 100644 --- a/homeassistant/components/pvpc_hourly_pricing/const.py +++ b/homeassistant/components/pvpc_hourly_pricing/const.py @@ -2,7 +2,7 @@ from aiopvpc import TARIFFS DOMAIN = "pvpc_hourly_pricing" -PLATFORM = "sensor" +PLATFORMS = ["sensor"] ATTR_TARIFF = "tariff" DEFAULT_NAME = "PVPC" DEFAULT_TARIFF = TARIFFS[1] diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 3f2dd00d832f4..c39d66163e026 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,8 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==2.0.2"], + "requirements": ["aiopvpc==2.1.1"], "codeowners": ["@azogue"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 8a446a032f84b..15cf837c90e6f 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -2,5 +2,6 @@ "domain": "pyload", "name": "pyLoad", "documentation": "https://www.home-assistant.io/integrations/pyload", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index 2f3e8cf4f1a00..241b9a5cff9cf 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -3,5 +3,6 @@ "name": "qBittorrent", "documentation": "https://www.home-assistant.io/integrations/qbittorrent", "requirements": ["python-qbittorrent==0.4.2"], - "codeowners": ["@geoffreylagaisse"] + "codeowners": ["@geoffreylagaisse"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index db98e2f7338ce..aeddc8cbeb086 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -3,5 +3,6 @@ "name": "Queensland Bushfire Alert", "documentation": "https://www.home-assistant.io/integrations/qld_bushfire", "requirements": ["georss_qld_bushfire_alert_client==0.3"], - "codeowners": ["@exxamalte"] + "codeowners": ["@exxamalte"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index 29750683abf05..abd5d6f5a4a6d 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -3,5 +3,6 @@ "name": "QNAP", "documentation": "https://www.home-assistant.io/integrations/qnap", "requirements": ["qnapstats==0.3.1"], - "codeowners": ["@colinodell"] + "codeowners": ["@colinodell"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index bd574af0297d4..18bf2d7db6dfc 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -3,5 +3,6 @@ "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": ["pillow==8.1.2", "pyzbar==0.1.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "calculated" } diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index 08f8a5191c9fc..228f6a5eab067 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -7,7 +7,7 @@ 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_SSL @@ -17,7 +17,7 @@ DEFAULT_HOST = "myfiosgateway.com" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_SSL, default=True): cv.boolean, diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index 1c4a7a13923eb..b734be8508e9c 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -3,5 +3,6 @@ "name": "Quantum Gateway", "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", "requirements": ["quantum-gateway==0.0.5"], - "codeowners": ["@cisasteelersfan"] + "codeowners": ["@cisasteelersfan"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qvr_pro/manifest.json b/homeassistant/components/qvr_pro/manifest.json index d6365afd213a1..eb08be180c6c4 100644 --- a/homeassistant/components/qvr_pro/manifest.json +++ b/homeassistant/components/qvr_pro/manifest.json @@ -3,5 +3,6 @@ "name": "QVR Pro", "documentation": "https://www.home-assistant.io/integrations/qvr_pro", "requirements": ["pyqvrpro==0.52"], - "codeowners": ["@oblogic7"] + "codeowners": ["@oblogic7"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/qvr_pro/services.yaml b/homeassistant/components/qvr_pro/services.yaml index 0f305d1fa8d69..edb879c784a2b 100644 --- a/homeassistant/components/qvr_pro/services.yaml +++ b/homeassistant/components/qvr_pro/services.yaml @@ -1,13 +1,23 @@ start_record: + name: Start record description: Start QVR Pro recording on specified channel. fields: guid: + name: GUID description: GUID of the channel to start recording. + required: true example: "245EBE933C0A597EBE865C0A245E0002" + selector: + text: stop_record: + name: Stop record description: Stop QVR Pro recording on specified channel. fields: guid: + name: GUID description: GUID of the channel to stop recording. + required: true example: "245EBE933C0A597EBE865C0A245E0002" + selector: + text: diff --git a/homeassistant/components/qwikswitch/manifest.json b/homeassistant/components/qwikswitch/manifest.json index 31e84fccf9a24..851e93dc67d95 100644 --- a/homeassistant/components/qwikswitch/manifest.json +++ b/homeassistant/components/qwikswitch/manifest.json @@ -3,5 +3,6 @@ "name": "QwikSwitch QSUSB", "documentation": "https://www.home-assistant.io/integrations/qwikswitch", "requirements": ["pyqwikswitch==0.93"], - "codeowners": ["@kellerza"] + "codeowners": ["@kellerza"], + "iot_class": "local_push" } diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 30015dcf8c1de..3f75537cc8dbf 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -1,5 +1,4 @@ """Integration with the Rachio Iro sprinkler system controller.""" -import asyncio import logging import secrets @@ -26,28 +25,11 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the rachio component from YAML.""" - - hass.data.setdefault(DOMAIN, {}) - - 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 - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok @@ -84,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Get the API user try: - await hass.async_add_executor_job(person.setup, hass) + await person.async_setup(hass) except ConnectTimeout as error: _LOGGER.error("Could not reach the Rachio API: %s", error) raise ConfigEntryNotReady from error @@ -100,12 +82,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Enable platform + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = person async_register_webhook(hass, webhook_id, entry.entry_id) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 5719dd810660f..ee03d042ec2c3 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -55,7 +55,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Rachio.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH async def async_step_user(self, user_input=None): """Handle the initial step.""" @@ -78,18 +77,11 @@ async def async_step_user(self, user_input=None): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_homekit(self, homekit_info): + async def async_step_homekit(self, discovery_info): """Handle HomeKit discovery.""" - if self._async_current_entries(): - # We can see rachio on the network to tell them to configure - # it, but since the device will not give up the account it is - # bound to and there can be multiple rachio systems on a single - # account, we avoid showing the device as discovered once - # they already have one configured as they can always - # add a new one via "+" - return self.async_abort(reason="already_configured") + self._async_abort_entries_match() properties = { - key.lower(): value for (key, value) in homekit_info["properties"].items() + key.lower(): value for (key, value) in discovery_info["properties"].items() } await self.async_set_unique_id(properties["id"]) return await self.async_step_user() @@ -104,7 +96,7 @@ def async_get_options_flow(config_entry): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Rachio.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index a6ed596db0421..ac2fea20bcfda 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -57,23 +57,65 @@ def __init__(self, rachio, config_entry): self._id = None self._controllers = [] - def setup(self, hass): - """Rachio device setup.""" - all_devices = [] + async def async_setup(self, hass): + """Create rachio devices and services.""" + await hass.async_add_executor_job(self._setup, hass) can_pause = False - response = self.rachio.person.info() + for rachio_iro in self._controllers: + # Generation 1 controllers don't support pause or resume + if rachio_iro.model.split("_")[0] != MODEL_GENERATION_1: + can_pause = True + break + + if not can_pause: + return + + all_devices = [rachio_iro.name for rachio_iro in self._controllers] + + def pause_water(service): + """Service to pause watering on all or specific controllers.""" + duration = service.data[ATTR_DURATION] + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.pause_watering(duration) + + def resume_water(service): + """Service to resume watering on all or specific controllers.""" + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.resume_watering() + + hass.services.async_register( + DOMAIN, + SERVICE_PAUSE_WATERING, + pause_water, + schema=PAUSE_SERVICE_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_RESUME_WATERING, + resume_water, + schema=RESUME_SERVICE_SCHEMA, + ) + + def _setup(self, hass): + """Rachio device setup.""" + rachio = self.rachio + + response = rachio.person.info() assert int(response[0][KEY_STATUS]) == HTTP_OK, "API key error" self._id = response[1][KEY_ID] # Use user ID to get user data - data = self.rachio.person.get(self._id) + data = rachio.person.get(self._id) assert int(data[0][KEY_STATUS]) == HTTP_OK, "User ID error" self.username = data[1][KEY_USERNAME] devices = data[1][KEY_DEVICES] for controller in devices: - webhooks = self.rachio.notification.get_device_webhook(controller[KEY_ID])[ - 1 - ] + webhooks = rachio.notification.get_device_webhook(controller[KEY_ID])[1] # The API does not provide a way to tell if a controller is shared # or if they are the owner. To work around this problem we fetch the webooks # before we setup the device so we can skip it instead of failing. @@ -94,46 +136,12 @@ def setup(self, hass): ) continue - rachio_iro = RachioIro(hass, self.rachio, controller, webhooks) + rachio_iro = RachioIro(hass, rachio, controller, webhooks) rachio_iro.setup() self._controllers.append(rachio_iro) - all_devices.append(rachio_iro.name) - # Generation 1 controllers don't support pause or resume - if rachio_iro.model.split("_")[0] != MODEL_GENERATION_1: - can_pause = True _LOGGER.info('Using Rachio API as user "%s"', self.username) - def pause_water(service): - """Service to pause watering on all or specific controllers.""" - duration = service.data[ATTR_DURATION] - devices = service.data.get(ATTR_DEVICES, all_devices) - for iro in self._controllers: - if iro.name in devices: - iro.pause_watering(duration) - - def resume_water(service): - """Service to resume watering on all or specific controllers.""" - devices = service.data.get(ATTR_DEVICES, all_devices) - for iro in self._controllers: - if iro.name in devices: - iro.resume_watering() - - if can_pause: - hass.services.register( - DOMAIN, - SERVICE_PAUSE_WATERING, - pause_water, - schema=PAUSE_SERVICE_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_RESUME_WATERING, - resume_water, - schema=RESUME_SERVICE_SCHEMA, - ) - @property def user_id(self) -> str: """Get the user ID as defined by the Rachio API.""" diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index ba81b65b37f36..735e2f35bf4aa 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -7,19 +7,28 @@ "after_dependencies": ["cloud"], "codeowners": ["@bdraco"], "config_flow": true, - "dhcp": [{ - "hostname": "rachio-*", - "macaddress": "009D6B*" - }, - { - "hostname": "rachio-*", - "macaddress": "F0038C*" - }, - { - "hostname": "rachio-*", - "macaddress": "74C63B*" - }], + "dhcp": [ + { + "hostname": "rachio-*", + "macaddress": "009D6B*" + }, + { + "hostname": "rachio-*", + "macaddress": "F0038C*" + }, + { + "hostname": "rachio-*", + "macaddress": "74C63B*" + } + ], "homekit": { "models": ["Rachio"] - } + }, + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "rachio*" + } + ], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/rachio/services.yaml b/homeassistant/components/rachio/services.yaml index 815a860131485..93f63fcb9c309 100644 --- a/homeassistant/components/rachio/services.yaml +++ b/homeassistant/components/rachio/services.yaml @@ -1,33 +1,65 @@ set_zone_moisture_percent: - description: Set the moisture percentage of a zone or group of zones. + name: Set zone moisture percent + description: Set the moisture percentage of a zone or list of zones. + target: + entity: + integration: rachio + domain: switch fields: - entity_id: - description: Name of the zone entity. Can also be a group of zones. [Required] - example: "switch.front_yard" percent: - description: Set the desired zone moisture percentage from 0 to 100. [Required] + name: Percent + description: Set the desired zone moisture percentage. + required: true example: 50 + selector: + number: + min: 0 + max: 100 + mode: slider + unit_of_measurement: "%" start_multiple_zone_schedule: - description: Create a custom schedule of zones and runtimes. + name: Start multiple zones + description: Create a custom schedule of zones and runtimes. Note that all zones should be on the same controller to avoid issues. + target: + entity: + integration: rachio + domain: switch fields: - entity_id: - description: Name of the zone or zones to run. Zones should all be on the same controller, attempting to start zones on multiple controllers may have undesired results. [Required] - example: "switch.front_yard, switch.side_yard" duration: - description: Number of minutes to run the zone(s). If only 1 duration is given, that time will be used for all zones. If given a list of durations, the durations will apply to the respective zone listed above. [Required] + name: Duration + description: Number of minutes to run the zone(s). If only 1 duration is given, that time will be used for all zones. If given a list of durations, the durations will apply to the respective zones listed above. example: 15, 20 + required: true + selector: + object: pause_watering: + name: Pause watering description: Pause any currently running zones or schedules. fields: devices: - description: Name of controllers to pause. Defaults to all controllers on the account if not provided. [Optional] - example: Main House + name: Devices + description: Name of controllers to pause. Defaults to all controllers on the account if not provided. + example: "Main House" + selector: + text: duration: - description: The number of minutes to pause running schedules. Accepts 1-60. Default is 60 minutes. [Optional] + name: Duration + description: The time to pause running schedules. example: 30 + default: 60 + selector: + number: + min: 1 + max: 60 + mode: slider + unit_of_measurement: "minutes" resume_watering: + name: Resume watering description: Resume any paused zone runs or schedules. fields: devices: - description: Name of controllers to resume. Defaults to all controllers on the account if not provided. [Optional] - example: Main House + name: Devices + description: Name of controllers to resume. Defaults to all controllers on the account if not provided. + example: "Main House" + selector: + text: diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 8d87b688aa47e..de897cb7f07e3 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -140,7 +140,7 @@ def start_multiple(service): ) if has_flex_sched: - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_ZONE_MOISTURE, {vol.Required(ATTR_PERCENT): cv.positive_int}, @@ -356,7 +356,7 @@ def __init__(self, person, controller, data, current_schedule): def __str__(self): """Display the zone as a string.""" - return 'Rachio Zone "{}" on {}'.format(self.name, str(self._controller)) + return f'Rachio Zone "{self.name}" on {str(self._controller)}' @property def zone_id(self) -> str: @@ -418,6 +418,7 @@ def turn_on(self, **kwargs) -> None: CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS ) ) + # The API limit is 3 hours, and requires an int be passed self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds) _LOGGER.debug( "Watering %s on %s for %s", diff --git a/homeassistant/components/rachio/translations/nl.json b/homeassistant/components/rachio/translations/nl.json index 6a94ac2dcd41e..7071401a1672d 100644 --- a/homeassistant/components/rachio/translations/nl.json +++ b/homeassistant/components/rachio/translations/nl.json @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "Hoe lang, in minuten, om een station in te schakelen wanneer de schakelaar is ingeschakeld." + "manual_run_mins": "Looptijd in minuten bij activering van een zoneschakelaar" } } } diff --git a/homeassistant/components/rachio/translations/zh-Hant.json b/homeassistant/components/rachio/translations/zh-Hant.json index b800daee779c6..a65e4e279f93d 100644 --- a/homeassistant/components/rachio/translations/zh-Hant.json +++ b/homeassistant/components/rachio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 8f752f0350077..611b4a33f3be8 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,5 +2,6 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 542ff285261f5..fda7a37756b69 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -3,7 +3,6 @@ import logging import time -from pytz import timezone import requests import voluptuous as vol @@ -26,6 +25,7 @@ HTTP_OK, ) import homeassistant.helpers.config_validation as cv +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -112,7 +112,6 @@ def __init__(self, hass, conf, sensor_type): self.ssl = "https" if conf.get(CONF_SSL) else "http" self._state = None self.data = [] - self._tz = timezone(str(hass.config.time_zone)) self.type = sensor_type self._name = SENSOR_TYPES[self.type][0] if self.type == "diskspace": @@ -177,8 +176,9 @@ def icon(self): def update(self): """Update the data for the sensor.""" - start = get_date(self._tz) - end = get_date(self._tz, self.days) + time_zone = dt_util.get_time_zone(self.hass.config.time_zone) + start = get_date(time_zone) + end = get_date(time_zone, self.days) try: res = requests.get( ENDPOINTS[self.type].format( diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index 0220c2338419d..b051ba65b3b0c 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -3,5 +3,6 @@ "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": ["radiotherm==2.1.0"], - "codeowners": ["@vinnyfuria"] + "codeowners": ["@vinnyfuria"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 89ca65fd44bd5..120e38e8058e7 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,5 +3,6 @@ "name": "Rain Bird", "documentation": "https://www.home-assistant.io/integrations/rainbird", "requirements": ["pyrainbird==0.4.2"], - "codeowners": ["@konikvranik"] + "codeowners": ["@konikvranik"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml index ed1ec8b62df00..795fe5343d2d8 100644 --- a/homeassistant/components/rainbird/services.yaml +++ b/homeassistant/components/rainbird/services.yaml @@ -1,9 +1,18 @@ start_irrigation: + name: Start irrigation description: Start the irrigation fields: entity_id: + name: Entity description: Name of a single irrigation to turn on + required: true example: "switch.sprinkler_1" + selector: + entity: + integration: rainbird + domain: switch duration: + name: Duration description: Duration for this sprinkler to be turned on + required: true example: 1 diff --git a/homeassistant/components/raincloud/manifest.json b/homeassistant/components/raincloud/manifest.json index a0edaa87825f4..309dc6bdb5199 100644 --- a/homeassistant/components/raincloud/manifest.json +++ b/homeassistant/components/raincloud/manifest.json @@ -3,5 +3,6 @@ "name": "Melnor RainCloud", "documentation": "https://www.home-assistant.io/integrations/raincloud", "requirements": ["raincloudy==0.0.7"], - "codeowners": ["@vanstinator"] + "codeowners": ["@vanstinator"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index 4fbce5d04cec4..fd28e5b09944d 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -3,5 +3,6 @@ "name": "Rainforest Eagle-200", "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "requirements": ["eagle200_reader==0.2.4", "uEagle==0.0.2"], - "codeowners": ["@gtdiehl", "@jcalbert"] + "codeowners": ["@gtdiehl", "@jcalbert"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index e71e8a1f6d2c1..09af357617d2e 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -18,12 +18,16 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv +import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, UpdateFailed, ) +from homeassistant.util.network import is_ip_address +from .config_flow import get_client_controller from .const import ( CONF_ZONE_RUN_TIME, DATA_CONTROLLER, @@ -37,8 +41,6 @@ LOGGER, ) -DATA_LISTENER = "listener" - DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True @@ -69,32 +71,10 @@ async def async_update_programs_and_zones( ) -async def async_setup(hass: HomeAssistant, config: dict) -> bool: - """Set up the RainMachine component.""" - hass.data[DOMAIN] = {DATA_CONTROLLER: {}, DATA_COORDINATOR: {}, DATA_LISTENER: {}} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up RainMachine as config entry.""" + hass.data.setdefault(DOMAIN, {DATA_CONTROLLER: {}, DATA_COORDINATOR: {}}) hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} - - entry_updates = {} - if not entry.unique_id: - # If the config entry doesn't already have a unique ID, set one: - entry_updates["unique_id"] = entry.data[CONF_IP_ADDRESS] - if CONF_ZONE_RUN_TIME in entry.data: - # If a zone run time exists in the config entry's data, pop it and move it to - # options: - data = {**entry.data} - entry_updates["data"] = data - entry_updates["options"] = { - **entry.options, - CONF_ZONE_RUN_TIME: data.pop(CONF_ZONE_RUN_TIME), - } - if entry_updates: - hass.config_entries.async_update_entry(entry, **entry_updates) - websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -106,14 +86,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), ) except RainMachineError as err: - LOGGER.error("An error occurred: %s", err) raise ConfigEntryNotReady from err # regenmaschine can load multiple controllers at once, but we only grab the one # we loaded above: - controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] = next( - iter(client.controllers.values()) - ) + controller = hass.data[DOMAIN][DATA_CONTROLLER][ + entry.entry_id + ] = get_client_controller(client) + + entry_updates = {} + if not entry.unique_id or is_ip_address(entry.unique_id): + # If the config entry doesn't already have a unique ID, set one: + entry_updates["unique_id"] = controller.mac + if CONF_ZONE_RUN_TIME in entry.data: + # If a zone run time exists in the config entry's data, pop it and move it to + # options: + data = {**entry.data} + entry_updates["data"] = data + entry_updates["options"] = { + **entry.options, + CONF_ZONE_RUN_TIME: data.pop(CONF_ZONE_RUN_TIME), + } + if entry_updates: + hass.config_entries.async_update_entry(entry, **entry_updates) async def async_update(api_category: str) -> dict: """Update the appropriate API data based on a category.""" @@ -155,31 +150,18 @@ async def async_update(api_category: str) -> dict: await asyncio.gather(*controller_init_tasks) - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) - cancel_listener() - return unload_ok @@ -211,10 +193,11 @@ def device_class(self) -> str: return self._device_class @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" return { "identifiers": {(DOMAIN, self._controller.mac)}, + "connections": {(dr.CONNECTION_NETWORK_MAC, self._controller.mac)}, "name": self._controller.name, "manufacturer": "RainMachine", "model": ( diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 5d141b0f00856..4b89b52befe6a 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -1,12 +1,12 @@ """This platform provides binary sensors for key RainMachine data.""" from functools import partial -from typing import Callable from regenmaschine.controller import Controller from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import RainMachineEntity @@ -73,7 +73,7 @@ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up RainMachine binary sensors based on a config entry.""" controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index e076a1055766c..55ff68c5ea067 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -7,6 +7,7 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_ZONE_RUN_TIME, DEFAULT_PORT, DEFAULT_ZONE_RUN, DOMAIN @@ -19,11 +20,31 @@ ) +def get_client_controller(client): + """Return the first local controller.""" + return next(iter(client.controllers.values())) + + +async def async_get_controller(hass, ip_address, password, port, ssl): + """Auth and fetch the mac address from the controller.""" + websession = aiohttp_client.async_get_clientsession(hass) + client = Client(session=websession) + try: + await client.load_local(ip_address, password, port=port, ssl=ssl) + except RainMachineError: + return None + else: + return get_client_controller(client) + + class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a RainMachine config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize config flow.""" + self.discovered_ip_address = None @staticmethod @callback @@ -31,47 +52,90 @@ def async_get_options_flow(config_entry): """Define the config flow to handle options.""" return RainMachineOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): - """Handle the start of the config flow.""" - if not user_input: - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors={} - ) - - await self.async_set_unique_id(user_input[CONF_IP_ADDRESS]) + async def async_step_homekit(self, discovery_info): + """Handle a flow initialized by homekit discovery.""" + return await self.async_step_zeroconf(discovery_info) + + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Handle discovery via zeroconf.""" + ip_address = discovery_info["host"] + + self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address}) + # Handle IP change + for entry in self._async_current_entries(include_ignore=False): + # Try our existing credentials to check for ip change + if controller := await async_get_controller( + self.hass, + ip_address, + entry.data[CONF_PASSWORD], + entry.data[CONF_PORT], + entry.data.get(CONF_SSL, True), + ): + await self.async_set_unique_id(controller.mac) + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: ip_address} + ) + + # A new rain machine: We will change out the unique id + # for the mac address once we authenticate, however we want to + # prevent multiple different rain machines on the same network + # from being shown in discovery + await self.async_set_unique_id(ip_address) self._abort_if_unique_id_configured() + self.discovered_ip_address = ip_address + return await self.async_step_user() - websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(session=websession) + @callback + def _async_generate_schema(self): + """Generate schema.""" + return vol.Schema( + { + vol.Required(CONF_IP_ADDRESS, default=self.discovered_ip_address): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + } + ) - try: - await client.load_local( + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + errors = {} + if user_input: + self._async_abort_entries_match( + {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]} + ) + controller = await async_get_controller( + self.hass, user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], - port=user_input[CONF_PORT], - ssl=user_input.get(CONF_SSL, True), - ) - except RainMachineError: - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={CONF_PASSWORD: "invalid_auth"}, + user_input[CONF_PORT], + user_input.get(CONF_SSL, True), ) - - # Unfortunately, RainMachine doesn't provide a way to refresh the - # access token without using the IP address and password, so we have to - # store it: - return self.async_create_entry( - title=user_input[CONF_IP_ADDRESS], - data={ - CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_PORT: user_input[CONF_PORT], - CONF_SSL: user_input.get(CONF_SSL, True), - CONF_ZONE_RUN_TIME: user_input.get( - CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN - ), - }, + if controller: + await self.async_set_unique_id(controller.mac) + self._abort_if_unique_id_configured() + + # Unfortunately, RainMachine doesn't provide a way to refresh the + # access token without using the IP address and password, so we have to + # store it: + return self.async_create_entry( + title=controller.name, + data={ + CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_PORT: user_input[CONF_PORT], + CONF_SSL: user_input.get(CONF_SSL, True), + CONF_ZONE_RUN_TIME: user_input.get( + CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN + ), + }, + ) + + errors = {CONF_PASSWORD: "invalid_auth"} + + if self.discovered_ip_address: + self.context["title_placeholders"] = {"ip": self.discovered_ip_address} + return self.async_show_form( + step_id="user", data_schema=self._async_generate_schema(), errors=errors ) diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index 568108e23a643..56c1660a0ba4b 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -9,7 +9,6 @@ DATA_CONTROLLER = "controller" DATA_COORDINATOR = "coordinator" -DATA_LISTENER = "listener" DATA_PROGRAMS = "programs" DATA_PROVISION_SETTINGS = "provision.settings" DATA_RESTRICTIONS_CURRENT = "restrictions.current" diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 5d03155deac62..b6021d02c39d4 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -4,5 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", "requirements": ["regenmaschine==3.0.0"], - "codeowners": ["@bachya"] + "codeowners": ["@bachya"], + "iot_class": "local_polling", + "homekit": { + "models": ["Touch HD", "SPK5"] + }, + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "rainmachine*" + } + ] } diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 20912809cb1a4..2ebd9d0fdb496 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -1,6 +1,5 @@ """This platform provides support for sensor data from RainMachine.""" from functools import partial -from typing import Callable from regenmaschine.controller import Controller @@ -8,6 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import RainMachineEntity @@ -70,7 +70,7 @@ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up RainMachine sensors based on a config entry.""" controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index a73dc5c899dc2..fa2706921429a 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -1,97 +1,174 @@ # Describes the format for available RainMachine services disable_program: + name: Disable program description: Disable a program. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 program_id: + name: Program ID description: The program to disable. + required: true example: 3 + selector: + number: + min: 1 + max: 255 disable_zone: + name: Disable zone description: Disable a zone. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 zone_id: + name: Zone ID description: The zone to disable. + required: true example: 3 + selector: + number: + min: 1 + max: 255 enable_program: + name: Enable program description: Enable a program. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 program_id: + name: Program ID description: The program to enable. + required: true example: 3 + selector: + number: + min: 1 + max: 255 enable_zone: + name: Enable zone description: Enable a zone. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 zone_id: + name: Zone ID description: The zone to enable. + required: true example: 3 + selector: + number: + min: 1 + max: 255 pause_watering: + name: Pause watering description: Pause all watering for a number of seconds. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 seconds: - description: The number of seconds to pause. + name: Seconds + description: The time to pause. + required: true example: 30 + selector: + number: + min: 1 + max: 86400 + unit_of_measurement: seconds start_program: + name: Start program description: Start a program. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 program_id: + name: Program ID description: The program to start. + required: true example: 3 + selector: + number: + min: 1 + max: 255 start_zone: + name: Start zone description: Start a zone for a set number of seconds. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 zone_id: + name: Zone ID description: The zone to start. + required: true example: 3 + selector: + number: + min: 1 + max: 255 zone_run_time: + name: Zone run time description: The number of seconds to run the zone. example: 120 + default: 600 stop_all: + name: Stop all description: Stop all watering activities. - fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 + target: + entity: + integration: rainmachine + domain: switch stop_program: + name: Stop program description: Stop a program. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 program_id: + name: Program ID description: The program to stop. + required: true example: 3 + selector: + number: + min: 1 + max: 255 stop_zone: + name: Stop zone description: Stop a zone. + target: + entity: + integration: rainmachine + domain: switch fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 zone_id: + name: Zone ID description: The zone to stop. + required: true example: 3 + selector: + number: + min: 1 + max: 255 unpause_watering: + name: Unpause watering description: Unpause all watering. - fields: - entity_id: - description: An entity from the desired RainMachine controller - example: switch.zone_1 + target: + entity: + integration: rainmachine + domain: switch diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json index 1f5a21d37d807..7634c0a69c53b 100644 --- a/homeassistant/components/rainmachine/strings.json +++ b/homeassistant/components/rainmachine/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{ip}", "step": { "user": { "title": "Fill in your information", diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 6741abbfc9f58..a90091c6c3a6d 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -1,6 +1,8 @@ """This component provides support for RainMachine programs and zones.""" +from __future__ import annotations + +from collections.abc import Coroutine from datetime import datetime -from typing import Callable, Coroutine from regenmaschine.controller import Controller from regenmaschine.errors import RequestError @@ -11,6 +13,7 @@ from homeassistant.const import ATTR_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import RainMachineEntity, async_update_programs_and_zones @@ -107,10 +110,10 @@ async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up RainMachine switches based on a config entry.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() alter_program_schema = {vol.Required(CONF_PROGRAM_ID): cv.positive_int} alter_zone_schema = {vol.Required(CONF_ZONE_ID): cv.positive_int} diff --git a/homeassistant/components/rainmachine/translations/ca.json b/homeassistant/components/rainmachine/translations/ca.json index 9472211f8df25..d441654ce0831 100644 --- a/homeassistant/components/rainmachine/translations/ca.json +++ b/homeassistant/components/rainmachine/translations/ca.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/de.json b/homeassistant/components/rainmachine/translations/de.json index 511d85b36b6c4..20c49ea30f40b 100644 --- a/homeassistant/components/rainmachine/translations/de.json +++ b/homeassistant/components/rainmachine/translations/de.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung" }, + "flow_title": "{ip}", "step": { "user": { "data": { @@ -20,6 +21,9 @@ "options": { "step": { "init": { + "data": { + "zone_run_time": "Standard-Zonenlaufzeit (in Sekunden)" + }, "title": "RainMachine konfigurieren" } } diff --git a/homeassistant/components/rainmachine/translations/en.json b/homeassistant/components/rainmachine/translations/en.json index f65463626e46f..9369eeae4c84d 100644 --- a/homeassistant/components/rainmachine/translations/en.json +++ b/homeassistant/components/rainmachine/translations/en.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Invalid authentication" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/es.json b/homeassistant/components/rainmachine/translations/es.json index 9562aa599283d..317339ed39f32 100644 --- a/homeassistant/components/rainmachine/translations/es.json +++ b/homeassistant/components/rainmachine/translations/es.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/et.json b/homeassistant/components/rainmachine/translations/et.json index e3eb3e604620e..cad1284ad3d4a 100644 --- a/homeassistant/components/rainmachine/translations/et.json +++ b/homeassistant/components/rainmachine/translations/et.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Tuvastamise viga" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/fr.json b/homeassistant/components/rainmachine/translations/fr.json index 02b7dbc26990c..df0f9efa588fd 100644 --- a/homeassistant/components/rainmachine/translations/fr.json +++ b/homeassistant/components/rainmachine/translations/fr.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Authentification invalide" }, + "flow_title": "RainMachine {ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/it.json b/homeassistant/components/rainmachine/translations/it.json index 72e377dd95dce..c63bbd7db118a 100644 --- a/homeassistant/components/rainmachine/translations/it.json +++ b/homeassistant/components/rainmachine/translations/it.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Autenticazione non valida" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json index 8b767ced6c0a4..cbf76b879cba2 100644 --- a/homeassistant/components/rainmachine/translations/nl.json +++ b/homeassistant/components/rainmachine/translations/nl.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Ongeldige authenticatie" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index 214b50404a6a9..7fbeda3837445 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Ugyldig godkjenning" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/pl.json b/homeassistant/components/rainmachine/translations/pl.json index ff8918660f82f..665152e8e0f87 100644 --- a/homeassistant/components/rainmachine/translations/pl.json +++ b/homeassistant/components/rainmachine/translations/pl.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "Niepoprawne uwierzytelnienie" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json index 8502b66aff7e5..8dbe804ecab09 100644 --- a/homeassistant/components/rainmachine/translations/ru.json +++ b/homeassistant/components/rainmachine/translations/ru.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index 9b5829cf209be..d37ae79541f8d 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/random/manifest.json b/homeassistant/components/random/manifest.json index 5e73fbd442105..ae135c9de40a6 100644 --- a/homeassistant/components/random/manifest.json +++ b/homeassistant/components/random/manifest.json @@ -3,5 +3,6 @@ "name": "Random", "documentation": "https://www.home-assistant.io/integrations/random", "codeowners": ["@fabaff"], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_polling" } diff --git a/homeassistant/components/raspihats/manifest.json b/homeassistant/components/raspihats/manifest.json index 400cd275dc12f..984f440e0646d 100644 --- a/homeassistant/components/raspihats/manifest.json +++ b/homeassistant/components/raspihats/manifest.json @@ -3,5 +3,6 @@ "name": "Raspihats", "documentation": "https://www.home-assistant.io/integrations/raspihats", "requirements": ["raspihats==2.2.3", "smbus-cffi==0.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/raspyrfm/manifest.json b/homeassistant/components/raspyrfm/manifest.json index ed840c708244d..6fd4b13dee07b 100644 --- a/homeassistant/components/raspyrfm/manifest.json +++ b/homeassistant/components/raspyrfm/manifest.json @@ -3,5 +3,6 @@ "name": "RaspyRFM", "documentation": "https://www.home-assistant.io/integrations/raspyrfm", "requirements": ["raspyrfm-client==1.2.8"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 2e6f780c74928..f061532c3d1b8 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,7 +1,6 @@ """The ReCollect Waste integration.""" from __future__ import annotations -import asyncio from datetime import date, timedelta from aiorecollect.client import Client, PickupEvent @@ -58,10 +57,7 @@ async def async_get_pickup_events() -> list[PickupEvent]: hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( async_reload_entry @@ -77,14 +73,7 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py index 62b42e2bddf6d..9919c2653a8d5 100644 --- a/homeassistant/components/recollect_waste/config_flow.py +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -21,7 +21,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for ReCollect Waste.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback @@ -74,7 +73,7 @@ async def async_step_user(self, user_input: dict = None) -> dict: class RecollectWasteOptionsFlowHandler(config_entries.OptionsFlow): """Handle a Recollect Waste options flow.""" - def __init__(self, entry: config_entries.ConfigEntry): + def __init__(self, entry: config_entries.ConfigEntry) -> None: """Initialize.""" self._entry = entry diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index dc8a85ce2aacd..e33edcc2ab5fc 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -3,10 +3,7 @@ "name": "ReCollect Waste", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/recollect_waste", - "requirements": [ - "aiorecollect==1.0.1" - ], - "codeowners": [ - "@bachya" - ] + "requirements": ["aiorecollect==1.0.4"], + "codeowners": ["@bachya"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 1c3dabc2c87ce..68c810bc90d7c 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,20 +1,25 @@ """Support for ReCollect Waste sensors.""" from __future__ import annotations -from typing import Callable - from aiorecollect.client import PickupType import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_FRIENDLY_NAME, + CONF_NAME, + DEVICE_CLASS_TIMESTAMP, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) +from homeassistant.util.dt import as_utc from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER @@ -25,7 +30,6 @@ DEFAULT_ATTRIBUTION = "Pickup data provided by ReCollect Waste" DEFAULT_NAME = "recollect_waste" -DEFAULT_ICON = "mdi:trash-can-outline" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -52,7 +56,7 @@ def async_get_pickup_type_names( async def async_setup_platform( hass: HomeAssistant, config: dict, - async_add_entities: Callable, + async_add_entities: AddEntitiesCallback, discovery_info: dict = None, ): """Import Recollect Waste configuration from YAML.""" @@ -70,7 +74,7 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ReCollect Waste sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] @@ -87,16 +91,16 @@ def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> No self._entry = entry self._state = None + @property + def device_class(self) -> dict: + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + @property def extra_state_attributes(self) -> dict: """Return the state attributes.""" return self._attributes - @property - def icon(self) -> str: - """Icon to use in the frontend.""" - return DEFAULT_ICON - @property def name(self) -> str: """Return the name of the sensor.""" @@ -128,9 +132,8 @@ def update_from_latest_data(self) -> None: """Update the state.""" pickup_event = self.coordinator.data[0] next_pickup_event = self.coordinator.data[1] - next_date = str(next_pickup_event.date) - self._state = pickup_event.date + self._state = as_utc(pickup_event.date).isoformat() self._attributes.update( { ATTR_PICKUP_TYPES: async_get_pickup_type_names( @@ -140,6 +143,6 @@ def update_from_latest_data(self) -> None: ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names( self._entry, next_pickup_event.pickup_types ), - ATTR_NEXT_PICKUP_DATE: next_date, + ATTR_NEXT_PICKUP_DATE: as_utc(next_pickup_event.date).isoformat(), } ) diff --git a/homeassistant/components/recollect_waste/translations/de.json b/homeassistant/components/recollect_waste/translations/de.json index 7cbcea1b25e30..fdeab56f54e4c 100644 --- a/homeassistant/components/recollect_waste/translations/de.json +++ b/homeassistant/components/recollect_waste/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, + "error": { + "invalid_place_or_service_id": "Ung\u00fcltige Orts- oder Service-ID" + }, "step": { "user": { "data": { @@ -15,6 +18,9 @@ "options": { "step": { "init": { + "data": { + "friendly_name": "Verwenden Sie freundliche Namen f\u00fcr Pickup-Typen (wenn m\u00f6glich)" + }, "title": "Recollect Waste konfigurieren" } } diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json index 75615c1cce7f5..2444a2027200b 100644 --- a/homeassistant/components/recollect_waste/translations/zh-Hant.json +++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_place_or_service_id": "\u5730\u9ede\u6216\u670d\u52d9 ID \u7121\u6548" diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f93d965a4b9d0..0d6dddfa2d57a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -3,7 +3,7 @@ import asyncio import concurrent.futures -from datetime import datetime +from datetime import datetime, timedelta import logging import queue import sqlite3 @@ -12,6 +12,7 @@ from typing import Any, Callable, NamedTuple from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool import voluptuous as vol @@ -20,7 +21,8 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_EXCLUDE, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, @@ -32,23 +34,38 @@ INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER, convert_include_exclude_filter, + generate_filter, ) +from homeassistant.helpers.event import ( + async_track_time_change, + async_track_time_interval, +) +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) +from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -from . import migration, purge +from . import history, migration, purge, statistics from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX from .models import Base, Events, RecorderRuns, States +from .pool import RecorderPool from .util import ( dburl_to_path, + end_incomplete_runs, move_away_broken_database, + perodic_db_cleanups, session_scope, + setup_connection_for_dialect, validate_or_move_away_sqlite_database, ) _LOGGER = logging.getLogger(__name__) SERVICE_PURGE = "purge" +SERVICE_PURGE_ENTITIES = "purge_entities" SERVICE_ENABLE = "enable" SERVICE_DISABLE = "disable" @@ -56,6 +73,8 @@ ATTR_REPACK = "repack" ATTR_APPLY_FILTER = "apply_filter" +MAX_QUEUE_BACKLOG = 30000 + SERVICE_PURGE_SCHEMA = vol.Schema( { vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, @@ -63,6 +82,18 @@ vol.Optional(ATTR_APPLY_FILTER, default=False): cv.boolean, } ) + +ATTR_DOMAINS = "domains" +ATTR_ENTITY_GLOBS = "entity_globs" + +SERVICE_PURGE_ENTITIES_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } +).extend(cv.ENTITY_SERVICE_FIELDS) SERVICE_ENABLE_SCHEMA = vol.Schema({}) SERVICE_DISABLE_SCHEMA = vol.Schema({}) @@ -87,6 +118,9 @@ CONF_EVENT_TYPES = "event_types" CONF_COMMIT_INTERVAL = "commit_interval" +INVALIDATED_ERR = "Database connection invalidated" +CONNECTIVITY_ERR = "Error in database connectivity during commit" + EXCLUDE_SCHEMA = INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER.extend( {vol.Optional(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string])} ) @@ -99,6 +133,7 @@ { vol.Optional(DOMAIN, default=dict): vol.All( cv.deprecated(CONF_PURGE_INTERVAL), + cv.deprecated(CONF_DB_INTEGRITY_CHECK), FILTER_SCHEMA.extend( { vol.Optional(CONF_AUTO_PURGE, default=True): cv.boolean, @@ -127,6 +162,18 @@ ) +@bind_hass +async def async_migration_in_progress(hass: HomeAssistant) -> bool: + """Determine is a migration is in progress. + + This is a thin wrapper that allows us to change + out the implementation later. + """ + if DATA_INSTANCE not in hass.data: + return False + return hass.data[DATA_INSTANCE].migration_in_progress + + def run_information(hass, point_in_time: datetime | None = None): """Return information about current run. @@ -169,6 +216,7 @@ def run_information_with_session(session, point_in_time: datetime | None = None) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" + hass.data[DOMAIN] = {} conf = config[DOMAIN] entity_filter = convert_include_exclude_filter(conf) auto_purge = conf[CONF_AUTO_PURGE] @@ -176,11 +224,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: commit_interval = conf[CONF_COMMIT_INTERVAL] db_max_retries = conf[CONF_DB_MAX_RETRIES] db_retry_wait = conf[CONF_DB_RETRY_WAIT] - db_integrity_check = conf[CONF_DB_INTEGRITY_CHECK] - - db_url = conf.get(CONF_DB_URL) - if not db_url: - db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + db_url = conf.get(CONF_DB_URL) or DEFAULT_URL.format( + hass_config_path=hass.config.path(DEFAULT_DB_FILE) + ) exclude = conf[CONF_EXCLUDE] exclude_t = exclude.get(CONF_EVENT_TYPES, []) instance = hass.data[DATA_INSTANCE] = Recorder( @@ -193,10 +239,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: db_retry_wait=db_retry_wait, entity_filter=entity_filter, exclude_t=exclude_t, - db_integrity_check=db_integrity_check, ) instance.async_initialize() instance.start() + _async_register_services(hass, instance) + history.async_setup(hass) + statistics.async_setup(hass) + await async_process_integration_platforms(hass, DOMAIN, _process_recorder_platform) + + return await instance.async_db_ready + + +async def _process_recorder_platform(hass, domain, platform): + """Process a recorder platform.""" + hass.data[DOMAIN][domain] = platform + + +@callback +def _async_register_services(hass, instance): + """Register recorder services.""" async def async_handle_purge_service(service): """Handle calls to the purge service.""" @@ -206,11 +267,29 @@ async def async_handle_purge_service(service): DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA ) - async def async_handle_enable_sevice(service): + async def async_handle_purge_entities_service(service): + """Handle calls to the purge entities service.""" + entity_ids = await async_extract_entity_ids(hass, service) + domains = service.data.get(ATTR_DOMAINS, []) + entity_globs = service.data.get(ATTR_ENTITY_GLOBS, []) + + instance.do_adhoc_purge_entities(entity_ids, domains, entity_globs) + + hass.services.async_register( + DOMAIN, + SERVICE_PURGE_ENTITIES, + async_handle_purge_entities_service, + schema=SERVICE_PURGE_ENTITIES_SCHEMA, + ) + + async def async_handle_enable_service(service): instance.set_enable(True) hass.services.async_register( - DOMAIN, SERVICE_ENABLE, async_handle_enable_sevice, schema=SERVICE_ENABLE_SCHEMA + DOMAIN, + SERVICE_ENABLE, + async_handle_enable_service, + schema=SERVICE_ENABLE_SCHEMA, ) async def async_handle_disable_service(service): @@ -223,8 +302,6 @@ async def async_handle_disable_service(service): schema=SERVICE_DISABLE_SCHEMA, ) - return await instance.async_db_ready - class PurgeTask(NamedTuple): """Object to store information about purge task.""" @@ -234,6 +311,22 @@ class PurgeTask(NamedTuple): apply_filter: bool +class PurgeEntitiesTask(NamedTuple): + """Object to store entity information about purge task.""" + + entity_filter: Callable[[str], bool] + + +class PerodicCleanupTask: + """An object to insert into the recorder to trigger cleanup tasks when auto purge is disabled.""" + + +class StatisticsTask(NamedTuple): + """An object to insert into the recorder queue to run a statistics task.""" + + start: datetime.datetime + + class WaitTask: """An object to insert into the recorder queue to tell it set the _queue_watch event.""" @@ -252,7 +345,6 @@ def __init__( db_retry_wait: int, entity_filter: Callable[[str], bool], exclude_t: list[str], - db_integrity_check: bool, ) -> None: """Initialize the recorder.""" threading.Thread.__init__(self, name="Recorder") @@ -266,8 +358,8 @@ def __init__( self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait - self.db_integrity_check = db_integrity_check self.async_db_ready = asyncio.Future() + self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() self.engine: Any = None self.run_info: Any = None @@ -282,7 +374,11 @@ def __init__( self._pending_expunge = [] self.event_session = None self.get_session = None - self._completed_database_setup = None + self._completed_first_database_setup = None + self._event_listener = None + self.async_migration_event = asyncio.Event() + self.migration_in_progress = False + self._queue_watcher = None self.enabled = True @@ -293,18 +389,61 @@ def set_enable(self, enable): @callback def async_initialize(self): """Initialize the recorder.""" - self.hass.bus.async_listen( + self._event_listener = self.hass.bus.async_listen( MATCH_ALL, self.event_listener, event_filter=self._async_event_filter ) + self._queue_watcher = async_track_time_interval( + self.hass, self._async_check_queue, timedelta(minutes=10) + ) + + @callback + def _async_check_queue(self, *_): + """Periodic check of the queue size to ensure we do not exaust memory. + + The queue grows during migraton or if something really goes wrong. + """ + size = self.queue.qsize() + _LOGGER.debug("Recorder queue size is: %s", size) + if self.queue.qsize() <= MAX_QUEUE_BACKLOG: + return + _LOGGER.error( + "The recorder queue reached the maximum size of %s; Events are no longer being recorded", + MAX_QUEUE_BACKLOG, + ) + self._async_stop_queue_watcher_and_event_listener() + + @callback + def _async_stop_queue_watcher_and_event_listener(self): + """Stop watching the queue and listening for events.""" + if self._queue_watcher: + self._queue_watcher() + self._queue_watcher = None + if self._event_listener: + self._event_listener() + self._event_listener = None @callback - def _async_event_filter(self, event): + def _async_event_filter(self, event) -> bool: """Filter events.""" if event.event_type in self.exclude_t: return False entity_id = event.data.get(ATTR_ENTITY_ID) - return bool(entity_id is None or self.entity_filter(entity_id)) + + if entity_id is None: + return True + + if isinstance(entity_id, str): + return self.entity_filter(entity_id) + + if isinstance(entity_id, list): + for eid in entity_id: + if self.entity_filter(eid): + return True + return False + + # Unknown what it is. + return True def do_adhoc_purge(self, **kwargs): """Trigger an adhoc purge retaining keep_days worth of data.""" @@ -314,89 +453,205 @@ def do_adhoc_purge(self, **kwargs): self.queue.put(PurgeTask(keep_days, repack, apply_filter)) - def run(self): - """Start processing events to save.""" + def do_adhoc_purge_entities(self, entity_ids, domains, entity_globs): + """Trigger an adhoc purge of requested entities.""" + entity_filter = generate_filter(domains, entity_ids, [], [], entity_globs) + self.queue.put(PurgeEntitiesTask(entity_filter)) - if not self._setup_recorder(): - return + def do_adhoc_statistics(self, **kwargs): + """Trigger an adhoc statistics run.""" + start = kwargs.get("start") + if not start: + start = statistics.get_start_time() + self.queue.put(StatisticsTask(start)) - shutdown_task = object() - hass_started = concurrent.futures.Future() + @callback + def async_register(self, shutdown_task, hass_started): + """Post connection initialize.""" + + def _empty_queue(event): + """Empty the queue if its still present at final write.""" + + # If the queue is full of events to be processed because + # the database is so broken that every event results in a retry + # we will never be able to get though the events to shutdown in time. + # + # We drain all the events in the queue and then insert + # an empty one to ensure the next thing the recorder sees + # is a request to shutdown. + while True: + try: + self.queue.get_nowait() + except queue.Empty: + break + self.queue.put(None) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, _empty_queue) + + def shutdown(event): + """Shut down the Recorder.""" + if not hass_started.done(): + hass_started.set_result(shutdown_task) + self.queue.put(None) + self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) + self.join() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + + if self.hass.state == CoreState.running: + hass_started.set_result(None) + return @callback - def register(): - """Post connection initialize.""" - self.async_db_ready.set_result(True) + def async_hass_started(event): + """Notify that hass has started.""" + hass_started.set_result(None) - def shutdown(event): - """Shut down the Recorder.""" - if not hass_started.done(): - hass_started.set_result(shutdown_task) - self.queue.put(None) - self.join() + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, async_hass_started) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + @callback + def async_connection_failed(self): + """Connect failed tasks.""" + self.async_db_ready.set_result(False) + persistent_notification.async_create( + self.hass, + "The recorder could not start, check [the logs](/config/logs)", + "Recorder", + ) + self._async_stop_queue_watcher_and_event_listener() - if self.hass.state == CoreState.running: - hass_started.set_result(None) - else: + @callback + def async_connection_success(self): + """Connect success tasks.""" + self.async_db_ready.set_result(True) - @callback - def notify_hass_started(event): - """Notify that hass has started.""" - hass_started.set_result(None) + @callback + def _async_recorder_ready(self): + """Finish start and mark recorder ready.""" + self._async_setup_periodic_tasks() + self.async_recorder_ready.set() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, notify_hass_started - ) + @callback + def async_nightly_tasks(self, now): + """Trigger the purge.""" + if self.auto_purge: + # Purge will schedule the perodic cleanups + # after it completes to ensure it does not happen + # until after the database is vacuumed + self.queue.put(PurgeTask(self.keep_days, repack=False, apply_filter=False)) + else: + self.queue.put(PerodicCleanupTask()) + + @callback + def async_hourly_statistics(self, now): + """Trigger the hourly statistics run.""" + start = statistics.get_start_time() + self.queue.put(StatisticsTask(start)) + + def _async_setup_periodic_tasks(self): + """Prepare periodic tasks.""" + # Run nightly tasks at 4:12am + async_track_time_change( + self.hass, self.async_nightly_tasks, hour=4, minute=12, second=0 + ) + # Compile hourly statistics every hour at *:12 + async_track_time_change( + self.hass, self.async_hourly_statistics, minute=12, second=0 + ) + + def run(self): + """Start processing events to save.""" + shutdown_task = object() + hass_started = concurrent.futures.Future() + + self.hass.add_job(self.async_register, shutdown_task, hass_started) - self.hass.add_job(register) - result = hass_started.result() + current_version = self._setup_recorder() + if current_version is None: + self.hass.add_job(self.async_connection_failed) + return + + schema_is_current = migration.schema_is_current(current_version) + if schema_is_current: + self._setup_run() + else: + self.migration_in_progress = True + + self.hass.add_job(self.async_connection_success) # If shutdown happened before Home Assistant finished starting - if result is shutdown_task: + if hass_started.result() is shutdown_task: + self.migration_in_progress = False # Make sure we cleanly close the run if # we restart before startup finishes self._shutdown() return - # Start periodic purge - if self.auto_purge: - - @callback - def async_purge(now): - """Trigger the purge.""" - self.queue.put( - PurgeTask(self.keep_days, repack=False, apply_filter=False) + # We wait to start the migration until startup has finished + # since it can be cpu intensive and we do not want it to compete + # with startup which is also cpu intensive + if not schema_is_current: + if self._migrate_schema_and_setup_run(current_version): + if not self._event_listener: + # If the schema migration takes so longer that the end + # queue watcher safety kicks in because MAX_QUEUE_BACKLOG + # is reached, we need to reinitialize the listener. + self.hass.add_job(self.async_initialize) + else: + persistent_notification.create( + self.hass, + "The database migration failed, check [the logs](/config/logs)." + "Database Migration Failed", + "recorder_database_migration", ) - - # Purge every night at 4:12am - self.hass.helpers.event.track_time_change( - async_purge, hour=4, minute=12, second=0 - ) + self._shutdown() + return _LOGGER.debug("Recorder processing the queue") + self.hass.add_job(self._async_recorder_ready) + self._run_event_loop() + + def _run_event_loop(self): + """Run the event loop for the recorder.""" # Use a session for the event read loop # with a commit every time the event time # has changed. This reduces the disk io. - while True: - event = self.queue.get() + while event := self.queue.get(): + try: + self._process_one_event_or_recover(event) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while processing event %s: %s", event, err) - if event is None: - self._shutdown() - return + self._shutdown() + def _process_one_event_or_recover(self, event): + """Process an event, reconnect, or recover a malformed database.""" + try: self._process_one_event(event) + return + except exc.DatabaseError as err: + if self._handle_database_error(err): + return + _LOGGER.exception( + "Unhandled database error while processing event %s: %s", event, err + ) + except SQLAlchemyError as err: + _LOGGER.exception( + "SQLAlchemyError error processing event %s: %s", event, err + ) + + # Reset the session if an SQLAlchemyError (including DatabaseError) + # happens to rollback and recover + self._reopen_event_session() - def _setup_recorder(self) -> bool: - """Create schema and connect to the database.""" + def _setup_recorder(self) -> None | int: + """Create connect to the database and get the schema version.""" tries = 1 while tries <= self.db_max_retries: try: self._setup_connection() - migration.migrate_schema(self) - self._setup_run() + return migration.get_schema_version(self) except Exception as err: # pylint: disable=broad-except _LOGGER.exception( "Error during connection setup to %s: %s (retrying in %s seconds)", @@ -404,37 +659,81 @@ def _setup_recorder(self) -> bool: err, self.db_retry_wait, ) - else: - _LOGGER.debug("Connected to recorder database") - self._open_event_session() - return True - tries += 1 time.sleep(self.db_retry_wait) - @callback - def connection_failed(): - """Connect failed tasks.""" - self.async_db_ready.set_result(False) - persistent_notification.async_create( - self.hass, - "The recorder could not start, please check the log", - "Recorder", - ) + return None - self.hass.add_job(connection_failed) - return False + @callback + def _async_migration_started(self): + """Set the migration started event.""" + self.async_migration_event.set() + + def _migrate_schema_and_setup_run(self, current_version) -> bool: + """Migrate schema to the latest version.""" + persistent_notification.create( + self.hass, + "System performance will temporarily degrade during the database upgrade. Do not power down or restart the system until the upgrade completes. Integrations that read the database, such as logbook and history, may return inconsistent results until the upgrade completes.", + "Database upgrade in progress", + "recorder_database_migration", + ) + self.hass.add_job(self._async_migration_started) + + try: + migration.migrate_schema(self, current_version) + except exc.DatabaseError as err: + if self._handle_database_error(err): + return True + _LOGGER.exception("Database error during schema migration") + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error during schema migration") + return False + else: + self._setup_run() + return True + finally: + self.migration_in_progress = False + persistent_notification.dismiss(self.hass, "recorder_database_migration") + + def _run_purge(self, keep_days, repack, apply_filter): + """Purge the database.""" + if purge.purge_old_data(self, keep_days, repack, apply_filter): + # We always need to do the db cleanups after a purge + # is finished to ensure the WAL checkpoint and other + # tasks happen after a vacuum. + perodic_db_cleanups(self) + return + # Schedule a new purge task if this one didn't finish + self.queue.put(PurgeTask(keep_days, repack, apply_filter)) + + def _run_purge_entities(self, entity_filter): + """Purge entities from the database.""" + if purge.purge_entity_data(self, entity_filter): + return + # Schedule a new purge task if this one didn't finish + self.queue.put(PurgeEntitiesTask(entity_filter)) + + def _run_statistics(self, start): + """Run statistics task.""" + if statistics.compile_statistics(self, start): + return + # Schedule a new statistics task if this one didn't finish + self.queue.put(StatisticsTask(start)) def _process_one_event(self, event): """Process one event.""" if isinstance(event, PurgeTask): - # Schedule a new purge task if this one didn't finish - if not purge.purge_old_data( - self, event.keep_days, event.repack, event.apply_filter - ): - self.queue.put( - PurgeTask(event.keep_days, event.repack, event.apply_filter) - ) + self._run_purge(event.keep_days, event.repack, event.apply_filter) + return + if isinstance(event, PurgeEntitiesTask): + self._run_purge_entities(event.entity_filter) + return + if isinstance(event, PerodicCleanupTask): + perodic_db_cleanups(self) + return + if isinstance(event, StatisticsTask): + self._run_statistics(event.start) return if isinstance(event, WaitTask): self._queue_watch.set() @@ -448,7 +747,7 @@ def _process_one_event(self, event): self._timechanges_seen += 1 if self._timechanges_seen >= self.commit_interval: self._timechanges_seen = 0 - self._commit_event_session_or_recover() + self._commit_event_session_or_retry() return if not self.enabled: @@ -464,10 +763,6 @@ def _process_one_event(self, event): except (TypeError, ValueError): _LOGGER.warning("Event is not JSON serializable: %s", event) return - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error adding event: %s", err) - return if event.event_type == EVENT_STATE_CHANGED: try: @@ -492,49 +787,35 @@ def _process_one_event(self, event): "State is not JSON serializable: %s", event.data.get("new_state"), ) - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Error adding state change: %s", err) # If they do not have a commit interval # than we commit right away if not self.commit_interval: - self._commit_event_session_or_recover() - - def _commit_event_session_or_recover(self): - """Commit changes to the database and recover if the database fails when possible.""" - try: self._commit_event_session_or_retry() - return - except exc.DatabaseError as err: - if isinstance(err.__cause__, sqlite3.DatabaseError): - _LOGGER.exception( - "Unrecoverable sqlite3 database corruption detected: %s", err - ) - self._handle_sqlite_corruption() - return - _LOGGER.exception("Unexpected error saving events: %s", err) - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing - _LOGGER.exception("Unexpected error saving events: %s", err) - self._reopen_event_session() - return + def _handle_database_error(self, err): + """Handle a database error that may result in moving away the corrupt db.""" + if isinstance(err.__cause__, sqlite3.DatabaseError): + _LOGGER.exception( + "Unrecoverable sqlite3 database corruption detected: %s", err + ) + self._handle_sqlite_corruption() + return True + return False def _commit_event_session_or_retry(self): + """Commit the event session if there is work to do.""" + if not self.event_session.new and not self.event_session.dirty: + return tries = 1 while tries <= self.db_max_retries: try: self._commit_event_session() return except (exc.InternalError, exc.OperationalError) as err: - if err.connection_invalidated: - message = "Database connection invalidated" - else: - message = "Error in database connectivity during commit" _LOGGER.error( "%s: Error executing query: %s. (retrying in %s seconds)", - message, + INVALIDATED_ERR if err.connection_invalidated else CONNECTIVITY_ERR, err, self.db_retry_wait, ) @@ -566,44 +847,41 @@ def _commit_event_session(self): def _handle_sqlite_corruption(self): """Handle the sqlite3 database being corrupt.""" + self._close_event_session() self._close_connection() move_away_broken_database(dburl_to_path(self.db_url)) self._setup_recorder() + self._setup_run() - def _reopen_event_session(self): - """Rollback the event session and reopen it after a failure.""" + def _close_event_session(self): + """Close the event session.""" self._old_states = {} + if not self.event_session: + return + try: self.event_session.rollback() self.event_session.close() - except Exception as err: # pylint: disable=broad-except - # Must catch the exception to prevent the loop from collapsing + except SQLAlchemyError as err: _LOGGER.exception( "Error while rolling back and closing the event session: %s", err ) + def _reopen_event_session(self): + """Rollback the event session and reopen it after a failure.""" + self._close_event_session() self._open_event_session() def _open_event_session(self): """Open the event session.""" - try: - self.event_session = self.get_session() - self.event_session.expire_on_commit = False - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error while creating new event session: %s", err) + self.event_session = self.get_session() + self.event_session.expire_on_commit = False def _send_keep_alive(self): - try: - _LOGGER.debug("Sending keepalive") - self.event_session.connection().scalar(select([1])) - return - except Exception as err: # pylint: disable=broad-except - _LOGGER.error( - "Error in database connectivity during keepalive: %s", - err, - ) - self._reopen_event_session() + """Send a keep alive to keep the db connection open.""" + _LOGGER.debug("Sending keepalive") + self.event_session.connection().scalar(select([1])) @callback def event_listener(self, event): @@ -629,54 +907,28 @@ def block_till_done(self): def _setup_connection(self): """Ensure database is ready to fly.""" kwargs = {} - self._completed_database_setup = False + self._completed_first_database_setup = False def setup_recorder_connection(dbapi_connection, connection_record): """Dbapi specific connection settings.""" - if self._completed_database_setup: - return - - # We do not import sqlite3 here so mysql/other - # users do not have to pay for it to be loaded in - # memory - if self.db_url.startswith(SQLITE_URL_PREFIX): - old_isolation = dbapi_connection.isolation_level - dbapi_connection.isolation_level = None - cursor = dbapi_connection.cursor() - cursor.execute("PRAGMA journal_mode=WAL") - cursor.close() - dbapi_connection.isolation_level = old_isolation - # WAL mode only needs to be setup once - # instead of every time we open the sqlite connection - # as its persistent and isn't free to call every time. - self._completed_database_setup = True - elif self.db_url.startswith("mysql"): - cursor = dbapi_connection.cursor() - cursor.execute("SET session wait_timeout=28800") - cursor.close() + setup_connection_for_dialect( + self.engine.dialect.name, + dbapi_connection, + not self._completed_first_database_setup, + ) + self._completed_first_database_setup = True if self.db_url == SQLITE_URL_PREFIX or ":memory:" in self.db_url: kwargs["connect_args"] = {"check_same_thread": False} kwargs["poolclass"] = StaticPool kwargs["pool_reset_on_return"] = None + elif self.db_url.startswith(SQLITE_URL_PREFIX): + kwargs["poolclass"] = RecorderPool else: kwargs["echo"] = False if self._using_file_sqlite: - with self.hass.timeout.freeze(DOMAIN): - # - # Here we run an sqlite3 quick_check. In the majority - # of cases, the quick_check takes under 10 seconds. - # - # On systems with very large databases and - # very slow disk or cpus, this can take a while. - # - validate_or_move_away_sqlite_database( - self.db_url, self.db_integrity_check - ) - - if self.engine is not None: - self.engine.dispose() + validate_or_move_away_sqlite_database(self.db_url) self.engine = create_engine(self.db_url, **kwargs) @@ -684,6 +936,7 @@ def setup_recorder_connection(dbapi_connection, connection_record): Base.metadata.create_all(self.engine) self.get_session = scoped_session(sessionmaker(bind=self.engine)) + _LOGGER.debug("Connected to recorder database") @property def _using_file_sqlite(self): @@ -701,33 +954,31 @@ def _close_connection(self): def _setup_run(self): """Log the start of the current run.""" with session_scope(session=self.get_session()) as session: - for run in session.query(RecorderRuns).filter_by(end=None): - run.closed_incorrect = True - run.end = self.recording_start - _LOGGER.warning( - "Ended unfinished session (id=%s from %s)", run.run_id, run.start - ) - session.add(run) - - self.run_info = RecorderRuns( - start=self.recording_start, created=dt_util.utcnow() - ) + start = self.recording_start + end_incomplete_runs(session, start) + self.run_info = RecorderRuns(start=start, created=dt_util.utcnow()) session.add(self.run_info) session.flush() session.expunge(self.run_info) - def _shutdown(self): - """Save end time for current run.""" - if self.event_session is not None: + self._open_event_session() + + def _end_session(self): + """End the recorder session.""" + if self.event_session is None: + return + try: self.run_info.end = dt_util.utcnow() self.event_session.add(self.run_info) - try: - self._commit_event_session_or_retry() - self.event_session.close() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception( - "Error saving the event session during shutdown: %s", err - ) + self._commit_event_session_or_retry() + self.event_session.close() + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error saving the event session during shutdown: %s", err) self.run_info = None + + def _shutdown(self): + """Save end time for current run.""" + self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) + self._end_session() self._close_connection() diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py new file mode 100644 index 0000000000000..6c89fef2be3f5 --- /dev/null +++ b/homeassistant/components/recorder/history.py @@ -0,0 +1,403 @@ +"""Provide pre-made queries on top of the recorder component.""" +from __future__ import annotations + +from collections import defaultdict +from itertools import groupby +import logging +import time + +from sqlalchemy import and_, bindparam, func +from sqlalchemy.ext import baked + +from homeassistant.components import recorder +from homeassistant.components.recorder.models import ( + States, + process_timestamp_to_utc_isoformat, +) +from homeassistant.components.recorder.util import execute, session_scope +from homeassistant.core import split_entity_id +import homeassistant.util.dt as dt_util + +from .models import LazyState + +# mypy: allow-untyped-defs, no-check-untyped-defs + +_LOGGER = logging.getLogger(__name__) + +STATE_KEY = "state" +LAST_CHANGED_KEY = "last_changed" + +SIGNIFICANT_DOMAINS = ( + "climate", + "device_tracker", + "humidifier", + "thermostat", + "water_heater", +) +IGNORE_DOMAINS = ("zone", "scene") +NEED_ATTRIBUTE_DOMAINS = { + "climate", + "humidifier", + "input_datetime", + "thermostat", + "water_heater", +} + +QUERY_STATES = [ + States.domain, + States.entity_id, + States.state, + States.attributes, + States.last_changed, + States.last_updated, +] + +HISTORY_BAKERY = "recorder_history_bakery" + + +def async_setup(hass): + """Set up the history hooks.""" + hass.data[HISTORY_BAKERY] = baked.bakery() + + +def get_significant_states(hass, *args, **kwargs): + """Wrap _get_significant_states with a sql session.""" + with session_scope(hass=hass) as session: + return _get_significant_states(hass, session, *args, **kwargs) + + +def _get_significant_states( + hass, + session, + start_time, + end_time=None, + entity_ids=None, + filters=None, + include_start_time_state=True, + significant_changes_only=True, + minimal_response=False, +): + """ + Return states changes during UTC period start_time - end_time. + + Significant states are all states where there is a state change, + as well as all states from certain domains (for instance + thermostat so that we get current temperature in our graphs). + """ + timer_start = time.perf_counter() + + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + + if significant_changes_only: + baked_query += lambda q: q.filter( + ( + States.domain.in_(SIGNIFICANT_DOMAINS) + | (States.last_changed == States.last_updated) + ) + & (States.last_updated > bindparam("start_time")) + ) + else: + baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time")) + + if entity_ids is not None: + baked_query += lambda q: q.filter( + States.entity_id.in_(bindparam("entity_ids", expanding=True)) + ) + else: + baked_query += lambda q: q.filter(~States.domain.in_(IGNORE_DOMAINS)) + if filters: + filters.bake(baked_query) + + if end_time is not None: + baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) + + baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) + + states = execute( + baked_query(session).params( + start_time=start_time, end_time=end_time, entity_ids=entity_ids + ) + ) + + if _LOGGER.isEnabledFor(logging.DEBUG): + elapsed = time.perf_counter() - timer_start + _LOGGER.debug("get_significant_states took %fs", elapsed) + + return _sorted_states_to_dict( + hass, + session, + states, + start_time, + entity_ids, + filters, + include_start_time_state, + minimal_response, + ) + + +def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): + """Return states changes during UTC period start_time - end_time.""" + with session_scope(hass=hass) as session: + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + + baked_query += lambda q: q.filter( + (States.last_changed == States.last_updated) + & (States.last_updated > bindparam("start_time")) + ) + + if end_time is not None: + baked_query += lambda q: q.filter( + States.last_updated < bindparam("end_time") + ) + + if entity_id is not None: + baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) + entity_id = entity_id.lower() + + baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) + + states = execute( + baked_query(session).params( + start_time=start_time, end_time=end_time, entity_id=entity_id + ) + ) + + entity_ids = [entity_id] if entity_id is not None else None + + return _sorted_states_to_dict(hass, session, states, start_time, entity_ids) + + +def get_last_state_changes(hass, number_of_states, entity_id): + """Return the last number_of_states.""" + start_time = dt_util.utcnow() + + with session_scope(hass=hass) as session: + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + baked_query += lambda q: q.filter(States.last_changed == States.last_updated) + + if entity_id is not None: + baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) + entity_id = entity_id.lower() + + baked_query += lambda q: q.order_by( + States.entity_id, States.last_updated.desc() + ) + + baked_query += lambda q: q.limit(bindparam("number_of_states")) + + states = execute( + baked_query(session).params( + number_of_states=number_of_states, entity_id=entity_id + ) + ) + + entity_ids = [entity_id] if entity_id is not None else None + + return _sorted_states_to_dict( + hass, + session, + reversed(states), + start_time, + entity_ids, + include_start_time_state=False, + ) + + +def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): + """Return the states at a specific point in time.""" + if run is None: + run = recorder.run_information_from_instance(hass, utc_point_in_time) + + # History did not run before utc_point_in_time + if run is None: + return [] + + with session_scope(hass=hass) as session: + return _get_states_with_session( + hass, session, utc_point_in_time, entity_ids, run, filters + ) + + +def _get_states_with_session( + hass, session, utc_point_in_time, entity_ids=None, run=None, filters=None +): + """Return the states at a specific point in time.""" + if entity_ids and len(entity_ids) == 1: + return _get_single_entity_states_with_session( + hass, session, utc_point_in_time, entity_ids[0] + ) + + if run is None: + run = recorder.run_information_with_session(session, utc_point_in_time) + + # History did not run before utc_point_in_time + if run is None: + return [] + + # We have more than one entity to look at (most commonly we want + # all entities,) so we need to do a search on all states since the + # last recorder run started. + query = session.query(*QUERY_STATES) + + most_recent_states_by_date = session.query( + States.entity_id.label("max_entity_id"), + func.max(States.last_updated).label("max_last_updated"), + ).filter( + (States.last_updated >= run.start) & (States.last_updated < utc_point_in_time) + ) + + if entity_ids: + most_recent_states_by_date.filter(States.entity_id.in_(entity_ids)) + + most_recent_states_by_date = most_recent_states_by_date.group_by(States.entity_id) + + most_recent_states_by_date = most_recent_states_by_date.subquery() + + most_recent_state_ids = session.query( + func.max(States.state_id).label("max_state_id") + ).join( + most_recent_states_by_date, + and_( + States.entity_id == most_recent_states_by_date.c.max_entity_id, + States.last_updated == most_recent_states_by_date.c.max_last_updated, + ), + ) + + most_recent_state_ids = most_recent_state_ids.group_by(States.entity_id) + + most_recent_state_ids = most_recent_state_ids.subquery() + + query = query.join( + most_recent_state_ids, + States.state_id == most_recent_state_ids.c.max_state_id, + ) + + if entity_ids is not None: + query = query.filter(States.entity_id.in_(entity_ids)) + else: + query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) + if filters: + query = filters.apply(query) + + return [LazyState(row) for row in execute(query)] + + +def _get_single_entity_states_with_session(hass, session, utc_point_in_time, entity_id): + # Use an entirely different (and extremely fast) query if we only + # have a single entity id + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + baked_query += lambda q: q.filter( + States.last_updated < bindparam("utc_point_in_time"), + States.entity_id == bindparam("entity_id"), + ) + baked_query += lambda q: q.order_by(States.last_updated.desc()) + baked_query += lambda q: q.limit(1) + + query = baked_query(session).params( + utc_point_in_time=utc_point_in_time, entity_id=entity_id + ) + + return [LazyState(row) for row in execute(query)] + + +def _sorted_states_to_dict( + hass, + session, + states, + start_time, + entity_ids, + filters=None, + include_start_time_state=True, + minimal_response=False, +): + """Convert SQL results into JSON friendly data structure. + + This takes our state list and turns it into a JSON friendly data + structure {'entity_id': [list of states], 'entity_id2': [list of states]} + + States must be sorted by entity_id and last_updated + + We also need to go back and create a synthetic zero data point for + each list of states, otherwise our graphs won't start on the Y + axis correctly. + """ + result = defaultdict(list) + # Set all entity IDs to empty lists in result set to maintain the order + if entity_ids is not None: + for ent_id in entity_ids: + result[ent_id] = [] + + # Get the states at the start time + timer_start = time.perf_counter() + if include_start_time_state: + run = recorder.run_information_from_instance(hass, start_time) + for state in _get_states_with_session( + hass, session, start_time, entity_ids, run=run, filters=filters + ): + state.last_changed = start_time + state.last_updated = start_time + result[state.entity_id].append(state) + + if _LOGGER.isEnabledFor(logging.DEBUG): + elapsed = time.perf_counter() - timer_start + _LOGGER.debug("getting %d first datapoints took %fs", len(result), elapsed) + + # Called in a tight loop so cache the function + # here + _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat + + # Append all changes to it + for ent_id, group in groupby(states, lambda state: state.entity_id): + domain = split_entity_id(ent_id)[0] + ent_results = result[ent_id] + if not minimal_response or domain in NEED_ATTRIBUTE_DOMAINS: + ent_results.extend(LazyState(db_state) for db_state in group) + + # With minimal response we only provide a native + # State for the first and last response. All the states + # in-between only provide the "state" and the + # "last_changed". + if not ent_results: + ent_results.append(LazyState(next(group))) + + prev_state = ent_results[-1] + initial_state_count = len(ent_results) + + for db_state in group: + # With minimal response we do not care about attribute + # changes so we can filter out duplicate states + if db_state.state == prev_state.state: + continue + + ent_results.append( + { + STATE_KEY: db_state.state, + LAST_CHANGED_KEY: _process_timestamp_to_utc_isoformat( + db_state.last_changed + ), + } + ) + prev_state = db_state + + if prev_state and len(ent_results) != initial_state_count: + # There was at least one state change + # replace the last minimal state with + # a full state + ent_results[-1] = LazyState(prev_state) + + # Filter out the empty lists if some states had 0 results. + return {key: val for key, val in result.items() if val} + + +def get_state(hass, utc_point_in_time, entity_id, run=None): + """Return a state at a specific point in time.""" + states = get_states(hass, utc_point_in_time, (entity_id,), run) + return states[0] if states else None diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index a7e5eb0814d79..6a3f6ae6b546a 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,8 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.23"], + "requirements": ["sqlalchemy==1.4.13"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 5ab2d9091727a..8e6c4861739bc 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1,8 +1,8 @@ """Schema migration helpers.""" import logging +import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, text -from sqlalchemy.engine import reflection from sqlalchemy.exc import ( InternalError, OperationalError, @@ -11,15 +11,25 @@ ) from sqlalchemy.schema import AddConstraint, DropConstraint -from .const import DOMAIN -from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges +from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges, Statistics from .util import session_scope _LOGGER = logging.getLogger(__name__) -def migrate_schema(instance): - """Check if the schema needs to be upgraded.""" +def raise_if_exception_missing_str(ex, match_substrs): + """Raise an exception if the exception and cause do not contain the match substrs.""" + lower_ex_strs = [str(ex).lower(), str(ex.__cause__).lower()] + for str_sub in match_substrs: + for exc_str in lower_ex_strs: + if exc_str and str_sub in exc_str: + return + + raise ex + + +def get_schema_version(instance): + """Get the schema version.""" with session_scope(session=instance.get_session()) as session: res = ( session.query(SchemaChanges) @@ -34,24 +44,30 @@ def migrate_schema(instance): "No schema version found. Inspected version: %s", current_version ) - if current_version == SCHEMA_VERSION: - return + return current_version + +def schema_is_current(current_version): + """Check if the schema is current.""" + return current_version == SCHEMA_VERSION + + +def migrate_schema(instance, current_version): + """Check if the schema needs to be upgraded.""" + with session_scope(session=instance.get_session()) as session: _LOGGER.warning( "Database is about to upgrade. Schema version: %s", current_version ) + for version in range(current_version, SCHEMA_VERSION): + new_version = version + 1 + _LOGGER.info("Upgrading recorder db schema to version %s", new_version) + _apply_update(instance.engine, session, new_version, current_version) + session.add(SchemaChanges(schema_version=new_version)) - with instance.hass.timeout.freeze(DOMAIN): - for version in range(current_version, SCHEMA_VERSION): - new_version = version + 1 - _LOGGER.info("Upgrading recorder db schema to version %s", new_version) - _apply_update(instance.engine, new_version, current_version) - session.add(SchemaChanges(schema_version=new_version)) - - _LOGGER.info("Upgrade to version %s done", new_version) + _LOGGER.info("Upgrade to version %s done", new_version) -def _create_index(engine, table_name, index_name): +def _create_index(connection, table_name, index_name): """Create an index for the specified table. The index name should match the name given for the index @@ -73,13 +89,9 @@ def _create_index(engine, table_name, index_name): index_name, ) try: - index.create(engine) + index.create(connection) except (InternalError, ProgrammingError, OperationalError) as err: - lower_err_str = str(err).lower() - - if "already exists" not in lower_err_str and "duplicate" not in lower_err_str: - raise - + raise_if_exception_missing_str(err, ["already exists", "duplicate"]) _LOGGER.warning( "Index %s already exists on %s, continuing", index_name, table_name ) @@ -87,7 +99,7 @@ def _create_index(engine, table_name, index_name): _LOGGER.debug("Finished creating %s", index_name) -def _drop_index(engine, table_name, index_name): +def _drop_index(connection, table_name, index_name): """Drop an index from a specified table. There is no universal way to do something like `DROP INDEX IF EXISTS` @@ -103,7 +115,7 @@ def _drop_index(engine, table_name, index_name): # Engines like DB2/Oracle try: - engine.execute(text(f"DROP INDEX {index_name}")) + connection.execute(text(f"DROP INDEX {index_name}")) except SQLAlchemyError: pass else: @@ -112,7 +124,7 @@ def _drop_index(engine, table_name, index_name): # Engines like SQLite, SQL Server if not success: try: - engine.execute( + connection.execute( text( "DROP INDEX {table}.{index}".format( index=index_name, table=table_name @@ -127,7 +139,7 @@ def _drop_index(engine, table_name, index_name): if not success: # Engines like MySQL, MS Access try: - engine.execute( + connection.execute( text( "DROP INDEX {index} ON {table}".format( index=index_name, table=table_name @@ -158,7 +170,7 @@ def _drop_index(engine, table_name, index_name): ) -def _add_columns(engine, table_name, columns_def): +def _add_columns(connection, table_name, columns_def): """Add columns to a table.""" _LOGGER.warning( "Adding columns %s to table %s. Note: this can take several " @@ -171,7 +183,7 @@ def _add_columns(engine, table_name, columns_def): columns_def = [f"ADD {col_def}" for col_def in columns_def] try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {columns_def}".format( table=table_name, columns_def=", ".join(columns_def) @@ -186,7 +198,7 @@ def _add_columns(engine, table_name, columns_def): for column_def in columns_def: try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {column_def}".format( table=table_name, column_def=column_def @@ -194,9 +206,7 @@ def _add_columns(engine, table_name, columns_def): ) ) except (InternalError, OperationalError) as err: - if "duplicate" not in str(err).lower(): - raise - + raise_if_exception_missing_str(err, ["duplicate"]) _LOGGER.warning( "Column %s already exists on %s, continuing", column_def.split(" ")[1], @@ -204,8 +214,18 @@ def _add_columns(engine, table_name, columns_def): ) -def _modify_columns(engine, table_name, columns_def): +def _modify_columns(connection, engine, table_name, columns_def): """Modify columns in a table.""" + if engine.dialect.name == "sqlite": + _LOGGER.debug( + "Skipping to modify columns %s in table %s; " + "Modifying column length in SQLite is unnecessary, " + "it does not impose any length restrictions", + ", ".join(column.split(" ")[0] for column in columns_def), + table_name, + ) + return + _LOGGER.warning( "Modifying columns %s in table %s. Note: this can take several " "minutes on large databases and slow computers. Please " @@ -213,10 +233,21 @@ def _modify_columns(engine, table_name, columns_def): ", ".join(column.split(" ")[0] for column in columns_def), table_name, ) - columns_def = [f"MODIFY {col_def}" for col_def in columns_def] + + if engine.dialect.name == "postgresql": + columns_def = [ + "ALTER {column} TYPE {type}".format( + **dict(zip(["column", "type"], col_def.split(" ", 1))) + ) + for col_def in columns_def + ] + elif engine.dialect.name == "mssql": + columns_def = [f"ALTER COLUMN {col_def}" for col_def in columns_def] + else: + columns_def = [f"MODIFY {col_def}" for col_def in columns_def] try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {columns_def}".format( table=table_name, columns_def=", ".join(columns_def) @@ -229,7 +260,7 @@ def _modify_columns(engine, table_name, columns_def): for column_def in columns_def: try: - engine.execute( + connection.execute( text( "ALTER TABLE {table} {column_def}".format( table=table_name, column_def=column_def @@ -242,9 +273,9 @@ def _modify_columns(engine, table_name, columns_def): ) -def _update_states_table_with_foreign_key_options(engine): +def _update_states_table_with_foreign_key_options(connection, engine): """Add the options to foreign key constraints.""" - inspector = reflection.Inspector.from_engine(engine) + inspector = sqlalchemy.inspect(engine) alters = [] for foreign_key in inspector.get_foreign_keys(TABLE_STATES): if foreign_key["name"] and ( @@ -271,25 +302,54 @@ def _update_states_table_with_foreign_key_options(engine): for alter in alters: try: - engine.execute(DropConstraint(alter["old_fk"])) + connection.execute(DropConstraint(alter["old_fk"])) for fkc in states_key_constraints: if fkc.column_keys == alter["columns"]: - engine.execute(AddConstraint(fkc)) + connection.execute(AddConstraint(fkc)) except (InternalError, OperationalError): _LOGGER.exception( "Could not update foreign options in %s table", TABLE_STATES ) -def _apply_update(engine, new_version, old_version): +def _drop_foreign_key_constraints(connection, engine, table, columns): + """Drop foreign key constraints for a table on specific columns.""" + inspector = sqlalchemy.inspect(engine) + drops = [] + for foreign_key in inspector.get_foreign_keys(table): + if ( + foreign_key["name"] + and foreign_key["options"].get("ondelete") + and foreign_key["constrained_columns"] == columns + ): + drops.append(ForeignKeyConstraint((), (), name=foreign_key["name"])) + + # Bind the ForeignKeyConstraints to the table + old_table = Table( # noqa: F841 pylint: disable=unused-variable + table, MetaData(), *drops + ) + + for drop in drops: + try: + connection.execute(DropConstraint(drop)) + except (InternalError, OperationalError): + _LOGGER.exception( + "Could not drop foreign constraints in %s table on %s", + TABLE_STATES, + columns, + ) + + +def _apply_update(engine, session, new_version, old_version): """Perform operations to bring schema up to date.""" + connection = session.connection() if new_version == 1: - _create_index(engine, "events", "ix_events_time_fired") + _create_index(connection, "events", "ix_events_time_fired") elif new_version == 2: # Create compound start/end index for recorder_runs - _create_index(engine, "recorder_runs", "ix_recorder_runs_start_end") + _create_index(connection, "recorder_runs", "ix_recorder_runs_start_end") # Create indexes for states - _create_index(engine, "states", "ix_states_last_updated") + _create_index(connection, "states", "ix_states_last_updated") elif new_version == 3: # There used to be a new index here, but it was removed in version 4. pass @@ -299,41 +359,41 @@ def _apply_update(engine, new_version, old_version): if old_version == 3: # Remove index that was added in version 3 - _drop_index(engine, "states", "ix_states_created_domain") + _drop_index(connection, "states", "ix_states_created_domain") if old_version == 2: # Remove index that was added in version 2 - _drop_index(engine, "states", "ix_states_entity_id_created") + _drop_index(connection, "states", "ix_states_entity_id_created") # Remove indexes that were added in version 0 - _drop_index(engine, "states", "states__state_changes") - _drop_index(engine, "states", "states__significant_changes") - _drop_index(engine, "states", "ix_states_entity_id_created") + _drop_index(connection, "states", "states__state_changes") + _drop_index(connection, "states", "states__significant_changes") + _drop_index(connection, "states", "ix_states_entity_id_created") - _create_index(engine, "states", "ix_states_entity_id_last_updated") + _create_index(connection, "states", "ix_states_entity_id_last_updated") elif new_version == 5: # Create supporting index for States.event_id foreign key - _create_index(engine, "states", "ix_states_event_id") + _create_index(connection, "states", "ix_states_event_id") elif new_version == 6: _add_columns( - engine, + session, "events", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(engine, "events", "ix_events_context_id") - _create_index(engine, "events", "ix_events_context_user_id") + _create_index(connection, "events", "ix_events_context_id") + _create_index(connection, "events", "ix_events_context_user_id") _add_columns( - engine, + connection, "states", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(engine, "states", "ix_states_context_id") - _create_index(engine, "states", "ix_states_context_user_id") + _create_index(connection, "states", "ix_states_context_id") + _create_index(connection, "states", "ix_states_context_user_id") elif new_version == 7: - _create_index(engine, "states", "ix_states_entity_id") + _create_index(connection, "states", "ix_states_entity_id") elif new_version == 8: - _add_columns(engine, "events", ["context_parent_id CHARACTER(36)"]) - _add_columns(engine, "states", ["old_state_id INTEGER"]) - _create_index(engine, "events", "ix_events_context_parent_id") + _add_columns(connection, "events", ["context_parent_id CHARACTER(36)"]) + _add_columns(connection, "states", ["old_state_id INTEGER"]) + _create_index(connection, "events", "ix_events_context_parent_id") elif new_version == 9: # We now get the context from events with a join # since its always there on state_changed events @@ -343,32 +403,36 @@ def _apply_update(engine, new_version, old_version): # and we would have to move to something like # sqlalchemy alembic to make that work # - _drop_index(engine, "states", "ix_states_context_id") - _drop_index(engine, "states", "ix_states_context_user_id") + _drop_index(connection, "states", "ix_states_context_id") + _drop_index(connection, "states", "ix_states_context_user_id") # This index won't be there if they were not running # nightly but we don't treat that as a critical issue - _drop_index(engine, "states", "ix_states_context_parent_id") + _drop_index(connection, "states", "ix_states_context_parent_id") # Redundant keys on composite index: # We already have ix_states_entity_id_last_updated - _drop_index(engine, "states", "ix_states_entity_id") - _create_index(engine, "events", "ix_events_event_type_time_fired") - _drop_index(engine, "events", "ix_events_event_type") + _drop_index(connection, "states", "ix_states_entity_id") + _create_index(connection, "events", "ix_events_event_type_time_fired") + _drop_index(connection, "events", "ix_events_event_type") elif new_version == 10: # Now done in step 11 pass elif new_version == 11: - _create_index(engine, "states", "ix_states_old_state_id") - _update_states_table_with_foreign_key_options(engine) + _create_index(connection, "states", "ix_states_old_state_id") + _update_states_table_with_foreign_key_options(connection, engine) elif new_version == 12: if engine.dialect.name == "mysql": - _modify_columns(engine, "events", ["event_data LONGTEXT"]) - _modify_columns(engine, "states", ["attributes LONGTEXT"]) + _modify_columns(connection, engine, "events", ["event_data LONGTEXT"]) + _modify_columns(connection, engine, "states", ["attributes LONGTEXT"]) elif new_version == 13: if engine.dialect.name == "mysql": _modify_columns( - engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"] + connection, + engine, + "events", + ["time_fired DATETIME(6)", "created DATETIME(6)"], ) _modify_columns( + connection, engine, "states", [ @@ -377,6 +441,17 @@ def _apply_update(engine, new_version, old_version): "created DATETIME(6)", ], ) + elif new_version == 14: + _modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"]) + elif new_version == 15: + if sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): + # Recreate the statistics table + Statistics.__table__.drop(engine) + Statistics.__table__.create(engine) + elif new_version == 16: + _drop_foreign_key_constraints( + connection, engine, TABLE_STATES, ["old_state_id"] + ) else: raise ValueError(f"No schema migration defined for version {new_version}") @@ -390,7 +465,7 @@ def _inspect_schema_version(engine, session): version 1 are present to make the determination. Eventually this logic can be removed and we can assume a new db is being created. """ - inspector = reflection.Inspector.from_engine(engine) + inspector = sqlalchemy.inspect(engine) indexes = inspector.get_indexes("events") for index in indexes: diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index a547f3151338b..4fefcaa19e367 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -6,7 +6,9 @@ Boolean, Column, DateTime, + Float, ForeignKey, + Identity, Index, Integer, String, @@ -18,6 +20,7 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm.session import Session +from homeassistant.const import MAX_LENGTH_EVENT_TYPE from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util @@ -26,7 +29,7 @@ # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 13 +SCHEMA_VERSION = 16 _LOGGER = logging.getLogger(__name__) @@ -36,8 +39,15 @@ TABLE_STATES = "states" TABLE_RECORDER_RUNS = "recorder_runs" TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" -ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES] +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, +] DATETIME_TYPE = DateTime(timezone=True).with_variant( mysql.DATETIME(timezone=True, fsp=6), "mysql" @@ -52,8 +62,8 @@ class Events(Base): # type: ignore "mysql_collate": "utf8mb4_unicode_ci", } __tablename__ = TABLE_EVENTS - event_id = Column(Integer, primary_key=True) - event_type = Column(String(32)) + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_TYPE)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(32)) time_fired = Column(DATETIME_TYPE, index=True) @@ -119,7 +129,7 @@ class States(Base): # type: ignore "mysql_collate": "utf8mb4_unicode_ci", } __tablename__ = TABLE_STATES - state_id = Column(Integer, primary_key=True) + state_id = Column(Integer, Identity(), primary_key=True) domain = Column(String(64)) entity_id = Column(String(255)) state = Column(String(255)) @@ -130,9 +140,7 @@ class States(Base): # type: ignore last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) created = Column(DATETIME_TYPE, default=dt_util.utcnow) - old_state_id = Column( - Integer, ForeignKey("states.state_id", ondelete="NO ACTION"), index=True - ) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) @@ -197,11 +205,47 @@ def to_native(self, validate_entity_id=True): return None +class Statistics(Base): # type: ignore + """Statistics.""" + + __table_args__ = { + "mysql_default_charset": "utf8mb4", + "mysql_collate": "utf8mb4_unicode_ci", + } + __tablename__ = TABLE_STATISTICS + id = Column(Integer, primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + source = Column(String(32)) + statistic_id = Column(String(255)) + start = Column(DATETIME_TYPE, index=True) + mean = Column(Float()) + min = Column(Float()) + max = Column(Float()) + last_reset = Column(DATETIME_TYPE) + state = Column(Float()) + sum = Column(Float()) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "statistic_id", "start"), + ) + + @staticmethod + def from_stats(source, statistic_id, start, stats): + """Create object from a statistics.""" + return Statistics( + source=source, + statistic_id=statistic_id, + start=start, + **stats, + ) + + class RecorderRuns(Base): # type: ignore """Representation of recorder run.""" __tablename__ = TABLE_RECORDER_RUNS - run_id = Column(Integer, primary_key=True) + run_id = Column(Integer, Identity(), primary_key=True) start = Column(DateTime(timezone=True), default=dt_util.utcnow) end = Column(DateTime(timezone=True)) closed_incorrect = Column(Boolean, default=False) @@ -252,7 +296,7 @@ class SchemaChanges(Base): # type: ignore """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES - change_id = Column(Integer, primary_key=True) + change_id = Column(Integer, Identity(), primary_key=True) schema_version = Column(Integer) changed = Column(DateTime(timezone=True), default=dt_util.utcnow) @@ -285,3 +329,116 @@ def process_timestamp_to_utc_isoformat(ts): if ts.tzinfo is None: return f"{ts.isoformat()}{DB_TIMEZONE}" return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "entity_id", + "state", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + ] + + def __init__(self, row): # pylint: disable=super-init-not-called + """Init the lazy state.""" + self._row = row + self.entity_id = self._row.entity_id + self.state = self._row.state or "" + self._attributes = None + self._last_changed = None + self._last_updated = None + self._context = None + + @property # type: ignore + def attributes(self): + """State attributes.""" + if not self._attributes: + try: + self._attributes = json.loads(self._row.attributes) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self._row) + self._attributes = {} + return self._attributes + + @attributes.setter + def attributes(self, value): + """Set attributes.""" + self._attributes = value + + @property # type: ignore + def context(self): + """State context.""" + if not self._context: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value): + """Set context.""" + self._context = value + + @property # type: ignore + def last_changed(self): + """Last changed datetime.""" + if not self._last_changed: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value): + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore + def last_updated(self): + """Last updated datetime.""" + if not self._last_updated: + self._last_updated = process_timestamp(self._row.last_updated) + return self._last_updated + + @last_updated.setter + def last_updated(self, value): + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self): + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed: + last_changed_isoformat = self._last_changed.isoformat() + else: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if self._last_updated: + last_updated_isoformat = self._last_updated.isoformat() + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other): + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py new file mode 100644 index 0000000000000..9ee89d248cced --- /dev/null +++ b/homeassistant/components/recorder/pool.py @@ -0,0 +1,34 @@ +"""A pool for sqlite connections.""" +import threading + +from sqlalchemy.pool import NullPool, StaticPool + + +class RecorderPool(StaticPool, NullPool): + """A hybird of NullPool and StaticPool. + + When called from the creating thread acts like StaticPool + When called from any other thread, acts like NullPool + """ + + def __init__(self, *args, **kw): # pylint: disable=super-init-not-called + """Create the pool.""" + self._tid = threading.current_thread().ident + StaticPool.__init__(self, *args, **kw) + + def _do_return_conn(self, conn): + if threading.current_thread().ident == self._tid: + return super()._do_return_conn(conn) + conn.close() + + def dispose(self): + """Dispose of the connection.""" + if threading.current_thread().ident == self._tid: + return super().dispose() + + def _do_get(self): + if threading.current_thread().ident == self._tid: + return super()._do_get() + return super( # pylint: disable=bad-super-call + NullPool, self + )._create_connection() diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index ef626a744c4fa..e1cf15e331dbd 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -3,10 +3,8 @@ from datetime import datetime, timedelta import logging -import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable -from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct @@ -15,7 +13,7 @@ from .const import MAX_ROWS_TO_PURGE from .models import Events, RecorderRuns, States from .repack import repack_database -from .util import session_scope +from .util import retryable_database_job, session_scope if TYPE_CHECKING: from . import Recorder @@ -23,6 +21,7 @@ _LOGGER = logging.getLogger(__name__) +@retryable_database_job("purge") def purge_old_data( instance: Recorder, purge_days: int, repack: bool, apply_filter: bool = False ) -> bool: @@ -35,42 +34,25 @@ def purge_old_data( "Purging states and events before target %s", purge_before.isoformat(sep=" ", timespec="seconds"), ) - try: - with session_scope(session=instance.get_session()) as session: # type: ignore - # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record - event_ids = _select_event_ids_to_purge(session, purge_before) - state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) - if state_ids: - _purge_state_ids(session, state_ids) - if event_ids: - _purge_event_ids(session, event_ids) - # If states or events purging isn't processing the purge_before yet, - # return false, as we are not done yet. - _LOGGER.debug("Purging hasn't fully completed yet") - return False - if apply_filter and _purge_filtered_data(instance, session) is False: - _LOGGER.debug("Cleanup filtered data hasn't fully completed yet") - return False - _purge_old_recorder_runs(instance, session, purge_before) - if repack: - repack_database(instance) - except OperationalError as err: - # Retry when one of the following MySQL errors occurred: - # 1205: Lock wait timeout exceeded; try restarting transaction - # 1206: The total number of locks exceeds the lock table size - # 1213: Deadlock found when trying to get lock; try restarting transaction - if instance.engine.driver in ("mysqldb", "pymysql") and err.orig.args[0] in ( - 1205, - 1206, - 1213, - ): - _LOGGER.info("%s; purge not completed, retrying", err.orig.args[1]) - time.sleep(instance.db_retry_wait) - return False - _LOGGER.warning("Error purging history: %s", err) - except SQLAlchemyError as err: - _LOGGER.warning("Error purging history: %s", err) + with session_scope(session=instance.get_session()) as session: # type: ignore + # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record + event_ids = _select_event_ids_to_purge(session, purge_before) + state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) + if state_ids: + _purge_state_ids(session, state_ids) + if event_ids: + _purge_event_ids(session, event_ids) + # If states or events purging isn't processing the purge_before yet, + # return false, as we are not done yet. + _LOGGER.debug("Purging hasn't fully completed yet") + return False + if apply_filter and _purge_filtered_data(instance, session) is False: + _LOGGER.debug("Cleanup filtered data hasn't fully completed yet") + return False + _purge_old_recorder_runs(instance, session, purge_before) + if repack: + repack_database(instance) return True @@ -213,3 +195,22 @@ def _purge_filtered_events(session: Session, excluded_event_types: list[str]) -> state_ids: list[int] = [state.state_id for state in states] _purge_state_ids(session, state_ids) _purge_event_ids(session, event_ids) + + +@retryable_database_job("purge") +def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) -> bool: + """Purge states and events of specified entities.""" + with session_scope(session=instance.get_session()) as session: # type: ignore + selected_entity_ids: list[str] = [ + entity_id + for (entity_id,) in session.query(distinct(States.entity_id)).all() + if entity_filter(entity_id) + ] + _LOGGER.debug("Purging entity data for %s", selected_entity_ids) + if len(selected_entity_ids) > 0: + # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record + _purge_filtered_states(session, selected_entity_ids) + _LOGGER.debug("Purging entity data hasn't fully completed yet") + return False + + return True diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index 2c4f35b5e7a29..67879867cc72f 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -18,8 +18,7 @@ purge: repack: name: Repack - description: - Attempt to save disk space by rewriting the entire database file. + description: Attempt to save disk space by rewriting the entire database file. example: true default: false selector: @@ -33,8 +32,34 @@ purge: selector: boolean: +purge_entities: + name: Purge Entities + description: Start purge task to remove specific entities from your database. + target: + entity: {} + fields: + domains: + name: Domains to remove + description: List the domains that need to be removed from the recorder database. + example: "sun" + required: false + default: [] + selector: + object: + + entity_globs: + name: Entity Globs to remove + description: List the regular expressions to select entities for removal from the recorder database. + example: "domain*.object_id*" + required: false + default: [] + selector: + object: + disable: + name: Disable description: Stop the recording of events and state changes -enabled: +enable: + name: Enable description: Start the recording of events and state changes diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py new file mode 100644 index 0000000000000..ee733039d4310 --- /dev/null +++ b/homeassistant/components/recorder/statistics.py @@ -0,0 +1,166 @@ +"""Statistics helper.""" +from __future__ import annotations + +from collections import defaultdict +from datetime import datetime, timedelta +from itertools import groupby +import logging +from typing import TYPE_CHECKING + +from sqlalchemy import bindparam +from sqlalchemy.ext import baked + +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .models import Statistics, process_timestamp_to_utc_isoformat +from .util import execute, retryable_database_job, session_scope + +if TYPE_CHECKING: + from . import Recorder + +QUERY_STATISTICS = [ + Statistics.statistic_id, + Statistics.start, + Statistics.mean, + Statistics.min, + Statistics.max, + Statistics.last_reset, + Statistics.state, + Statistics.sum, +] + +STATISTICS_BAKERY = "recorder_statistics_bakery" + +_LOGGER = logging.getLogger(__name__) + + +def async_setup(hass): + """Set up the history hooks.""" + hass.data[STATISTICS_BAKERY] = baked.bakery() + + +def get_start_time() -> datetime.datetime: + """Return start time.""" + last_hour = dt_util.utcnow() - timedelta(hours=1) + start = last_hour.replace(minute=0, second=0, microsecond=0) + return start + + +@retryable_database_job("statistics") +def compile_statistics(instance: Recorder, start: datetime.datetime) -> bool: + """Compile statistics.""" + start = dt_util.as_utc(start) + end = start + timedelta(hours=1) + _LOGGER.debug( + "Compiling statistics for %s-%s", + start, + end, + ) + platform_stats = [] + for domain, platform in instance.hass.data[DOMAIN].items(): + if not hasattr(platform, "compile_statistics"): + continue + platform_stats.append(platform.compile_statistics(instance.hass, start, end)) + _LOGGER.debug( + "Statistics for %s during %s-%s: %s", domain, start, end, platform_stats[-1] + ) + + with session_scope(session=instance.get_session()) as session: # type: ignore + for stats in platform_stats: + for entity_id, stat in stats.items(): + session.add(Statistics.from_stats(DOMAIN, entity_id, start, stat)) + + return True + + +def statistics_during_period(hass, start_time, end_time=None, statistic_id=None): + """Return states changes during UTC period start_time - end_time.""" + with session_scope(hass=hass) as session: + baked_query = hass.data[STATISTICS_BAKERY]( + lambda session: session.query(*QUERY_STATISTICS) + ) + + baked_query += lambda q: q.filter(Statistics.start >= bindparam("start_time")) + + if end_time is not None: + baked_query += lambda q: q.filter(Statistics.start < bindparam("end_time")) + + if statistic_id is not None: + baked_query += lambda q: q.filter_by(statistic_id=bindparam("statistic_id")) + statistic_id = statistic_id.lower() + + baked_query += lambda q: q.order_by(Statistics.statistic_id, Statistics.start) + + stats = execute( + baked_query(session).params( + start_time=start_time, end_time=end_time, statistic_id=statistic_id + ) + ) + + statistic_ids = [statistic_id] if statistic_id is not None else None + + return _sorted_statistics_to_dict(stats, statistic_ids) + + +def get_last_statistics(hass, number_of_stats, statistic_id=None): + """Return the last number_of_stats statistics.""" + with session_scope(hass=hass) as session: + baked_query = hass.data[STATISTICS_BAKERY]( + lambda session: session.query(*QUERY_STATISTICS) + ) + + if statistic_id is not None: + baked_query += lambda q: q.filter_by(statistic_id=bindparam("statistic_id")) + + baked_query += lambda q: q.order_by( + Statistics.statistic_id, Statistics.start.desc() + ) + + baked_query += lambda q: q.limit(bindparam("number_of_stats")) + + stats = execute( + baked_query(session).params( + number_of_stats=number_of_stats, statistic_id=statistic_id + ) + ) + + statistic_ids = [statistic_id] if statistic_id is not None else None + + return _sorted_statistics_to_dict(stats, statistic_ids) + + +def _sorted_statistics_to_dict( + stats, + statistic_ids, +): + """Convert SQL results into JSON friendly data structure.""" + result = defaultdict(list) + # Set all statistic IDs to empty lists in result set to maintain the order + if statistic_ids is not None: + for stat_id in statistic_ids: + result[stat_id] = [] + + # Called in a tight loop so cache the function + # here + _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat + + # Append all changes to it + for ent_id, group in groupby(stats, lambda state: state.statistic_id): + ent_results = result[ent_id] + ent_results.extend( + { + "statistic_id": db_state.statistic_id, + "start": _process_timestamp_to_utc_isoformat(db_state.start), + "mean": db_state.mean, + "min": db_state.min, + "max": db_state.max, + "last_reset": _process_timestamp_to_utc_isoformat(db_state.last_reset), + "state": db_state.state, + "sum": db_state.sum, + } + for db_state in group + ) + + # Filter out the empty lists if some states had 0 results. + return {key: val for key, val in result.items() if val} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c17fb33d365ed..db9fb46425ba8 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -4,18 +4,30 @@ from collections.abc import Generator from contextlib import contextmanager from datetime import timedelta +import functools import logging import os import time +from typing import TYPE_CHECKING from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.session import Session -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, SQLITE_URL_PREFIX -from .models import ALL_TABLES, process_timestamp +from .const import DATA_INSTANCE, SQLITE_URL_PREFIX +from .models import ( + ALL_TABLES, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + RecorderRuns, + process_timestamp, +) + +if TYPE_CHECKING: + from . import Recorder _LOGGER = logging.getLogger(__name__) @@ -28,10 +40,16 @@ # should do a check on the sqlite3 database. MAX_RESTART_TIME = timedelta(minutes=10) +# Retry when one of the following MySQL errors occurred: +RETRYABLE_MYSQL_ERRORS = (1205, 1206, 1213) +# 1205: Lock wait timeout exceeded; try restarting transaction +# 1206: The total number of locks exceeds the lock table size +# 1213: Deadlock found when trying to get lock; try restarting transaction + @contextmanager def session_scope( - *, hass: HomeAssistantType | None = None, session: Session | None = None + *, hass: HomeAssistant | None = None, session: Session | None = None ) -> Generator[Session, None, None]: """Provide a transactional scope around a series of operations.""" if session is None and hass is not None: @@ -43,7 +61,7 @@ def session_scope( need_rollback = False try: yield session - if session.transaction: + if session.get_transaction(): need_rollback = True session.commit() except Exception as err: @@ -117,7 +135,7 @@ def execute(qry, to_native=False, validate_entity_ids=True): time.sleep(QUERY_RETRY_WAIT) -def validate_or_move_away_sqlite_database(dburl: str, db_integrity_check: bool) -> bool: +def validate_or_move_away_sqlite_database(dburl: str) -> bool: """Ensure that the database is valid or move it away.""" dbpath = dburl_to_path(dburl) @@ -125,7 +143,7 @@ def validate_or_move_away_sqlite_database(dburl: str, db_integrity_check: bool) # Database does not exist yet, this is OK return True - if not validate_sqlite_database(dbpath, db_integrity_check): + if not validate_sqlite_database(dbpath): move_away_broken_database(dbpath) return False @@ -161,18 +179,23 @@ def basic_sanity_check(cursor): """Check tables to make sure select does not fail.""" for table in ALL_TABLES: - cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # nosec # not injection + if table == TABLE_STATISTICS: + continue + if table in (TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES): + cursor.execute(f"SELECT * FROM {table};") # nosec # not injection + else: + cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # nosec # not injection return True -def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool: +def validate_sqlite_database(dbpath: str) -> bool: """Run a quick check on an sqlite database to see if it is corrupt.""" import sqlite3 # pylint: disable=import-outside-toplevel try: conn = sqlite3.connect(dbpath) - run_checks_on_open_db(dbpath, conn.cursor(), db_integrity_check) + run_checks_on_open_db(dbpath, conn.cursor()) conn.close() except sqlite3.DatabaseError: _LOGGER.exception("The database at %s is corrupt or malformed", dbpath) @@ -181,24 +204,14 @@ def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool: return True -def run_checks_on_open_db(dbpath, cursor, db_integrity_check): +def run_checks_on_open_db(dbpath, cursor): """Run checks that will generate a sqlite3 exception if there is corruption.""" sanity_check_passed = basic_sanity_check(cursor) last_run_was_clean = last_run_was_recently_clean(cursor) if sanity_check_passed and last_run_was_clean: _LOGGER.debug( - "The quick_check will be skipped as the system was restarted cleanly and passed the basic sanity check" - ) - return - - if not db_integrity_check: - # Always warn so when it does fail they remember it has - # been manually disabled - _LOGGER.warning( - "The quick_check on the sqlite3 database at %s was skipped because %s was disabled", - dbpath, - CONF_DB_INTEGRITY_CHECK, + "The system was restarted cleanly and passed the basic sanity check" ) return @@ -214,11 +227,6 @@ def run_checks_on_open_db(dbpath, cursor, db_integrity_check): dbpath, ) - _LOGGER.info( - "A quick_check is being performed on the sqlite3 database at %s", dbpath - ) - cursor.execute("PRAGMA QUICK_CHECK") - def move_away_broken_database(dbfile: str) -> None: """Move away a broken sqlite3 database.""" @@ -237,3 +245,88 @@ def move_away_broken_database(dbfile: str) -> None: if not os.path.exists(path): continue os.rename(path, f"{path}{corrupt_postfix}") + + +def execute_on_connection(dbapi_connection, statement): + """Execute a single statement with a dbapi connection.""" + cursor = dbapi_connection.cursor() + cursor.execute(statement) + cursor.close() + + +def setup_connection_for_dialect(dialect_name, dbapi_connection, first_connection): + """Execute statements needed for dialect connection.""" + # Returns False if the the connection needs to be setup + # on the next connection, returns True if the connection + # never needs to be setup again. + if dialect_name == "sqlite": + if first_connection: + old_isolation = dbapi_connection.isolation_level + dbapi_connection.isolation_level = None + execute_on_connection(dbapi_connection, "PRAGMA journal_mode=WAL") + dbapi_connection.isolation_level = old_isolation + # WAL mode only needs to be setup once + # instead of every time we open the sqlite connection + # as its persistent and isn't free to call every time. + + # approximately 8MiB of memory + execute_on_connection(dbapi_connection, "PRAGMA cache_size = -8192") + + if dialect_name == "mysql": + execute_on_connection(dbapi_connection, "SET session wait_timeout=28800") + + +def end_incomplete_runs(session, start_time): + """End any incomplete recorder runs.""" + for run in session.query(RecorderRuns).filter_by(end=None): + run.closed_incorrect = True + run.end = start_time + _LOGGER.warning( + "Ended unfinished session (id=%s from %s)", run.run_id, run.start + ) + session.add(run) + + +def retryable_database_job(description: str): + """Try to execute a database job. + + The job should return True if it finished, and False if it needs to be rescheduled. + """ + + def decorator(job: callable): + @functools.wraps(job) + def wrapper(instance: Recorder, *args, **kwargs): + try: + return job(instance, *args, **kwargs) + except OperationalError as err: + if ( + instance.engine.dialect.name == "mysql" + and err.orig.args[0] in RETRYABLE_MYSQL_ERRORS + ): + _LOGGER.info( + "%s; %s not completed, retrying", err.orig.args[1], description + ) + time.sleep(instance.db_retry_wait) + # Failed with retryable error + return False + + _LOGGER.warning("Error executing %s: %s", description, err) + + # Failed with permanent error + return True + + return wrapper + + return decorator + + +def perodic_db_cleanups(instance: Recorder): + """Run any database cleanups that need to happen perodiclly. + + These cleanups will happen nightly or after any purge. + """ + + if instance.engine.dialect.name == "sqlite": + # Execute sqlite to create a wal checkpoint and free up disk space + _LOGGER.debug("WAL checkpoint") + instance.engine.execute("PRAGMA wal_checkpoint(TRUNCATE);") diff --git a/homeassistant/components/recswitch/manifest.json b/homeassistant/components/recswitch/manifest.json index 4d155b6ec02d3..c8a724471883e 100644 --- a/homeassistant/components/recswitch/manifest.json +++ b/homeassistant/components/recswitch/manifest.json @@ -3,5 +3,6 @@ "name": "Ankuoo REC Switch", "documentation": "https://www.home-assistant.io/integrations/recswitch", "requirements": ["pyrecswitch==1.0.2"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 252052ac5c221..0b5f539bccec5 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,6 +2,7 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": ["praw==7.1.4"], - "codeowners": [] + "requirements": ["praw==7.2.0"], + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index a88de916009e0..1e755b950bf66 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -56,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Reddit sensor platform.""" subreddits = config[CONF_SUBREDDITS] - user_agent = "{}_home_assistant_sensor".format(config[CONF_USERNAME]) + user_agent = f"{config[CONF_USERNAME]}_home_assistant_sensor" limit = config[CONF_MAXIMUM] sort_by = config[CONF_SORT_BY] diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index 6f91e2a9abe18..58594f1757745 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -3,5 +3,6 @@ "name": "Rejseplanen", "documentation": "https://www.home-assistant.io/integrations/rejseplanen", "requirements": ["rjpl==0.3.6"], - "codeowners": ["@DarkFox"] + "codeowners": ["@DarkFox"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index 8ce8cb98e5bfa..c19cc701afce5 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", "requirements": ["RtmAPI==0.7.2", "httplib2==0.19.0"], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/remember_the_milk/services.yaml b/homeassistant/components/remember_the_milk/services.yaml index 448b35a3a04bf..1458075fbd563 100644 --- a/homeassistant/components/remember_the_milk/services.yaml +++ b/homeassistant/components/remember_the_milk/services.yaml @@ -1,21 +1,34 @@ # Describes the format for available Remember The Milk services create_task: + name: Create task description: >- Create (or update) a new task in your Remember The Milk account. If you want to update a task later on, you have to set an "id" when creating the task. Note: Updating a tasks does not support the smart syntax. fields: name: + name: Name description: name of the new task, you can use the smart syntax here + required: true example: "do this ^today #from_hass" + selector: + text: id: - description: (optional) identifier for the task you're creating, can be used to update or complete the task later on + name: ID + description: Identifier for the task you're creating, can be used to update or complete the task later on example: myid + selector: + text: complete_task: + name: Complete task description: Complete a tasks that was privously created. fields: id: + name: ID description: identifier that was defined when creating the task + required: true example: myid + selector: + text: diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index ecde6f67b67c5..fef0da4dae635 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -1,10 +1,11 @@ """Support to interface with universal remote control devices.""" from __future__ import annotations +from collections.abc import Iterable from datetime import timedelta import functools as ft import logging -from typing import Any, Iterable, cast, final +from typing import Any, cast, final import voluptuous as vol @@ -16,6 +17,7 @@ SERVICE_TURN_ON, STATE_ON, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -24,7 +26,7 @@ ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -68,12 +70,12 @@ @bind_hass -def is_on(hass: HomeAssistantType, entity_id: str) -> bool: +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the remote is on based on the statemachine.""" return hass.states.is_state(entity_id, STATE_ON) -async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for remotes.""" component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL @@ -130,12 +132,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" return await cast(EntityComponent, hass.data[DOMAIN]).async_setup_entry(entry) -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await cast(EntityComponent, hass.data[DOMAIN]).async_unload_entry(entry) diff --git a/homeassistant/components/remote/group.py b/homeassistant/components/remote/group.py index 1636054663dc6..234883ffd5a04 100644 --- a/homeassistant/components/remote/group.py +++ b/homeassistant/components/remote/group.py @@ -3,13 +3,12 @@ from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant, callback @callback def async_describe_on_off_states( - hass: HomeAssistantType, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: GroupIntegrationRegistry ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index 30c442b540b6b..e2caf2d56067e 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -2,5 +2,6 @@ "domain": "remote", "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/remote/reproduce_state.py b/homeassistant/components/remote/reproduce_state.py index b42a0bdc611a7..cc9685dee2fb3 100644 --- a/homeassistant/components/remote/reproduce_state.py +++ b/homeassistant/components/remote/reproduce_state.py @@ -2,8 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable import logging -from typing import Any, Iterable +from typing import Any from homeassistant.const import ( ATTR_ENTITY_ID, @@ -12,8 +13,7 @@ STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, State -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import Context, HomeAssistant, State from . import DOMAIN @@ -23,7 +23,7 @@ async def _async_reproduce_state( - hass: HomeAssistantType, + hass: HomeAssistant, state: State, *, context: Context | None = None, @@ -59,7 +59,7 @@ async def _async_reproduce_state( async def async_reproduce_states( - hass: HomeAssistantType, + hass: HomeAssistant, states: Iterable[State], *, context: Context | None = None, diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 3868479efc65b..a36e33aa77d6e 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -1,82 +1,141 @@ # Describes the format for available remote services turn_on: + name: Turn On description: Sends the Power On Command. + target: + entity: + domain: remote fields: - entity_id: - description: Name(s) of entities to turn on. - example: "remote.family_room" activity: description: Activity ID or Activity Name to start. example: "BedroomTV" + selector: + text: toggle: + name: Toggle description: Toggles a device. - fields: - entity_id: - description: Name(s) of entities to toggle. - example: "remote.family_room" + target: + entity: + domain: remote turn_off: + name: Turn Off description: Sends the Power Off Command. - fields: - entity_id: - description: Name(s) of entities to turn off. - example: "remote.family_room" + target: + entity: + domain: remote send_command: + name: Send Command description: Sends a command or a list of commands to a device. + target: + entity: + domain: remote fields: - entity_id: - description: Name(s) of entities to send command from. - example: "remote.family_room" device: + name: Device description: Device ID to send command to. example: "32756745" command: + name: Command description: A single command or a list of commands to send. + required: true example: "Play" + selector: + text: num_repeats: - description: An optional value that specifies the number of times you want to repeat the command(s). If not specified, the command(s) will not be repeated. + name: Repeats + description: An optional value that specifies the number of times you want to repeat the command(s). example: "5" + default: 1 + selector: + number: + min: 0 + max: 255 + step: 1 + mode: slider delay_secs: - description: An optional value that specifies that number of seconds you want to wait in between repeated commands. If not specified, the default of 0.4 seconds will be used. + name: Delay Seconds + description: Specify the number of seconds you want to wait in between repeated commands. example: "0.75" + default: 0.4 + selector: + number: + min: 0 + max: 60 + step: 0.1 + mode: slider hold_secs: - description: An optional value that specifies that number of seconds you want to have it held before the release is send. If not specified, the release will be send immediately after the press. + name: Hold Seconds + description: An optional value that specifies the number of seconds you want to have it held before the release is send. example: "2.5" + default: 0 + selector: + number: + min: 0 + max: 60 + step: 0.1 + mode: slider learn_command: + name: Learn Command description: Learns a command or a list of commands from a device. + target: + entity: + domain: remote fields: - entity_id: - description: Name(s) of entities to learn command from. - example: "remote.bedroom" device: description: Device ID to learn command from. example: "television" command: + name: Command description: A single command or a list of commands to learn. example: "Turn on" + selector: + object: command_type: + name: Command Type description: The type of command to be learned. example: "rf" + default: "ir" + selector: + select: + options: + - "ir" + - "rf" alternative: + name: Alternative description: If code must be stored as alternative (useful for discrete remotes). example: "True" + selector: + boolean: timeout: + name: Timeout description: Timeout, in seconds, for the command to be learned. example: "30" + selector: + number: + min: 0 + max: 60 + step: 5 + mode: slider delete_command: + name: Delete Command description: Deletes a command or a list of commands from the database. + target: + entity: + domain: remote fields: - entity_id: - description: Name(s) of the remote entities holding the database. - example: "remote.bedroom" device: description: Name of the device from which commands will be deleted. example: "television" command: + name: Command description: A single command or a list of commands to delete. + required: true example: "Mute" + selector: + object: diff --git a/homeassistant/components/remote/translations/ru.json b/homeassistant/components/remote/translations/ru.json index 7afb30bda1f2a..f4ddbc0902421 100644 --- a/homeassistant/components/remote/translations/ru.json +++ b/homeassistant/components/remote/translations/ru.json @@ -16,8 +16,8 @@ }, "state": { "_": { - "off": "\u0412\u044b\u043a\u043b", - "on": "\u0412\u043a\u043b" + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, "title": "\u041f\u0443\u043b\u044c\u0442 \u0414\u0423" diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index c69a9c92fde5c..b2ed060bffaba 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "remote_rpi_gpio", "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", "requirements": ["gpiozero==1.5.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index a680fd777612e..c104fc447e2ab 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -167,7 +167,7 @@ def setup(hass, config): for repetier in config[DOMAIN]: _LOGGER.debug("Repetier server config %s", repetier[CONF_HOST]) - url = "http://{}".format(repetier[CONF_HOST]) + url = f"http://{repetier[CONF_HOST]}" port = repetier[CONF_PORT] api_key = repetier[CONF_API_KEY] diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index b6d48aded2f8f..0fd3d9049875f 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -3,5 +3,6 @@ "name": "Repetier-Server", "documentation": "https://www.home-assistant.io/integrations/repetier", "requirements": ["pyrepetier==3.0.5"], - "codeowners": ["@MTrab"] + "codeowners": ["@MTrab"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 9692f5b9339a8..a90c5bd7c7706 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -40,9 +40,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= conf = config coordinator = None rest = create_rest_data_from_config(hass, conf) - await rest.async_update() + await rest.async_update(log_errors=False) if rest.data is None: + if rest.last_exception: + raise PlatformNotReady from rest.last_exception raise PlatformNotReady name = conf.get(CONF_NAME) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index dd2e29616c768..8b03bcfb12876 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -37,13 +37,14 @@ def __init__( self._verify_ssl = verify_ssl self._async_client = None self.data = None + self.last_exception = None self.headers = None def set_url(self, url): """Set url.""" self._resource = url - async def async_update(self): + async def async_update(self, log_errors=True): """Get the latest data from REST service with provided method.""" if not self._async_client: self._async_client = get_async_client( @@ -64,6 +65,10 @@ async def async_update(self): self.data = response.text self.headers = response.headers except httpx.RequestError as ex: - _LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex) + if log_errors: + _LOGGER.error( + "Error fetching data: %s failed with %s", self._resource, ex + ) + self.last_exception = ex self.data = None self.headers = None diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index 3ab926a3b1329..c81656d82b401 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -3,5 +3,6 @@ "name": "RESTful", "documentation": "https://www.home-assistant.io/integrations/rest", "requirements": ["jsonpath==0.82", "xmltodict==0.12.0"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index d303f7a57b30f..7727b5f09ab46 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -50,9 +50,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= conf = config coordinator = None rest = create_rest_data_from_config(hass, conf) - await rest.async_update() + await rest.async_update(log_errors=False) if rest.data is None: + if rest.last_exception: + raise PlatformNotReady from rest.last_exception raise PlatformNotReady name = conf.get(CONF_NAME) diff --git a/homeassistant/components/rest/services.yaml b/homeassistant/components/rest/services.yaml index 7e324670134a2..9ba509b63f6f4 100644 --- a/homeassistant/components/rest/services.yaml +++ b/homeassistant/components/rest/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all rest entities and notify services diff --git a/homeassistant/components/rest_command/manifest.json b/homeassistant/components/rest_command/manifest.json index a4441a7afa000..ced35e88293f4 100644 --- a/homeassistant/components/rest_command/manifest.json +++ b/homeassistant/components/rest_command/manifest.json @@ -2,5 +2,6 @@ "domain": "rest_command", "name": "RESTful Command", "documentation": "https://www.home-assistant.io/integrations/rest_command", - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index ebd1fb5afdca5..93afa8f5df43c 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -3,7 +3,6 @@ "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": ["rflink==0.0.58"], - "codeowners": [ - "@javicalle" - ] + "codeowners": ["@javicalle"], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/rflink/services.yaml b/homeassistant/components/rflink/services.yaml index 3a44d04f75d99..8e233bc7aac5d 100644 --- a/homeassistant/components/rflink/services.yaml +++ b/homeassistant/components/rflink/services.yaml @@ -1,9 +1,18 @@ send_command: + name: Send command description: Send device command through RFLink. fields: command: + name: Command description: The command to be sent. + required: true example: "on" + selector: + text: device_id: + name: Device ID description: RFLink device ID. + required: true example: newkaku_0000c6c2_1 + selector: + text: diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index d23a3e4e6ffb0..a4be36df998b1 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -202,24 +202,14 @@ async def async_setup_entry(hass, entry: config_entries.ConfigEntry): ) return False - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass, entry: config_entries.ConfigEntry): """Unload RFXtrx component.""" - if not all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ): + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return False hass.services.async_remove(DOMAIN, SERVICE_SEND) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index da4d6447e76cb..91afd9da9992a 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -444,7 +444,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for RFXCOM RFXtrx.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH async def async_step_user(self, user_input=None): """Step when user initializes a integration.""" diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 19e834d11d614..34c31c72a0df6 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": ["pyRFXtrx==0.26.1"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], - "config_flow": true + "config_flow": true, + "iot_class": "local_push" } diff --git a/homeassistant/components/rfxtrx/services.yaml b/homeassistant/components/rfxtrx/services.yaml index 088082758b6d1..43695554ed0e3 100644 --- a/homeassistant/components/rfxtrx/services.yaml +++ b/homeassistant/components/rfxtrx/services.yaml @@ -1,6 +1,11 @@ send: + name: Send description: Sends a raw event on radio. fields: event: + name: Event description: A hexadecimal string to send. + required: true example: "0b11009e00e6116202020070" + selector: + text: diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index 24e5ee56d7610..fbbfeb5d6a06b 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -37,7 +37,7 @@ }, "options": { "error": { - "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_event_code": "\u4e8b\u4ef6\u4ee3\u78bc\u7121\u6548", "invalid_input_2262_off": "\u547d\u4ee4\u95dc\u9589\u8f38\u5165\u7121\u6548", "invalid_input_2262_on": "\u547d\u4ee4\u958b\u555f\u8f38\u5165\u7121\u6548", diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index f5211ac54c03c..a8196b30302f6 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -100,10 +100,7 @@ def token_updater(token): ), } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) if hass.services.has_service(DOMAIN, "update"): return True @@ -124,15 +121,7 @@ async def async_refresh_all(_): async def async_unload_entry(hass, entry): """Unload Ring entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - if not unload_ok: + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return False hass.data[DOMAIN].pop(entry.entry_id) @@ -157,7 +146,7 @@ def __init__( ring: Ring, update_method: str, update_interval: timedelta, - ): + ) -> None: """Initialize global data updater.""" self.hass = hass self.data_type = data_type @@ -230,7 +219,7 @@ def __init__( ring: Ring, update_method: str, update_interval: timedelta, - ): + ) -> None: """Initialize device data updater.""" self.data_type = data_type self.hass = hass diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 18ce87e722e0d..28d686df06a69 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -52,7 +52,7 @@ def __init__(self, config_entry_id, ring, device, sensor_type): super().__init__(config_entry_id, device) self._ring = ring self._sensor_type = sensor_type - self._name = "{} {}".format(self._device.name, SENSOR_TYPES.get(sensor_type)[0]) + self._name = f"{self._device.name} {SENSOR_TYPES.get(sensor_type)[0]}" self._device_class = SENSOR_TYPES.get(sensor_type)[2] self._state = None self._unique_id = f"{device.id}-{sensor_type}" diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index a23a08b2a5409..d4cc6796bf1bb 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -36,7 +36,6 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Ring.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL user_pass = None diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 3808383031106..ecb64c99fd764 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -6,5 +6,11 @@ "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], "config_flow": true, - "dhcp": [{"hostname":"ring*","macaddress":"0CAE7D*"}] + "dhcp": [ + { + "hostname": "ring*", + "macaddress": "0CAE7D*" + } + ], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index a20d484d3fe0d..fb1c38fcbdeb3 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,6 +1,10 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" from homeassistant.components.sensor import SensorEntity -from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.const import ( + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, +) from homeassistant.core import callback from homeassistant.helpers.icon import icon_for_battery_level @@ -40,9 +44,9 @@ def __init__(self, config_entry_id, device, sensor_type): super().__init__(config_entry_id, device) self._sensor_type = sensor_type self._extra = None - self._icon = "mdi:{}".format(SENSOR_TYPES.get(sensor_type)[3]) + self._icon = f"mdi:{SENSOR_TYPES.get(sensor_type)[3]}" self._kind = SENSOR_TYPES.get(sensor_type)[4] - self._name = "{} {}".format(self._device.name, SENSOR_TYPES.get(sensor_type)[0]) + self._name = f"{self._device.name} {SENSOR_TYPES.get(sensor_type)[0]}" self._unique_id = f"{device.id}-{sensor_type}" @property @@ -210,7 +214,7 @@ def extra_state_attributes(self): None, "history", None, - "timestamp", + DEVICE_CLASS_TIMESTAMP, HistoryRingSensor, ], "last_ding": [ @@ -219,7 +223,7 @@ def extra_state_attributes(self): None, "history", "ding", - "timestamp", + DEVICE_CLASS_TIMESTAMP, HistoryRingSensor, ], "last_motion": [ @@ -228,7 +232,7 @@ def extra_state_attributes(self): None, "history", "motion", - "timestamp", + DEVICE_CLASS_TIMESTAMP, HistoryRingSensor, ], "volume": [ diff --git a/homeassistant/components/ring/services.yaml b/homeassistant/components/ring/services.yaml index bcc9b2f7ff4ec..c648f02139b0b 100644 --- a/homeassistant/components/ring/services.yaml +++ b/homeassistant/components/ring/services.yaml @@ -1,2 +1,3 @@ update: + name: Update description: Updates the data we have for all your ring devices diff --git a/homeassistant/components/ring/translations/zh-Hant.json b/homeassistant/components/ring/translations/zh-Hant.json index 9f3c91e2a7c49..9215c7ebe3861 100644 --- a/homeassistant/components/ring/translations/zh-Hant.json +++ b/homeassistant/components/ring/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/ripple/manifest.json b/homeassistant/components/ripple/manifest.json index d730093ed0f40..68adda3edeae1 100644 --- a/homeassistant/components/ripple/manifest.json +++ b/homeassistant/components/ripple/manifest.json @@ -3,5 +3,6 @@ "name": "Ripple", "documentation": "https://www.home-assistant.io/integrations/ripple", "requirements": ["python-ripple-api==0.0.3"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index eec30553870f9..48c50f9cc462b 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -27,12 +27,6 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Risco component.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Risco from a config entry.""" data = entry.data @@ -54,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): undo_listener = entry.add_update_listener(_update_listener) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: undo_listener, @@ -76,15 +71,7 @@ async def start_platforms(): 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 - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index ba32429c154b0..0e1d4d235c203 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Risco alarm control panel.""" - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service(SERVICE_BYPASS_ZONE, {}, "async_bypass_zone") platform.async_register_entity_service( SERVICE_UNBYPASS_ZONE, {}, "async_unbypass_zone" diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 76b6105df016f..0bc9c49707a81 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -58,7 +58,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Risco.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @core.callback diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 7f13af252f3d4..2da0a5254a485 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,11 +3,8 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": [ - "pyrisco==0.3.1" - ], - "codeowners": [ - "@OnFreund" - ], - "quality_scale": "platinum" -} \ No newline at end of file + "requirements": ["pyrisco==0.3.1"], + "codeowners": ["@OnFreund"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/risco/services.yaml b/homeassistant/components/risco/services.yaml index 8b6c8c06f01c7..c271df7b462ac 100644 --- a/homeassistant/components/risco/services.yaml +++ b/homeassistant/components/risco/services.yaml @@ -1,15 +1,17 @@ # Describes the format for available Risco services bypass_zone: + name: Bypass zone description: Bypass a Risco Zone - fields: - entity_id: - description: Entity ID of the zone to bypass - example: "binary_sensor.living_room_motion" + target: + entity: + integration: risco + domain: binary_sensor unbypass_zone: + name: Unbypass zone description: Unbypass a Risco Zone - fields: - entity_id: - description: Entity ID of the zone to unbypass - example: "binary_sensor.living_room_motion" + target: + entity: + integration: risco + domain: binary_sensor diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index 36d808bd6dec7..8e50b61f16ff9 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -20,6 +20,16 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "Aktiv, abwesend", + "armed_custom_bypass": "Aktiv, benutzerdefiniert", + "armed_home": "Aktiv, zu Hause", + "armed_night": "Aktiv, Nacht" + }, + "description": "W\u00e4hlen Sie aus, in welchen Zustand Ihr Risco-Alarm versetzt werden soll, wenn Sie den Alarm des Home Assistant scharf schalten", + "title": "Home Assistant Zust\u00e4nde den Risco Zust\u00e4nden zuordnen" + }, "init": { "data": { "code_arm_required": "PIN-Code zum Entsperren vorgeben", @@ -31,8 +41,12 @@ "A": "Gruppe A", "B": "Gruppe B", "C": "Gruppe C", - "D": "Gruppe D" - } + "D": "Gruppe D", + "arm": "Aktiv, abwesend", + "partial_arm": "Teilweise aktiv (STAY)" + }, + "description": "W\u00e4hlen Sie aus, welchen Zustand Ihr Home Assistant-Alarm f\u00fcr jeden von Risco gemeldeten Zustand melden soll", + "title": "Risco-Zust\u00e4nde den Home Assistant-Zust\u00e4nden zuordnen" } } } diff --git a/homeassistant/components/risco/translations/zh-Hant.json b/homeassistant/components/risco/translations/zh-Hant.json index c76871bcecd04..7553ec3e36a0c 100644 --- a/homeassistant/components/risco/translations/zh-Hant.json +++ b/homeassistant/components/risco/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index f2fd13a9ef451..65c1a2dd97c3a 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -1,27 +1,25 @@ """The Rituals Perfume Genie integration.""" -import asyncio +from datetime import timedelta import logging -from aiohttp.client_exceptions import ClientConnectorError -from pyrituals import Account +import aiohttp +from pyrituals import Account, Diffuser from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ACCOUNT_HASH, DOMAIN +from .const import ACCOUNT_HASH, COORDINATORS, DEVICES, DOMAIN, HUBLOT -_LOGGER = logging.getLogger(__name__) +PLATFORMS = ["binary_sensor", "sensor", "switch"] EMPTY_CREDENTIALS = "" -PLATFORMS = ["switch"] - +_LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Rituals Perfume Genie component.""" - return True +UPDATE_INTERVAL = timedelta(seconds=30) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -31,31 +29,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): account.data = {ACCOUNT_HASH: entry.data.get(ACCOUNT_HASH)} try: - await account.get_devices() - except ClientConnectorError as ex: - raise ConfigEntryNotReady from ex + account_devices = await account.get_devices() + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = account + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + COORDINATORS: {}, + DEVICES: {}, + } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + for device in account_devices: + hublot = device.hub_data[HUBLOT] + + coordinator = RitualsDataUpdateCoordinator(hass, device) + await coordinator.async_refresh() + + hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device + hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) 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 - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class RitualsDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Rituals Perufme Genie device data from single endpoint.""" + + def __init__(self, hass: HomeAssistant, device: Diffuser) -> None: + """Initialize global Rituals Perufme Genie data updater.""" + self._device = device + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}-{device.hub_data[HUBLOT]}", + update_interval=UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> None: + """Fetch data from Rituals.""" + await self._device.update_data() diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py new file mode 100644 index 0000000000000..2d82982388d98 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -0,0 +1,56 @@ +"""Support for Rituals Perfume Genie binary sensors.""" +from __future__ import annotations + +from pyrituals import Diffuser + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RitualsDataUpdateCoordinator +from .const import COORDINATORS, DEVICES, DOMAIN +from .entity import DiffuserEntity + +CHARGING_SUFFIX = " Battery Charging" +BATTERY_CHARGING_ID = 21 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the diffuser binary sensors.""" + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] + entities = [] + for hublot, diffuser in diffusers.items(): + if diffuser.has_battery: + coordinator = coordinators[hublot] + entities.append(DiffuserBatteryChargingBinarySensor(diffuser, coordinator)) + + async_add_entities(entities) + + +class DiffuserBatteryChargingBinarySensor(DiffuserEntity, BinarySensorEntity): + """Representation of a diffuser battery charging binary sensor.""" + + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: + """Initialize the battery charging binary sensor.""" + super().__init__(diffuser, coordinator, CHARGING_SUFFIX) + + @property + def is_on(self) -> bool: + """Return the state of the battery charging binary sensor.""" + return self._diffuser.charging + + @property + def device_class(self) -> str: + """Return the device class of the battery charging binary sensor.""" + return DEVICE_CLASS_BATTERY_CHARGING diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index 7bd75cdbbc089..f1f037941b36c 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACCOUNT_HASH, DOMAIN @@ -25,9 +26,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Rituals Perfume Genie.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/rituals_perfume_genie/const.py b/homeassistant/components/rituals_perfume_genie/const.py index 075d79ec8de9e..c0bf72fb90e87 100644 --- a/homeassistant/components/rituals_perfume_genie/const.py +++ b/homeassistant/components/rituals_perfume_genie/const.py @@ -1,5 +1,11 @@ """Constants for the Rituals Perfume Genie integration.""" - DOMAIN = "rituals_perfume_genie" +COORDINATORS = "coordinators" +DEVICES = "devices" + ACCOUNT_HASH = "account_hash" +ATTRIBUTES = "attributes" +HUBLOT = "hublot" +ID = "id" +SENSORS = "sensors" diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py new file mode 100644 index 0000000000000..1c1f3912c68b1 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -0,0 +1,65 @@ +"""Base class for Rituals Perfume Genie diffuser entity.""" +from __future__ import annotations + +from pyrituals import Diffuser + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import RitualsDataUpdateCoordinator +from .const import ATTRIBUTES, DOMAIN, HUBLOT, SENSORS + +MANUFACTURER = "Rituals Cosmetics" +MODEL = "The Perfume Genie" +MODEL2 = "The Perfume Genie 2.0" + +ROOMNAME = "roomnamec" +STATUS = "status" +VERSION = "versionc" + +AVAILABLE_STATE = 1 + + +class DiffuserEntity(CoordinatorEntity): + """Representation of a diffuser entity.""" + + coordinator: RitualsDataUpdateCoordinator + + def __init__( + self, + diffuser: Diffuser, + coordinator: RitualsDataUpdateCoordinator, + entity_suffix: str, + ) -> None: + """Init from config, hookup diffuser and coordinator.""" + super().__init__(coordinator) + self._diffuser = diffuser + self._entity_suffix = entity_suffix + self._hublot = self._diffuser.hub_data[HUBLOT] + self._hubname = self._diffuser.hub_data[ATTRIBUTES][ROOMNAME] + + @property + def unique_id(self) -> str: + """Return the unique ID of the entity.""" + return f"{self._hublot}{self._entity_suffix}" + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"{self._hubname}{self._entity_suffix}" + + @property + def available(self) -> bool: + """Return if the entity is available.""" + return super().available and self._diffuser.hub_data[STATUS] == AVAILABLE_STATE + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device.""" + return { + "name": self._hubname, + "identifiers": {(DOMAIN, self._hublot)}, + "manufacturer": MANUFACTURER, + "model": MODEL if self._diffuser.has_battery else MODEL2, + "sw_version": self._diffuser.hub_data[SENSORS][VERSION], + } diff --git a/homeassistant/components/rituals_perfume_genie/manifest.json b/homeassistant/components/rituals_perfume_genie/manifest.json index 8be7e98b93989..756af10f33bf5 100644 --- a/homeassistant/components/rituals_perfume_genie/manifest.json +++ b/homeassistant/components/rituals_perfume_genie/manifest.json @@ -3,10 +3,7 @@ "name": "Rituals Perfume Genie", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie", - "requirements": [ - "pyrituals==0.0.2" - ], - "codeowners": [ - "@milanmeu" - ] + "requirements": ["pyrituals==0.0.3"], + "codeowners": ["@milanmeu"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py new file mode 100644 index 0000000000000..31a04bb5b8f11 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -0,0 +1,147 @@ +"""Support for Rituals Perfume Genie sensors.""" +from __future__ import annotations + +from pyrituals import Diffuser + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_SIGNAL_STRENGTH, + PERCENTAGE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RitualsDataUpdateCoordinator +from .const import COORDINATORS, DEVICES, DOMAIN, ID, SENSORS +from .entity import DiffuserEntity + +TITLE = "title" +ICON = "icon" +WIFI = "wific" +PERFUME = "rfidc" +FILL = "fillc" + +PERFUME_NO_CARTRIDGE_ID = 19 +FILL_NO_CARTRIDGE_ID = 12 + +BATTERY_SUFFIX = " Battery" +PERFUME_SUFFIX = " Perfume" +FILL_SUFFIX = " Fill" +WIFI_SUFFIX = " Wifi" + +ATTR_SIGNAL_STRENGTH = "signal_strength" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the diffuser sensors.""" + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] + entities: list[DiffuserEntity] = [] + for hublot, diffuser in diffusers.items(): + coordinator = coordinators[hublot] + entities.append(DiffuserPerfumeSensor(diffuser, coordinator)) + entities.append(DiffuserFillSensor(diffuser, coordinator)) + entities.append(DiffuserWifiSensor(diffuser, coordinator)) + if diffuser.has_battery: + entities.append(DiffuserBatterySensor(diffuser, coordinator)) + + async_add_entities(entities) + + +class DiffuserPerfumeSensor(DiffuserEntity): + """Representation of a diffuser perfume sensor.""" + + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: + """Initialize the perfume sensor.""" + super().__init__(diffuser, coordinator, PERFUME_SUFFIX) + + @property + def icon(self) -> str: + """Return the perfume sensor icon.""" + if self._diffuser.hub_data[SENSORS][PERFUME][ID] == PERFUME_NO_CARTRIDGE_ID: + return "mdi:tag-remove" + return "mdi:tag-text" + + @property + def state(self) -> str: + """Return the state of the perfume sensor.""" + return self._diffuser.hub_data[SENSORS][PERFUME][TITLE] + + +class DiffuserFillSensor(DiffuserEntity): + """Representation of a diffuser fill sensor.""" + + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: + """Initialize the fill sensor.""" + super().__init__(diffuser, coordinator, FILL_SUFFIX) + + @property + def icon(self) -> str: + """Return the fill sensor icon.""" + if self._diffuser.hub_data[SENSORS][FILL][ID] == FILL_NO_CARTRIDGE_ID: + return "mdi:beaker-question" + return "mdi:beaker" + + @property + def state(self) -> str: + """Return the state of the fill sensor.""" + return self._diffuser.hub_data[SENSORS][FILL][TITLE] + + +class DiffuserBatterySensor(DiffuserEntity): + """Representation of a diffuser battery sensor.""" + + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: + """Initialize the battery sensor.""" + super().__init__(diffuser, coordinator, BATTERY_SUFFIX) + + @property + def state(self) -> int: + """Return the state of the battery sensor.""" + return self._diffuser.battery_percentage + + @property + def device_class(self) -> str: + """Return the class of the battery sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def unit_of_measurement(self) -> str: + """Return the battery unit of measurement.""" + return PERCENTAGE + + +class DiffuserWifiSensor(DiffuserEntity): + """Representation of a diffuser wifi sensor.""" + + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: + """Initialize the wifi sensor.""" + super().__init__(diffuser, coordinator, WIFI_SUFFIX) + + @property + def state(self) -> int: + """Return the state of the wifi sensor.""" + return self._diffuser.wifi_percentage + + @property + def device_class(self) -> str: + """Return the class of the wifi sensor.""" + return DEVICE_CLASS_SIGNAL_STRENGTH + + @property + def unit_of_measurement(self) -> str: + """Return the wifi unit of measurement.""" + return PERCENTAGE diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index bc8e2b5e17599..a2ca89dc2ac3c 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,104 +1,85 @@ """Support for Rituals Perfume Genie switches.""" -from datetime import timedelta -import logging +from __future__ import annotations -import aiohttp +from typing import Any -from homeassistant.components.switch import SwitchEntity +from pyrituals import Diffuser -from .const import DOMAIN +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback -_LOGGER = logging.getLogger(__name__) +from . import RitualsDataUpdateCoordinator +from .const import ATTRIBUTES, COORDINATORS, DEVICES, DOMAIN +from .entity import DiffuserEntity -SCAN_INTERVAL = timedelta(seconds=30) +FAN = "fanc" +SPEED = "speedc" +ROOM = "roomc" ON_STATE = "1" -AVAILABLE_STATE = 1 -MANUFACTURER = "Rituals Cosmetics" -MODEL = "Diffuser" -ICON = "mdi:fan" - -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the diffuser switch.""" - account = hass.data[DOMAIN][config_entry.entry_id] - diffusers = await account.get_devices() - + diffusers = hass.data[DOMAIN][config_entry.entry_id][DEVICES] + coordinators = hass.data[DOMAIN][config_entry.entry_id][COORDINATORS] entities = [] - for diffuser in diffusers: - entities.append(DiffuserSwitch(diffuser)) + for hublot, diffuser in diffusers.items(): + coordinator = coordinators[hublot] + entities.append(DiffuserSwitch(diffuser, coordinator)) - async_add_entities(entities, True) + async_add_entities(entities) -class DiffuserSwitch(SwitchEntity): +class DiffuserSwitch(SwitchEntity, DiffuserEntity): """Representation of a diffuser switch.""" - def __init__(self, diffuser): - """Initialize the switch.""" - self._diffuser = diffuser - self._available = True - - @property - def device_info(self): - """Return information about the device.""" - return { - "name": self._diffuser.data["hub"]["attributes"]["roomnamec"], - "identifiers": {(DOMAIN, self._diffuser.data["hub"]["hublot"])}, - "manufacturer": MANUFACTURER, - "model": MODEL, - "sw_version": self._diffuser.data["hub"]["sensors"]["versionc"], - } - - @property - def unique_id(self): - """Return the unique ID of the device.""" - return self._diffuser.data["hub"]["hublot"] - - @property - def available(self): - """Return if the device is available.""" - return self._available + def __init__( + self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator + ) -> None: + """Initialize the diffuser switch.""" + super().__init__(diffuser, coordinator, "") + self._is_on = self._diffuser.is_on @property - def name(self): - """Return the name of the device.""" - return self._diffuser.data["hub"]["attributes"]["roomnamec"] - - @property - def icon(self): + def icon(self) -> str: """Return the icon of the device.""" - return ICON + return "mdi:fan" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = { - "fan_speed": self._diffuser.data["hub"]["attributes"]["speedc"], - "room_size": self._diffuser.data["hub"]["attributes"]["roomc"], + "fan_speed": self._diffuser.hub_data[ATTRIBUTES][SPEED], + "room_size": self._diffuser.hub_data[ATTRIBUTES][ROOM], } return attributes @property - def is_on(self): + def is_on(self) -> bool: """If the device is currently on or off.""" - return self._diffuser.data["hub"]["attributes"]["fanc"] == ON_STATE + return self._is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._diffuser.turn_on() + self._is_on = True + self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._diffuser.turn_off() - - async def async_update(self): - """Update the data of the device.""" - try: - await self._diffuser.update_data() - except aiohttp.ClientError: - self._available = False - _LOGGER.error("Unable to retrieve data from rituals.sense-company.com") - else: - self._available = self._diffuser.data["hub"]["status"] == AVAILABLE_STATE + self._is_on = False + self.async_write_ha_state() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._is_on = self._diffuser.is_on + self.async_write_ha_state() diff --git a/homeassistant/components/rituals_perfume_genie/translations/cs.json b/homeassistant/components/rituals_perfume_genie/translations/cs.json new file mode 100644 index 0000000000000..29c2ebc17138c --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/nl.json b/homeassistant/components/rituals_perfume_genie/translations/nl.json index 432079cac257f..ddc5fcb062f87 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/nl.json +++ b/homeassistant/components/rituals_perfume_genie/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json index c91a500edd81f..f7fb5fcbab302 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json +++ b/homeassistant/components/rituals_perfume_genie/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 68f895cb2b885..a8e2b2533241d 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -3,9 +3,10 @@ "name": "RMV", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": [ - "PyRMVtransport==0.3.1" + "PyRMVtransport==0.3.2" ], "codeowners": [ "@cgtobi" - ] + ], + "iot_class": "cloud_polling" } \ No newline at end of file diff --git a/homeassistant/components/rocketchat/manifest.json b/homeassistant/components/rocketchat/manifest.json index 23798ff5df135..13e6a7bb745a1 100644 --- a/homeassistant/components/rocketchat/manifest.json +++ b/homeassistant/components/rocketchat/manifest.json @@ -3,5 +3,6 @@ "name": "Rocket.Chat", "documentation": "https://www.home-assistant.io/integrations/rocketchat", "requirements": ["rocketchat-API==0.6.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index f8294c878dde2..e81f5260ac1da 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,10 +1,8 @@ """Support for Roku.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging -from typing import Any from rokuecp import Roku, RokuConnectionError, RokuError from rokuecp.models import Device @@ -13,9 +11,10 @@ from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST +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 HomeAssistantType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,14 +38,9 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass: HomeAssistantType, config: dict) -> bool: - """Set up the Roku integration.""" - hass.data.setdefault(DOMAIN, {}) - return True - - -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" + hass.data.setdefault(DOMAIN, {}) coordinator = hass.data[DOMAIN].get(entry.entry_id) if not coordinator: coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) @@ -54,28 +48,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) - + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok @@ -100,10 +82,10 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): def __init__( self, - hass: HomeAssistantType, + hass: HomeAssistant, *, host: str, - ): + ) -> None: """Initialize global Roku data updater.""" self.roku = Roku(host=host, session=async_get_clientsession(hass)) @@ -151,7 +133,7 @@ def name(self) -> str: return self._name @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this Roku device.""" if self._device_id is None: return None diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 8424850fe6c6d..470dccbe37fb1 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any from urllib.parse import urlparse from rokuecp import Roku, RokuError @@ -13,11 +12,11 @@ ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL, ) -from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN @@ -29,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistantType, data: dict) -> dict: +async def validate_input(hass: HomeAssistant, data: dict) -> dict: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -48,14 +47,13 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Roku config flow.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL def __init__(self): """Set up the instance.""" self.discovery_info = {} @callback - def _show_form(self, errors: dict | None = None) -> dict[str, Any]: + def _show_form(self, errors: dict | None = None) -> FlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -63,7 +61,7 @@ def _show_form(self, errors: dict | None = None) -> dict[str, Any]: errors=errors or {}, ) - async def async_step_user(self, user_input: dict | None = None) -> dict[str, Any]: + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle a flow initialized by the user.""" if not user_input: return self._show_form() @@ -90,8 +88,7 @@ async def async_step_homekit(self, discovery_info): # If we already have the host configured do # not open connections to it if we can avoid it. - if self._host_already_configured(discovery_info[CONF_HOST]): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: discovery_info[CONF_HOST]}) self.discovery_info.update({CONF_HOST: discovery_info[CONF_HOST]}) @@ -114,9 +111,7 @@ async def async_step_homekit(self, discovery_info): return await self.async_step_discovery_confirm() - async def async_step_ssdp( - self, discovery_info: dict | None = None - ) -> dict[str, Any]: + async def async_step_ssdp(self, discovery_info: dict | None = None) -> FlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] @@ -142,7 +137,7 @@ async def async_step_ssdp( async def async_step_discovery_confirm( self, user_input: dict | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle user-confirmation of discovered device.""" if user_input is None: return self.async_show_form( @@ -155,12 +150,3 @@ async def async_step_discovery_confirm( title=self.discovery_info[CONF_NAME], data=self.discovery_info, ) - - def _host_already_configured(self, host): - """See if we already have a hub with the host address configured.""" - existing_hosts = { - entry.data[CONF_HOST] - for entry in self._async_current_entries() - if CONF_HOST in entry.data - } - return host in existing_hosts diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 981a9b080777d..81e3af86bb5bd 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -4,13 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": ["rokuecp==0.8.1"], "homekit": { - "models": [ - "3810X", - "4660X", - "7820X", - "C105X", - "C135X" - ] + "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, "ssdp": [ { @@ -21,5 +15,6 @@ ], "codeowners": ["@ctalkington"], "quality_scale": "silver", - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 6fee53595ac82..ce5a77f06f6e1 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass, entry, async_add_entities): unique_id = coordinator.data.info.serial_number async_add_entities([RokuMediaPlayer(unique_id, coordinator)], True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SEARCH, diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index da57866757866..7eb8396d6fac2 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,20 +1,19 @@ """Support for the Roku remote.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler from .const import DOMAIN async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list, bool], None], + async_add_entities: AddEntitiesCallback, ) -> bool: """Load Roku remote based on a config entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/roku/services.yaml b/homeassistant/components/roku/services.yaml index 1d2153061575b..16fd51ea95be4 100644 --- a/homeassistant/components/roku/services.yaml +++ b/homeassistant/components/roku/services.yaml @@ -1,9 +1,15 @@ search: + name: Search description: Emulates opening the search screen and entering the search keyword. + target: + entity: + integration: roku + domain: media_player fields: - entity_id: - description: The entities to search on. - example: "media_player.roku" keyword: + name: Keyword description: The keyword to search for. + required: true example: "Space Jam" + selector: + text: diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 3523615ff33a4..235cf4ad159b9 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "user": { "description": "Enter your Roku information.", diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json index b60b8f83eb9d3..be84d78fbff0d 100644 --- a/homeassistant/components/roku/translations/ca.json +++ b/homeassistant/components/roku/translations/ca.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Vols configurar {name}?", diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json index 2b54cafe8909a..192b9b2308538 100644 --- a/homeassistant/components/roku/translations/en.json +++ b/homeassistant/components/roku/translations/en.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Do you want to set up {name}?", diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 95e42643379c1..189a4aec17994 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/roku/translations/et.json b/homeassistant/components/roku/translations/et.json index 17bce39f5dfca..bb496ab871613 100644 --- a/homeassistant/components/roku/translations/et.json +++ b/homeassistant/components/roku/translations/et.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Kas soovid seadistada {name}?", diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index 3c11aa4d8ae12..5d83396024013 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Vuoi configurare {name}?", diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json index daecee2f1dc4e..6bf3435a19b41 100644 --- a/homeassistant/components/roku/translations/nl.json +++ b/homeassistant/components/roku/translations/nl.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "data": { diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index e7dc663b8f800..1bbd5bbea8604 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Vil du konfigurere {name}?", diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index 1a570c6434776..41ea348543ee9 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "data": { diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json index c3ae135ed76ae..4ba55bc8e1af2 100644 --- a/homeassistant/components/roku/translations/ru.json +++ b/homeassistant/components/roku/translations/ru.json @@ -8,7 +8,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": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index 429c03a991ea8..5cfe9232301fc 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "Roku\uff1a{name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 6de775e1d997f..3936d3f6d1dfe 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -3,22 +3,30 @@ import logging import async_timeout -from roombapy import Roomba, RoombaConnectionError +from roombapy import RoombaConnectionError, RoombaFactory from homeassistant import exceptions -from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD - -from .const import BLID, CONF_BLID, CONF_CONTINUOUS, DOMAIN, PLATFORMS, ROOMBA_SESSION +from homeassistant.const import ( + CONF_DELAY, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + EVENT_HOMEASSISTANT_STOP, +) + +from .const import ( + BLID, + CANCEL_STOP, + CONF_BLID, + CONF_CONTINUOUS, + DOMAIN, + PLATFORMS, + ROOMBA_SESSION, +) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - """Set up the roomba environment.""" - hass.data.setdefault(DOMAIN, {}) - return True - - async def async_setup_entry(hass, config_entry): """Set the config entry up.""" # Set up roomba platforms with config entry @@ -32,7 +40,7 @@ async def async_setup_entry(hass, config_entry): }, ) - roomba = Roomba( + roomba = RoombaFactory.create_roomba( address=config_entry.data[CONF_HOST], blid=config_entry.data[CONF_BLID], password=config_entry.data[CONF_PASSWORD], @@ -46,15 +54,21 @@ async def async_setup_entry(hass, config_entry): except CannotConnect as err: raise exceptions.ConfigEntryNotReady from err + async def _async_disconnect_roomba(event): + await async_disconnect_or_timeout(hass, roomba) + + cancel_stop = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_disconnect_roomba + ) + + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = { ROOMBA_SESSION: roomba, BLID: config_entry.data[CONF_BLID], + CANCEL_STOP: cancel_stop, } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) if not config_entry.update_listeners: config_entry.add_update_listener(async_update_options) @@ -76,12 +90,12 @@ async def async_connect_or_timeout(hass, roomba): break await asyncio.sleep(1) except RoombaConnectionError as err: - _LOGGER.error("Error to connect to vacuum") + _LOGGER.debug("Error to connect to vacuum: %s", err) raise CannotConnect from err except asyncio.TimeoutError as err: # api looping if user or password incorrect and roomba exist await async_disconnect_or_timeout(hass, roomba) - _LOGGER.error("Timeout expired") + _LOGGER.debug("Timeout expired: %s", err) raise CannotConnect from err return {ROOMBA_SESSION: roomba, CONF_NAME: name} @@ -102,16 +116,12 @@ async def async_update_options(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, platform) - for platform in PLATFORMS - ] - ) + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS ) if unload_ok: domain_data = hass.data[DOMAIN][config_entry.entry_id] + domain_data[CANCEL_STOP]() await async_disconnect_or_timeout(hass, roomba=domain_data[ROOMBA_SESSION]) hass.data[DOMAIN].pop(config_entry.entry_id) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 45c2d8b9a1bdc..c3ccd051dd8cf 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -2,7 +2,7 @@ import asyncio -from roombapy import Roomba +from roombapy import RoombaFactory from roombapy.discovery import RoombaDiscovery from roombapy.getpassword import RoombaPassword import voluptuous as vol @@ -40,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - roomba = Roomba( + roomba = RoombaFactory.create_roomba( address=data[CONF_HOST], blid=data[CONF_BLID], password=data[CONF_PASSWORD], @@ -63,7 +63,6 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Roomba configuration flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the roomba flow.""" @@ -78,16 +77,15 @@ def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - if self._async_host_already_configured(dhcp_discovery[IP_ADDRESS]): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: discovery_info[IP_ADDRESS]}) - if not dhcp_discovery[HOSTNAME].startswith(("irobot-", "roomba-")): + if not discovery_info[HOSTNAME].startswith(("irobot-", "roomba-")): return self.async_abort(reason="not_irobot_device") - self.host = dhcp_discovery[IP_ADDRESS] - self.blid = _async_blid_from_hostname(dhcp_discovery[HOSTNAME]) + self.host = discovery_info[IP_ADDRESS] + self.blid = _async_blid_from_hostname(discovery_info[HOSTNAME]) await self.async_set_unique_id(self.blid) self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) @@ -184,11 +182,7 @@ async def async_step_manual(self, user_input=None): ), ) - if any( - user_input["host"] == entry.data.get("host") - for entry in self._async_current_entries() - ): - return self.async_abort(reason="already_configured") + self._async_abort_entries_match({CONF_HOST: user_input["host"]}) self.host = user_input[CONF_HOST] self.blid = user_input[CONF_BLID].upper() @@ -261,14 +255,6 @@ async def async_step_link_manual(self, user_input=None): errors=errors, ) - @callback - def _async_host_already_configured(self, host): - """See if we already have an entry matching the host.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == host: - return True - return False - class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options.""" @@ -328,9 +314,8 @@ async def _async_discover_roombas(hass, host): discovery = _async_get_roomba_discovery() try: if host: - discovered = [ - await hass.async_add_executor_job(discovery.get, host) - ] + device = await hass.async_add_executor_job(discovery.get, host) + discovered = [device] if device else [] else: discovered = await hass.async_add_executor_job(discovery.get_all) except OSError: diff --git a/homeassistant/components/roomba/const.py b/homeassistant/components/roomba/const.py index 0509cd9211627..2e59279cfdb0b 100644 --- a/homeassistant/components/roomba/const.py +++ b/homeassistant/components/roomba/const.py @@ -9,3 +9,4 @@ DEFAULT_DELAY = 1 ROOMBA_SESSION = "roomba_session" BLID = "blid_key" +CANCEL_STOP = "cancel_stop" diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 9d6a0f5cafc53..45a69d3857613 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -21,6 +21,7 @@ SUPPORT_STOP, StateVacuumEntity, ) +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import Entity from . import roomba_reported_state @@ -92,13 +93,18 @@ def unique_id(self): @property def device_info(self): """Return the device info of the vacuum cleaner.""" - return { + info = { "identifiers": {(DOMAIN, self.robot_unique_id)}, "manufacturer": "iRobot", "name": str(self._name), "sw_version": self._version, "model": self._sku, } + if mac_address := self.vacuum_state.get("hwPartsRev", {}).get( + "wlan0HwAddr", self.vacuum_state.get("mac") + ): + info["connections"] = {(dr.CONNECTION_NETWORK_MAC, mac_address)} + return info @property def _battery_level(self): diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index d1858a46fdc9d..2aaa1f6762e5b 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,17 +3,17 @@ "name": "iRobot Roomba and Braava", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": ["roombapy==1.6.2"], + "requirements": ["roombapy==1.6.3"], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], "dhcp": [ - { - "hostname" : "irobot-*", - "macaddress" : "501479*" - }, - { - "hostname" : "roomba-*", - "macaddress" : "80A589*" - } - ] + { + "hostname": "irobot-*", + "macaddress": "501479*" + }, + { + "hostname": "roomba-*", + "macaddress": "80A589*" + } + ], + "iot_class": "local_push" } - diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index 16371041a15a6..867d2bf633fc6 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "title": "Automatically connect to the device", diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json index 3bdf842df9b66..d41b7d3833f7c 100644 --- a/homeassistant/components/roomba/translations/ca.json +++ b/homeassistant/components/roomba/translations/ca.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index 780d406bcafbc..b66a6681be700 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", - "not_irobot_device": "Das erkannte Ger\u00e4t ist kein iRobot-Ger\u00e4t" + "not_irobot_device": "Das erkannte Ger\u00e4t ist kein iRobot-Ger\u00e4t", + "short_blid": "Das BLID wurde abgeschnitten" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 5cc06f0cb5d36..32853564e5336 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index 29f0b47a655eb..c78b66bbb87ae 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "not_irobot_device": "El dispositivo descubierto no es un dispositivo iRobot" + "not_irobot_device": "El dispositivo descubierto no es un dispositivo iRobot", + "short_blid": "El BLID ha sido truncado" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/roomba/translations/et.json b/homeassistant/components/roomba/translations/et.json index 7943df95b4f2d..0f992a57de6d8 100644 --- a/homeassistant/components/roomba/translations/et.json +++ b/homeassistant/components/roomba/translations/et.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "iRobot {name} ( {host} )", + "flow_title": "{name} ( {host} )", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index b4bc615e4e3d7..767d7a9708a83 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "L'appareil est d\u00e9ja configur\u00e9 ", "cannot_connect": "Echec de connection", - "not_irobot_device": "L'appareil d\u00e9couvert n'est pas un appareil iRobot" + "not_irobot_device": "L'appareil d\u00e9couvert n'est pas un appareil iRobot", + "short_blid": "La BLID a \u00e9t\u00e9 tronqu\u00e9" }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" @@ -33,7 +34,7 @@ "blid": "BLID", "host": "H\u00f4te" }, - "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}", + "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}\u00b4", "title": "Se connecter manuellement \u00e0 l'appareil" }, "user": { diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 8f7c2c97884ab..931671f92d210 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "not_irobot_device": "A felfedezett eszk\u00f6z nem iRobot eszk\u00f6z" + "not_irobot_device": "A felfedezett eszk\u00f6z nem iRobot eszk\u00f6z", + "short_blid": "fel lett oldva" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/roomba/translations/id.json b/homeassistant/components/roomba/translations/id.json index 3afe75ae09da8..aaffac267aa47 100644 --- a/homeassistant/components/roomba/translations/id.json +++ b/homeassistant/components/roomba/translations/id.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", - "not_irobot_device": "Perangkat yang ditemukan bukan perangkat iRobot" + "not_irobot_device": "Perangkat yang ditemukan bukan perangkat iRobot", + "short_blid": "BLID terpotong" }, "error": { "cannot_connect": "Gagal terhubung" diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json index 5e2e2b47141b1..0b2c9079ac900 100644 --- a/homeassistant/components/roomba/translations/it.json +++ b/homeassistant/components/roomba/translations/it.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json index 5066225100bb9..bb33287c9b83e 100644 --- a/homeassistant/components/roomba/translations/ko.json +++ b/homeassistant/components/roomba/translations/ko.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "not_irobot_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 \uc544\uc774\ub85c\ubd07 \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" + "not_irobot_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 \uc544\uc774\ub85c\ubd07 \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", + "short_blid": "BLID\uac00 \uc798\ub838\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" @@ -18,7 +19,7 @@ "title": "\uae30\uae30\uc5d0 \uc790\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30" }, "link": { - "description": "\uae30\uae30\uc5d0\uc11c \uc18c\ub9ac\uac00 \ub0a0 \ub54c\uae4c\uc9c0 {name}\uc758 \ud648 \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub7ec\uc8fc\uc138\uc694 (\uc57d 2\ucd08).", + "description": "\uae30\uae30\uc5d0\uc11c \uc18c\ub9ac\uac00 \ub0a0 \ub54c\uae4c\uc9c0 {name}\uc758 \ud648 \ubc84\ud2bc\uc744 \uae38\uac8c \ub204\ub978 \ub2e4\uc74c(\uc57d 2\ucd08) 30\ucd08 \uc774\ub0b4\uc5d0 \ud655\uc778 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", "title": "\ube44\ubc00\ubc88\ud638 \uac00\uc838\uc624\uae30" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "\ud638\uc2a4\ud2b8" }, - "description": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\uac00 \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. BLID\ub294 `iRobot-` \ub4a4\uc758 \uae30\uae30 \ud638\uc2a4\ud2b8 \uc774\ub984 \ubd80\ubd84\uc785\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \ub098\uc640 \uc788\ub294 {auth_help_url} \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694.", + "description": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\uac00 \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. BLID\ub294 `iRobot-` \ub610\ub294 `Roomba-` \ub4a4\uc5d0 \uc788\ub294 \uae30\uae30 \ud638\uc2a4\ud2b8 \uc774\ub984\uc758 \uc77c\ubd80\uc785\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \uc124\uba85\ub41c \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694: {auth_help_url}", "title": "\uae30\uae30\uc5d0 \uc218\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index f26b28d22486e..a18bd89ae1275 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -3,12 +3,13 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", - "not_irobot_device": "Het gevonden apparaat is geen iRobot-apparaat" + "not_irobot_device": "Het gevonden apparaat is geen iRobot-apparaat", + "short_blid": "De BLID is afgekapt" }, "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -18,7 +19,7 @@ "title": "Automatisch verbinding maken met het apparaat" }, "link": { - "description": "Houd de Home-knop op {name} ingedrukt totdat het apparaat een geluid genereert (ongeveer twee seconden).", + "description": "Houd de Home-knop op {name} ingedrukt totdat het apparaat een geluid genereert (ongeveer twee seconden), bevestig vervolgens binnen 30 seconden.", "title": "Wachtwoord opvragen" }, "link_manual": { diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index 67df735719c9b..2caba79f50cde 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -3,12 +3,13 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "not_irobot_device": "Oppdaget enhet er ikke en iRobot-enhet" + "not_irobot_device": "Oppdaget enhet er ikke en iRobot-enhet", + "short_blid": "BLID ble avkortet" }, "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -18,7 +19,7 @@ "title": "Koble automatisk til enheten" }, "link": { - "description": "Trykk og hold inne Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder)", + "description": "Trykk og hold nede Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder), og send deretter innen 30 sekunder.", "title": "Hent passord" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "", "host": "Vert" }, - "description": "Ingen Roomba eller Braava har blitt oppdaget i nettverket ditt. BLID er delen av enhetens vertsnavn etter `iRobot-`. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", + "description": "Ingen Roomba eller Braava har blitt oppdaget i nettverket ditt. BLID er delen av enhetens vertsnavn etter `iRobot-` eller `Roomba-`. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", "title": "Koble til enheten manuelt" }, "user": { diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index e4951a366ddd2..d4624a9190664 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -3,12 +3,13 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "not_irobot_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem iRobot" + "not_irobot_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem iRobot", + "short_blid": "BLID zosta\u0142 obci\u0119ty" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -18,7 +19,7 @@ "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem" }, "link": { - "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy).", + "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie prze\u015blij w ci\u0105gu 30 sekund.", "title": "Odzyskiwanie has\u0142a" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "Nazwa hosta lub adres IP" }, - "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava. BLID to cz\u0119\u015b\u0107 nazwy hosta urz\u0105dzenia po `iRobot-`. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", + "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava. BLID to cz\u0119\u015b\u0107 nazwy hosta urz\u0105dzenia po `iRobot-` lub 'Roomba-'. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", "title": "R\u0119czne po\u0142\u0105czenie z urz\u0105dzeniem" }, "user": { diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index 26e5c61faf332..6ff4feb4e9cce 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -9,7 +9,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": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -45,7 +45,7 @@ "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "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, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c BLID \u0438 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "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, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c BLID \u0438 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" } } diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 790eba79c032d..81ba19a3a5732 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -1,14 +1,15 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e" + "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e", + "short_blid": "BLID \u906d\u622a\u77ed" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -18,7 +19,7 @@ "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" }, "link": { - "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\u3002", + "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\uff0c\u7136\u5f8c\u65bc 30 \u79d2\u5167\u50b3\u9001\u3002", "title": "\u91cd\u7f6e\u5bc6\u78bc" }, "link_manual": { @@ -33,7 +34,7 @@ "blid": "BLID", "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002BLID \u88dd\u7f6e\u65bc\u4e3b\u6a5f\u7aef\u7684\u90e8\u5206\u540d\u7a31\u70ba `iRobot-` \u958b\u982d\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", + "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002BLID \u88dd\u7f6e\u65bc\u4e3b\u6a5f\u7aef\u7684\u90e8\u5206\u540d\u7a31\u70ba `iRobot-` \u6216 `Roomba-` \u958b\u982d\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", "title": "\u624b\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" }, "user": { diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index 49527a44245a0..c9dbe86ee4b46 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -6,14 +6,9 @@ from .server import RoonServer -async def async_setup(hass, config): - """Set up the Roon platform.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass, entry): """Set up a roonserver from a config entry.""" + hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] roonserver = RoonServer(hass, entry) diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py index b1db9e39a25a0..799d50bdaaba7 100644 --- a/homeassistant/components/roon/config_flow.py +++ b/homeassistant/components/roon/config_flow.py @@ -105,7 +105,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for roon.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the Roon flow.""" diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index e4c4a25dcb540..09fcaad5f1fa2 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,10 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": [ - "roonapi==0.0.32" - ], - "codeowners": [ - "@pavoni" - ] + "requirements": ["roonapi==0.0.36"], + "codeowners": ["@pavoni"], + "iot_class": "local_push" } diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 773028da2d307..dd8d9e83c2d60 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -7,6 +7,7 @@ from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_BROWSE_MEDIA, + SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -42,6 +43,7 @@ SUPPORT_ROON = ( SUPPORT_BROWSE_MEDIA + | SUPPORT_GROUPING | SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_STOP @@ -59,12 +61,8 @@ _LOGGER = logging.getLogger(__name__) -SERVICE_JOIN = "join" -SERVICE_UNJOIN = "unjoin" SERVICE_TRANSFER = "transfer" -ATTR_JOIN = "join_ids" -ATTR_UNJOIN = "unjoin_ids" ATTR_TRANSFER = "transfer_id" @@ -74,17 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): media_players = set() # Register entity services - platform = entity_platform.current_platform.get() - platform.async_register_entity_service( - SERVICE_JOIN, - {vol.Required(ATTR_JOIN): vol.All(cv.ensure_list, [cv.entity_id])}, - "join", - ) - platform.async_register_entity_service( - SERVICE_UNJOIN, - {vol.Optional(ATTR_UNJOIN): vol.All(cv.ensure_list, [cv.entity_id])}, - "unjoin", - ) + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_TRANSFER, {vol.Required(ATTR_TRANSFER): cv.entity_id}, @@ -164,6 +152,13 @@ def supported_features(self): """Flag media player features that are supported.""" return SUPPORT_ROON + @property + def group_members(self): + """Return the grouped players.""" + + roon_names = self._server.roonapi.grouped_zone_names(self._output_id) + return [self._server.entity_id(roon_name) for roon_name in roon_names] + @property def device_info(self): """Return the device info.""" @@ -491,8 +486,8 @@ def play_media(self, media_type, media_id, **kwargs): path_list, ) - def join(self, join_ids): - """Add another Roon player to this player's join group.""" + def join_players(self, group_members): + """Join `group_members` as a player group with the current player.""" zone_data = self._server.roonapi.zone_by_output_id(self._output_id) if zone_data is None: @@ -511,7 +506,7 @@ def join(self, join_ids): sync_available[zone["display_name"]] = output["output_id"] names = [] - for entity_id in join_ids: + for entity_id in group_members: name = self._server.roon_name(entity_id) if name is None: _LOGGER.error("No roon player found for %s", entity_id) @@ -531,43 +526,17 @@ def join(self, join_ids): [self._output_id] + [sync_available[name] for name in names] ) - def unjoin(self, unjoin_ids=None): - """Remove a Roon player to this player's join group.""" + def unjoin_player(self): + """Remove this player from any group.""" - zone_data = self._server.roonapi.zone_by_output_id(self._output_id) - if zone_data is None: - _LOGGER.error("No zone data for %s", self.name) + if not self._server.roonapi.is_grouped(self._output_id): + _LOGGER.error( + "Can't unjoin player %s because it's not in a group", + self.name, + ) return - join_group = { - output["display_name"]: output["output_id"] - for output in zone_data["outputs"] - if output["display_name"] != self.name - } - - if unjoin_ids is None: - # unjoin everything - names = list(join_group) - else: - names = [] - for entity_id in unjoin_ids: - name = self._server.roon_name(entity_id) - if name is None: - _LOGGER.error("No roon player found for %s", entity_id) - return - - if name not in join_group: - _LOGGER.error( - "Can't unjoin player %s from %s because it's not in the joined group %s", - name, - self.name, - list(join_group), - ) - return - names.append(name) - - _LOGGER.debug("Unjoining %s from %s", names, self.name) - self._server.roonapi.ungroup_outputs([join_group[name] for name in names]) + self._server.roonapi.ungroup_outputs([self._output_id]) async def async_transfer(self, transfer_id): """Transfer playback from this roon player to another.""" diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index 83b620e176ea8..d216dca419dd4 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -28,6 +28,7 @@ def __init__(self, hass, config_entry): self.offline_devices = set() self._exit = False self._roon_name_by_id = {} + self._id_by_roon_name = {} async def async_setup(self, tries=0): """Set up a roon server based on config parameters.""" @@ -78,11 +79,16 @@ def zones(self): def add_player_id(self, entity_id, roon_name): """Register a roon player.""" self._roon_name_by_id[entity_id] = roon_name + self._id_by_roon_name[roon_name] = entity_id def roon_name(self, entity_id): """Get the name of the roon player from entity_id.""" return self._roon_name_by_id.get(entity_id) + def entity_id(self, roon_name): + """Get the id of the roon player from the roon name.""" + return self._id_by_roon_name.get(roon_name) + def stop_roon(self): """Stop background worker.""" self.roonapi.stop() diff --git a/homeassistant/components/roon/services.yaml b/homeassistant/components/roon/services.yaml index ec096effe5baa..d3a33ec2fe746 100644 --- a/homeassistant/components/roon/services.yaml +++ b/homeassistant/components/roon/services.yaml @@ -1,29 +1,17 @@ -join: - description: Group players together. - fields: - entity_id: - description: id of the player that will be the master of the group. - example: "media_player.study" - join_ids: - description: id(s) of the players that will join the master. - example: "['media_player.bedroom', 'media_player.kitchen']" - -unjoin: - description: Remove players from a group. - fields: - entity_id: - description: id of the player that is the master of the group.. - example: "media_player.study" - unjoin_ids: - description: Optional id(s) of the players that will be unjoined from the group. If not specified, all players will be unjoined from the master. - example: "['media_player.bedroom', 'media_player.kitchen']" - transfer: + name: Transfer description: Transfer playback from one player to another. + target: + entity: + integration: roon + domain: media_player fields: - entity_id: - description: id of the source player. - example: "media_player.bedroom" transfer_id: + name: Transfer ID description: id of the destination player. - example: "media_player.study" + required: true + selector: + entity: + integration: roon + domain: media_player + diff --git a/homeassistant/components/roon/translations/ca.json b/homeassistant/components/roon/translations/ca.json index ef32dd00e75f8..f05ac1a4acb6e 100644 --- a/homeassistant/components/roon/translations/ca.json +++ b/homeassistant/components/roon/translations/ca.json @@ -4,7 +4,6 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "duplicate_entry": "Aquest amfitri\u00f3 ja ha estat afegit.", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/roon/translations/cs.json b/homeassistant/components/roon/translations/cs.json index fd01ed1cd2577..20187f9d4d2c9 100644 --- a/homeassistant/components/roon/translations/cs.json +++ b/homeassistant/components/roon/translations/cs.json @@ -4,7 +4,6 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { - "duplicate_entry": "Tento hostitel ji\u017e byl p\u0159id\u00e1n.", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 4416589a23ee4..cd4aae46adc9d 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -4,15 +4,19 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "duplicate_entry": "Dieser Host wurde bereits hinzugef\u00fcgt.", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { + "link": { + "description": "Sie m\u00fcssen den Home Assistant in Roon autorisieren. Nachdem Sie auf \"Submit\" geklickt haben, gehen Sie zur Roon Core-Anwendung, \u00f6ffnen Sie die Einstellungen und aktivieren Sie HomeAssistant auf der Registerkarte \"Extensions\".", + "title": "HomeAssistant in Roon autorisieren" + }, "user": { "data": { "host": "Host" - } + }, + "description": "Roon-Server konnte nicht gefunden werden, bitte geben Sie den Hostnamen oder die IP ein." } } } diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json index fd197369cb84c..873f82d4f68b4 100644 --- a/homeassistant/components/roon/translations/el.json +++ b/homeassistant/components/roon/translations/el.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "duplicate_entry": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03b5\u03af." - }, "step": { "link": { "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03c3\u03c4\u03bf Roon. \u0391\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Roon Core, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u0395\u03c0\u03b5\u03ba\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2.", diff --git a/homeassistant/components/roon/translations/en.json b/homeassistant/components/roon/translations/en.json index b763fbb1e0c1e..d1f86fbce5f28 100644 --- a/homeassistant/components/roon/translations/en.json +++ b/homeassistant/components/roon/translations/en.json @@ -4,7 +4,6 @@ "already_configured": "Device is already configured" }, "error": { - "duplicate_entry": "That host has already been added.", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, diff --git a/homeassistant/components/roon/translations/es.json b/homeassistant/components/roon/translations/es.json index daf3200a0e41b..e38b7691f405c 100644 --- a/homeassistant/components/roon/translations/es.json +++ b/homeassistant/components/roon/translations/es.json @@ -4,7 +4,6 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "duplicate_entry": "Ese host ya ha sido a\u00f1adido.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/roon/translations/et.json b/homeassistant/components/roon/translations/et.json index e29b1ccc6c6d1..bbdf2b5edc5ca 100644 --- a/homeassistant/components/roon/translations/et.json +++ b/homeassistant/components/roon/translations/et.json @@ -4,7 +4,6 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { - "duplicate_entry": "See host on juba lisatud.", "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu viga" }, diff --git a/homeassistant/components/roon/translations/fr.json b/homeassistant/components/roon/translations/fr.json index 7e61b556d397a..94e31ba445fdc 100644 --- a/homeassistant/components/roon/translations/fr.json +++ b/homeassistant/components/roon/translations/fr.json @@ -4,7 +4,6 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "duplicate_entry": "Cet h\u00f4te a d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9.", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index 6ea0aa2b25c3a..f05fd8385721e 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -4,7 +4,6 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "duplicate_entry": "Ez a hoszt m\u00e1r konfigur\u00e1lva van.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/roon/translations/id.json b/homeassistant/components/roon/translations/id.json index bfd70955ac86b..96b26640320db 100644 --- a/homeassistant/components/roon/translations/id.json +++ b/homeassistant/components/roon/translations/id.json @@ -4,7 +4,6 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "duplicate_entry": "Host ini telah ditambahkan.", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/roon/translations/it.json b/homeassistant/components/roon/translations/it.json index e0450af9d3919..e21a74aae4356 100644 --- a/homeassistant/components/roon/translations/it.json +++ b/homeassistant/components/roon/translations/it.json @@ -4,7 +4,6 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "duplicate_entry": "Questo host \u00e8 gi\u00e0 stato aggiunto.", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/roon/translations/ko.json b/homeassistant/components/roon/translations/ko.json index b1e0fee408921..a55249417afbc 100644 --- a/homeassistant/components/roon/translations/ko.json +++ b/homeassistant/components/roon/translations/ko.json @@ -4,7 +4,6 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "duplicate_entry": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\ub294 \uc774\ubbf8 \ucd94\uac00\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/roon/translations/lb.json b/homeassistant/components/roon/translations/lb.json index a8fef968e8195..f5722ee75bb89 100644 --- a/homeassistant/components/roon/translations/lb.json +++ b/homeassistant/components/roon/translations/lb.json @@ -4,7 +4,6 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert" }, "error": { - "duplicate_entry": "D\u00ebsen Apparat gouf scho dob\u00e4igesat.", "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" }, diff --git a/homeassistant/components/roon/translations/nl.json b/homeassistant/components/roon/translations/nl.json index 535c56a2f983a..452436d0e05a7 100644 --- a/homeassistant/components/roon/translations/nl.json +++ b/homeassistant/components/roon/translations/nl.json @@ -4,7 +4,6 @@ "already_configured": "Apparaat is al toegevoegd" }, "error": { - "duplicate_entry": "Die host is al toegevoegd.", "invalid_auth": "Ongeldige authencatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json index e872e03a69d20..2558c2c27c73b 100644 --- a/homeassistant/components/roon/translations/no.json +++ b/homeassistant/components/roon/translations/no.json @@ -4,7 +4,6 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "duplicate_entry": "Denne verten er allerede lagt til.", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json index d763fc12bd26a..7ebfd0ad777b3 100644 --- a/homeassistant/components/roon/translations/pl.json +++ b/homeassistant/components/roon/translations/pl.json @@ -4,7 +4,6 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "duplicate_entry": "Ten host lub adres IP zosta\u0142 ju\u017c dodany", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/roon/translations/pt-BR.json b/homeassistant/components/roon/translations/pt-BR.json index cbcba0352a920..39538d2bf5232 100644 --- a/homeassistant/components/roon/translations/pt-BR.json +++ b/homeassistant/components/roon/translations/pt-BR.json @@ -4,7 +4,6 @@ "already_configured": "O dispositivo j\u00e1 foi configurado" }, "error": { - "duplicate_entry": "Esse host j\u00e1 foi adicionado.", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Ocorreu um erro inexperado" }, diff --git a/homeassistant/components/roon/translations/ru.json b/homeassistant/components/roon/translations/ru.json index c01006d626966..1bcb07c769546 100644 --- a/homeassistant/components/roon/translations/ru.json +++ b/homeassistant/components/roon/translations/ru.json @@ -4,7 +4,6 @@ "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": { - "duplicate_entry": "\u042d\u0442\u043e\u0442 \u0445\u043e\u0441\u0442 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d.", "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." }, diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json index 97241919c9b5f..94e452e48bbeb 100644 --- a/homeassistant/components/roon/translations/tr.json +++ b/homeassistant/components/roon/translations/tr.json @@ -4,7 +4,6 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "duplicate_entry": "Bu ana bilgisayar zaten eklendi.", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/roon/translations/uk.json b/homeassistant/components/roon/translations/uk.json index 91a530787ae37..a892e24692ddd 100644 --- a/homeassistant/components/roon/translations/uk.json +++ b/homeassistant/components/roon/translations/uk.json @@ -4,7 +4,6 @@ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." }, "error": { - "duplicate_entry": "\u0426\u0435\u0439 \u0445\u043e\u0441\u0442 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0438\u0439.", "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index 39099753f39ed..f8f52d11b1c4e 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -1,10 +1,9 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "duplicate_entry": "\u8a72\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u3002", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 4879f12a3be82..1611fdad6fc3b 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -2,6 +2,7 @@ "domain": "route53", "name": "AWS Route53", "documentation": "https://www.home-assistant.io/integrations/route53", - "requirements": ["boto3==1.9.252"], - "codeowners": [] + "requirements": ["boto3==1.16.52"], + "codeowners": [], + "iot_class": "cloud_push" } diff --git a/homeassistant/components/route53/services.yaml b/homeassistant/components/route53/services.yaml index 3ca109fcb3693..4936a49976401 100644 --- a/homeassistant/components/route53/services.yaml +++ b/homeassistant/components/route53/services.yaml @@ -1,2 +1,3 @@ update_records: + name: Update records description: Trigger update of records. diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index b3635b39f38c3..27421b2093695 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -3,5 +3,6 @@ "name": "ROVA", "documentation": "https://www.home-assistant.io/integrations/rova", "requirements": ["rova==0.2.1"], - "codeowners": [] + "codeowners": [], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index 47ce87c4a8d6a..2d7edd83fed62 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -56,9 +56,8 @@ def delete_temp_file(*args): # If no file path is defined, use a temporary file if file_path is None: - temp_file = NamedTemporaryFile(suffix=".jpg", delete=False) - temp_file.close() - file_path = temp_file.name + with NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file: + file_path = temp_file.name setup_config[CONF_FILE_PATH] = file_path hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, delete_temp_file) diff --git a/homeassistant/components/rpi_camera/manifest.json b/homeassistant/components/rpi_camera/manifest.json index 5f42be58ffe71..cc4cbbace88cb 100644 --- a/homeassistant/components/rpi_camera/manifest.json +++ b/homeassistant/components/rpi_camera/manifest.json @@ -2,5 +2,6 @@ "domain": "rpi_camera", "name": "Raspberry Pi Camera", "documentation": "https://www.home-assistant.io/integrations/rpi_camera", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 1a73c736d04c3..d09c21779fe80 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -3,5 +3,6 @@ "name": "Raspberry Pi GPIO", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": ["RPi.GPIO==0.7.1a4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/rpi_gpio/services.yaml b/homeassistant/components/rpi_gpio/services.yaml index d0564941cdb96..1858c5a9fa274 100644 --- a/homeassistant/components/rpi_gpio/services.yaml +++ b/homeassistant/components/rpi_gpio/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload all rpi_gpio entities. diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json index 35d09ea92bf02..ea0bdbcb0f357 100644 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ b/homeassistant/components/rpi_gpio_pwm/manifest.json @@ -3,5 +3,6 @@ "name": "pigpio Daemon PWM LED", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", "requirements": ["pwmled==1.6.7"], - "codeowners": ["@soldag"] + "codeowners": ["@soldag"], + "iot_class": "local_push" } diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json index f40c34a11a4f1..9e8f0a30e87f7 100644 --- a/homeassistant/components/rpi_pfio/manifest.json +++ b/homeassistant/components/rpi_pfio/manifest.json @@ -3,5 +3,6 @@ "name": "PiFace Digital I/O (PFIO)", "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", "requirements": ["pifacecommon==4.2.2", "pifacedigitalio==3.0.5"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 993d0b313c0a2..305ad7d1f6283 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -2,20 +2,15 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Raspberry Pi Power Supply Checker component.""" - return True +PLATFORMS = ["binary_sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Raspberry Pi Power Supply Checker from a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "binary_sensor") - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, "binary_sensor") + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index b635972f43fe5..1a038d05fdc5f 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -5,8 +5,8 @@ from rpi_bad_power import new_under_voltage -from homeassistant import config_entries from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler from .const import DOMAIN @@ -29,12 +29,11 @@ def __init__(self) -> None: DOMAIN, "Raspberry Pi Power Supply Checker", _async_supported, - config_entries.CONN_CLASS_LOCAL_POLL, ) async def async_step_onboarding( self, data: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResult: """Handle a flow initialized by onboarding.""" has_devices = await self._discovery_function(self.hass) diff --git a/homeassistant/components/rpi_power/manifest.json b/homeassistant/components/rpi_power/manifest.json index 1b355711535fd..34e249ccfc33b 100644 --- a/homeassistant/components/rpi_power/manifest.json +++ b/homeassistant/components/rpi_power/manifest.json @@ -2,12 +2,8 @@ "domain": "rpi_power", "name": "Raspberry Pi Power Supply Checker", "documentation": "https://www.home-assistant.io/integrations/rpi_power", - "codeowners": [ - "@shenxn", - "@swetoast" - ], - "requirements": [ - "rpi-bad-power==0.1.0" - ], - "config_flow": true + "codeowners": ["@shenxn", "@swetoast"], + "requirements": ["rpi-bad-power==0.1.0"], + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json index 0a2cc42b63351..e880671072467 100644 --- a/homeassistant/components/rpi_rf/manifest.json +++ b/homeassistant/components/rpi_rf/manifest.json @@ -3,5 +3,6 @@ "name": "Raspberry Pi RF", "documentation": "https://www.home-assistant.io/integrations/rpi_rf", "requirements": ["rpi-rf==0.9.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "assumed_state" } diff --git a/homeassistant/components/rss_feed_template/manifest.json b/homeassistant/components/rss_feed_template/manifest.json index 1ae8fe58d7b0c..46b449b03dd17 100644 --- a/homeassistant/components/rss_feed_template/manifest.json +++ b/homeassistant/components/rss_feed_template/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/rss_feed_template", "dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "iot_class": "local_push" } diff --git a/homeassistant/components/rtorrent/manifest.json b/homeassistant/components/rtorrent/manifest.json index 137a77b12942d..549c2406b2fb3 100644 --- a/homeassistant/components/rtorrent/manifest.json +++ b/homeassistant/components/rtorrent/manifest.json @@ -2,5 +2,6 @@ "domain": "rtorrent", "name": "rTorrent", "documentation": "https://www.home-assistant.io/integrations/rtorrent", - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 2eb4f14313139..6ea3b736dcd40 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -1,5 +1,4 @@ """The Ruckus Unleashed integration.""" -import asyncio from pyruckus import Ruckus @@ -27,12 +26,6 @@ from .coordinator import RuckusUnleashedDataUpdateCoordinator -async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up the Ruckus Unleashed component.""" - hass.data[DOMAIN] = {} - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ruckus Unleashed from a config entry.""" try: @@ -64,29 +57,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=system_info[API_SYSTEM_OVERVIEW][API_VERSION], ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: coordinator, UNDO_UPDATE_LISTENERS: [], } - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: for listener in hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENERS]: listener() diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index 26be0e5bed97a..463c7b1d55073 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -46,7 +46,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Ruckus Unleashed.""" 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/ruckus_unleashed/coordinator.py b/homeassistant/components/ruckus_unleashed/coordinator.py index 1e145a19c7443..8b80eaae0da2f 100644 --- a/homeassistant/components/ruckus_unleashed/coordinator.py +++ b/homeassistant/components/ruckus_unleashed/coordinator.py @@ -22,7 +22,7 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator): """Coordinator to manage data from Ruckus Unleashed client.""" - def __init__(self, hass: HomeAssistant, *, ruckus: Ruckus): + def __init__(self, hass: HomeAssistant, *, ruckus: Ruckus) -> None: """Initialize global Ruckus Unleashed data updater.""" self.ruckus = ruckus diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index 90a848b663b3a..a776930b5ac71 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -4,10 +4,10 @@ from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -22,7 +22,7 @@ async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities ) -> None: """Set up device tracker for Ruckus Unleashed component.""" coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] @@ -118,7 +118,7 @@ def source_type(self) -> str: return SOURCE_TYPE_ROUTER @property - def device_info(self) -> dict | None: + def device_info(self) -> DeviceInfo | None: """Return the device information.""" if self.is_connected: return { diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index b8bc14a108adf..b8b2ef6e46a80 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -3,10 +3,7 @@ "name": "Ruckus Unleashed", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", - "requirements": [ - "pyruckus==0.12" - ], - "codeowners": [ - "@gabe565" - ] + "requirements": ["pyruckus==0.12"], + "codeowners": ["@gabe565"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json index cad7d736a9d26..011a2f61c1e69 100644 --- a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json +++ b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 2fd9f039d53b4..a12d149550b13 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -3,5 +3,6 @@ "name": "Russound RIO", "documentation": "https://www.home-assistant.io/integrations/russound_rio", "requirements": ["russound_rio==0.1.7"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/russound_rnet/manifest.json b/homeassistant/components/russound_rnet/manifest.json index 6379dd021f2a2..0e7928fb23b05 100644 --- a/homeassistant/components/russound_rnet/manifest.json +++ b/homeassistant/components/russound_rnet/manifest.json @@ -3,5 +3,6 @@ "name": "Russound RNET", "documentation": "https://www.home-assistant.io/integrations/russound_rnet", "requirements": ["russound==0.1.9"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 6fec5c008b3d0..25dfe6788009c 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pysabnzbd==1.1.0"], "dependencies": ["configurator"], "after_dependencies": ["discovery"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_polling" } diff --git a/homeassistant/components/sabnzbd/services.yaml b/homeassistant/components/sabnzbd/services.yaml index 654cb50fa1e23..38f68bfe5ddf5 100644 --- a/homeassistant/components/sabnzbd/services.yaml +++ b/homeassistant/components/sabnzbd/services.yaml @@ -1,11 +1,17 @@ pause: + name: Pause description: Pauses downloads. resume: + name: Resume description: Resumes downloads. set_speed: + name: Set speed description: Sets the download speed limit. fields: speed: + name: Speed description: Speed limit. If specified as a number with no units, will be interpreted as a percent. If units are provided (e.g., 500K) will be interpreted absolutely. example: 100 default: 100 + selector: + text: diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index fdd999ac68461..79067e47c731c 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,5 +3,6 @@ "name": "SAJ Solar Inverter", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": ["pysaj==0.0.16"], - "codeowners": ["@fredericvl"] + "codeowners": ["@fredericvl"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 8c17ff4794c83..31b666793afae 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -3,21 +3,33 @@ import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import ( + CONF_HOST, + CONF_METHOD, + CONF_NAME, + CONF_PORT, + CONF_TOKEN, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import CONF_ON_ACTION, DEFAULT_NAME, DOMAIN +from .bridge import SamsungTVBridge +from .const import CONF_ON_ACTION, DEFAULT_NAME, DOMAIN, LOGGER def ensure_unique_hosts(value): """Validate that all configs have a unique host.""" vol.Schema(vol.Unique("duplicate host entries found"))( - [socket.gethostbyname(entry[CONF_HOST]) for entry in value] + [entry[CONF_HOST] for entry in value] ) return value +PLATFORMS = [MP_DOMAIN] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -42,28 +54,87 @@ def ensure_unique_hosts(value): async def async_setup(hass, config): """Set up the Samsung TV integration.""" - if DOMAIN in config: - hass.data[DOMAIN] = {} - for entry_config in config[DOMAIN]: - ip_address = await hass.async_add_executor_job( - socket.gethostbyname, entry_config[CONF_HOST] - ) - hass.data[DOMAIN][ip_address] = { - CONF_ON_ACTION: entry_config.get(CONF_ON_ACTION) - } - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=entry_config - ) - ) + hass.data[DOMAIN] = {} + if DOMAIN not in config: + return True + for entry_config in config[DOMAIN]: + ip_address = await hass.async_add_executor_job( + socket.gethostbyname, entry_config[CONF_HOST] + ) + hass.data[DOMAIN][ip_address] = { + CONF_ON_ACTION: entry_config.get(CONF_ON_ACTION) + } + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_config, + ) + ) return True +@callback +def _async_get_device_bridge(data): + """Get device bridge.""" + return SamsungTVBridge.get_bridge( + data[CONF_METHOD], + data[CONF_HOST], + data[CONF_PORT], + data.get(CONF_TOKEN), + ) + + async def async_setup_entry(hass, entry): """Set up the Samsung TV platform.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) + + # Initialize bridge + data = entry.data.copy() + bridge = _async_get_device_bridge(data) + if bridge.port is None and bridge.default_port is not None: + # For backward compat, set default port for websocket tv + data[CONF_PORT] = bridge.default_port + hass.config_entries.async_update_entry(entry, data=data) + bridge = _async_get_device_bridge(data) + + def stop_bridge(event): + """Stop SamsungTV bridge connection.""" + bridge.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) ) + hass.data[DOMAIN][entry.entry_id] = bridge + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN][entry.entry_id].stop() + return unload_ok + + +async def async_migrate_entry(hass, config_entry): + """Migrate old entry.""" + version = config_entry.version + + LOGGER.debug("Migrating from version %s", version) + + # 1 -> 2: Unique ID format changed, so delete and re-import: + if version == 1: + dev_reg = await hass.helpers.device_registry.async_get_registry() + dev_reg.async_clear_config_entry(config_entry) + + en_reg = await hass.helpers.entity_registry.async_get_registry() + en_reg.async_clear_config_entry(config_entry) + + version = config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry) + LOGGER.debug("Migration to version %s successful", version) + return True diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index dc8eb862ff783..84b518a463351 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -1,10 +1,11 @@ """samsungctl and samsungtvws bridge classes.""" from abc import ABC, abstractmethod +import contextlib from samsungctl import Remote from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse from samsungtvws import SamsungTVWS -from samsungtvws.exceptions import ConnectionFailure +from samsungtvws.exceptions import ConnectionFailure, HttpApiError from websocket import WebSocketException from homeassistant.const import ( @@ -25,8 +26,11 @@ RESULT_CANNOT_CONNECT, RESULT_NOT_SUPPORTED, RESULT_SUCCESS, + TIMEOUT_REQUEST, + TIMEOUT_WEBSOCKET, VALUE_CONF_ID, VALUE_CONF_NAME, + WEBSOCKET_PORTS, ) @@ -58,9 +62,14 @@ def register_reauth_callback(self, func): def try_connect(self): """Try to connect to the TV.""" + @abstractmethod + def device_info(self): + """Try to gather infos of this TV.""" + def is_on(self): """Tells if the TV is on.""" - self.close_remote() + if self._remote: + self.close_remote() try: return self._get_remote() is not None @@ -104,7 +113,7 @@ def _send_key(self, key): """Send the key.""" @abstractmethod - def _get_remote(self): + def _get_remote(self, avoid_open: bool = False): """Get Remote object.""" def close_remote(self): @@ -149,7 +158,7 @@ def try_connect(self): CONF_METHOD: self.method, CONF_PORT: None, # We need this high timeout because waiting for auth popup is just an open socket - CONF_TIMEOUT: 31, + CONF_TIMEOUT: TIMEOUT_REQUEST, } try: LOGGER.debug("Try config: %s", config) @@ -162,11 +171,15 @@ def try_connect(self): except UnhandledResponse: LOGGER.debug("Working but unsupported config: %s", config) return RESULT_NOT_SUPPORTED - except OSError as err: + except (ConnectionClosed, OSError) as err: LOGGER.debug("Failing config: %s, error: %s", config, err) return RESULT_CANNOT_CONNECT - def _get_remote(self): + def device_info(self): + """Try to gather infos of this device.""" + return None + + def _get_remote(self, avoid_open: bool = False): """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. @@ -184,6 +197,11 @@ def _send_key(self, key): """Send the key using legacy protocol.""" self._get_remote().control(key) + def stop(self): + """Stop Bridge.""" + LOGGER.debug("Stopping SamsungRemote") + self.close_remote() + class SamsungTVWSBridge(SamsungTVBridge): """The Bridge for WebSocket TVs.""" @@ -196,14 +214,14 @@ def __init__(self, method, host, port, token=None): def try_connect(self): """Try to connect to the Websocket TV.""" - for self.port in (8001, 8002): + for self.port in WEBSOCKET_PORTS: config = { CONF_NAME: VALUE_CONF_NAME, CONF_HOST: self.host, CONF_METHOD: self.method, CONF_PORT: self.port, # We need this high timeout because waiting for auth popup is just an open socket - CONF_TIMEOUT: 31, + CONF_TIMEOUT: TIMEOUT_REQUEST, } result = None @@ -234,31 +252,46 @@ def try_connect(self): return RESULT_CANNOT_CONNECT + def device_info(self): + """Try to gather infos of this TV.""" + remote = self._get_remote(avoid_open=True) + if not remote: + return None + with contextlib.suppress(HttpApiError): + return remote.rest_device_info() + def _send_key(self, key): """Send the key using websocket protocol.""" if key == "KEY_POWEROFF": key = "KEY_POWER" self._get_remote().send_key(key) - def _get_remote(self): + def _get_remote(self, avoid_open: bool = False): """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. try: - LOGGER.debug("Create SamsungTVWS") + LOGGER.debug( + "Create SamsungTVWS for %s (%s)", VALUE_CONF_NAME, self.host + ) self._remote = SamsungTVWS( host=self.host, port=self.port, token=self.token, - timeout=8, + timeout=TIMEOUT_WEBSOCKET, name=VALUE_CONF_NAME, ) - self._remote.open() + if not avoid_open: + self._remote.open() # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket except ConnectionFailure: self._notify_callback() - raise - except WebSocketException: + except (WebSocketException, OSError): self._remote = None return self._remote + + def stop(self): + """Stop Bridge.""" + LOGGER.debug("Stopping SamsungTVWS") + self.close_remote() diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 209b89f541af6..b45f6c5670b72 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -4,7 +4,8 @@ import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER, @@ -13,60 +14,85 @@ ) 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 callback +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.typing import DiscoveryInfoType from .bridge import SamsungTVBridge from .const import ( + ATTR_PROPERTIES, CONF_MANUFACTURER, CONF_MODEL, + DEFAULT_MANUFACTURER, DOMAIN, + LEGACY_PORT, LOGGER, METHOD_LEGACY, METHOD_WEBSOCKET, RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, + RESULT_NOT_SUPPORTED, RESULT_SUCCESS, + RESULT_UNKNOWN_HOST, + WEBSOCKET_PORTS, ) DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) SUPPORTED_METHODS = [METHOD_LEGACY, METHOD_WEBSOCKET] -def _get_ip(host): - if host is None: - return None - return socket.gethostbyname(host) +def _get_device_info(host): + """Fetch device info by any websocket method.""" + for port in WEBSOCKET_PORTS: + bridge = SamsungTVBridge.get_bridge(METHOD_WEBSOCKET, host, port) + if info := bridge.device_info(): + return info + return None + + +async def async_get_device_info(hass, bridge, host): + """Fetch device info from bridge or websocket.""" + if bridge: + return await hass.async_add_executor_job(bridge.device_info) + + return await hass.async_add_executor_job(_get_device_info, host) + + +def _strip_uuid(udn): + return udn[5:] if udn.startswith("uuid:") else udn class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Samsung TV config flow.""" - VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + VERSION = 2 def __init__(self): """Initialize flow.""" + self._reauth_entry = None self._host = None - self._ip = None + self._mac = None + self._udn = None self._manufacturer = None self._model = None self._name = None self._title = None self._id = None self._bridge = None + self._device_info = None - def _get_entry(self): + def _get_entry_from_bridge(self): + """Get device entry.""" data = { CONF_HOST: self._host, - CONF_ID: self._id, - CONF_IP_ADDRESS: self._ip, - CONF_MANUFACTURER: self._manufacturer, + CONF_MAC: self._mac, + CONF_MANUFACTURER: self._manufacturer or DEFAULT_MANUFACTURER, CONF_METHOD: self._bridge.method, CONF_MODEL: self._model, CONF_NAME: self._name, @@ -79,98 +105,205 @@ def _get_entry(self): data=data, ) + async def _async_set_device_unique_id(self, raise_on_progress=True): + """Set device unique_id.""" + await self._async_get_and_check_device_info() + await self._async_set_unique_id_from_udn(raise_on_progress) + + async def _async_set_unique_id_from_udn(self, raise_on_progress=True): + """Set the unique id from the udn.""" + await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) + self._async_update_existing_host_entry(self._host) + updates = {CONF_HOST: self._host} + if self._mac: + updates[CONF_MAC] = self._mac + self._abort_if_unique_id_configured(updates=updates) + def _try_connect(self): """Try to connect and check auth.""" for method in SUPPORTED_METHODS: self._bridge = SamsungTVBridge.get_bridge(method, self._host) result = self._bridge.try_connect() + if result == RESULT_SUCCESS: + return if result != RESULT_CANNOT_CONNECT: - return result + raise data_entry_flow.AbortFlow(result) LOGGER.debug("No working config found") - return RESULT_CANNOT_CONNECT + raise data_entry_flow.AbortFlow(RESULT_CANNOT_CONNECT) + + async def _async_get_and_check_device_info(self): + """Try to get the device info.""" + info = await async_get_device_info(self.hass, self._bridge, self._host) + if not info: + raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + dev_info = info.get("device", {}) + device_type = dev_info.get("type") + if device_type != "Samsung SmartTV": + raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + self._model = dev_info.get("modelName") + name = dev_info.get("name") + self._name = name.replace("[TV] ", "") if name else device_type + self._title = f"{self._name} ({self._model})" + self._udn = _strip_uuid(dev_info.get("udn", info["id"])) + if dev_info.get("networkType") == "wireless" and dev_info.get("wifiMac"): + self._mac = format_mac(dev_info.get("wifiMac")) + self._device_info = info async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" - return await self.async_step_user(user_input) + # We need to import even if we cannot validate + # since the TV may be off at startup + await self._async_set_name_host_from_input(user_input) + self._async_abort_entries_match({CONF_HOST: self._host}) + if user_input.get(CONF_PORT) in WEBSOCKET_PORTS: + user_input[CONF_METHOD] = METHOD_WEBSOCKET + else: + user_input[CONF_METHOD] = METHOD_LEGACY + user_input[CONF_PORT] = LEGACY_PORT + user_input[CONF_MANUFACTURER] = DEFAULT_MANUFACTURER + return self.async_create_entry( + title=self._title, + data=user_input, + ) + + async def _async_set_name_host_from_input(self, user_input): + try: + self._host = await self.hass.async_add_executor_job( + socket.gethostbyname, user_input[CONF_HOST] + ) + except socket.gaierror as err: + raise data_entry_flow.AbortFlow(RESULT_UNKNOWN_HOST) from err + self._name = user_input[CONF_NAME] + self._title = self._name async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if user_input is not None: - ip_address = await self.hass.async_add_executor_job( - _get_ip, user_input[CONF_HOST] - ) - - await self.async_set_unique_id(ip_address) - self._abort_if_unique_id_configured() + await self._async_set_name_host_from_input(user_input) + await self.hass.async_add_executor_job(self._try_connect) + self._async_abort_entries_match({CONF_HOST: self._host}) + if self._bridge.method != METHOD_LEGACY: + # Legacy bridge does not provide device info + await self._async_set_device_unique_id(raise_on_progress=False) + return self._get_entry_from_bridge() - self._host = user_input.get(CONF_HOST) - self._ip = self.context[CONF_IP_ADDRESS] = ip_address - self._name = user_input.get(CONF_NAME) - self._title = self._name - - result = await self.hass.async_add_executor_job(self._try_connect) + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) - if result != RESULT_SUCCESS: - return self.async_abort(reason=result) - return self._get_entry() + @callback + def _async_update_existing_host_entry(self, host): + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_HOST] != host: + continue + entry_kw_args = {} + if self.unique_id and entry.unique_id is None: + entry_kw_args["unique_id"] = self.unique_id + if self._mac and not entry.data.get(CONF_MAC): + data_copy = dict(entry.data) + data_copy[CONF_MAC] = self._mac + entry_kw_args["data"] = data_copy + if entry_kw_args: + self.hass.config_entries.async_update_entry(entry, **entry_kw_args) + return entry + return None - return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + async def _async_start_discovery_for_host(self, host): + """Start discovery for a host.""" + if entry := self._async_update_existing_host_entry(host): + if entry.unique_id: + # Let the flow continue to fill the missing + # unique id as we may be able to obtain it + # in the next step + raise data_entry_flow.AbortFlow("already_configured") - async def async_step_ssdp(self, discovery_info): - """Handle a flow initialized by discovery.""" - host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname - ip_address = await self.hass.async_add_executor_job(_get_ip, host) + self.context[CONF_HOST] = host + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == host: + raise data_entry_flow.AbortFlow("already_in_progress") self._host = host - self._ip = self.context[CONF_IP_ADDRESS] = ip_address - self._manufacturer = discovery_info.get(ATTR_UPNP_MANUFACTURER) - self._model = discovery_info.get(ATTR_UPNP_MODEL_NAME) - self._name = f"Samsung {self._model}" - self._id = discovery_info.get(ATTR_UPNP_UDN) - self._title = self._model - - # probably access denied - if self._id is None: - return self.async_abort(reason=RESULT_AUTH_MISSING) - if self._id.startswith("uuid:"): - self._id = self._id[5:] - - await self.async_set_unique_id(ip_address) - self._abort_if_unique_id_configured( - { - CONF_ID: self._id, - CONF_MANUFACTURER: self._manufacturer, - CONF_MODEL: self._model, - } + + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType): + """Handle a flow initialized by ssdp discovery.""" + self._udn = _strip_uuid(discovery_info[ATTR_UPNP_UDN]) + await self._async_set_unique_id_from_udn() + await self._async_start_discovery_for_host( + urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname + ) + self._manufacturer = discovery_info[ATTR_UPNP_MANUFACTURER] + if not self._manufacturer or not self._manufacturer.lower().startswith( + "samsung" + ): + raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + self._name = self._title = self._model = discovery_info.get( + ATTR_UPNP_MODEL_NAME ) + self.context["title_placeholders"] = {"device": self._title} + return await self.async_step_confirm() + + async def async_step_dhcp(self, discovery_info: DiscoveryInfoType): + """Handle a flow initialized by dhcp discovery.""" + self._mac = discovery_info[MAC_ADDRESS] + await self._async_start_discovery_for_host(discovery_info[IP_ADDRESS]) + await self._async_set_device_unique_id() + self.context["title_placeholders"] = {"device": self._title} + return await self.async_step_confirm() - self.context["title_placeholders"] = {"model": self._model} + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Handle a flow initialized by zeroconf discovery.""" + self._mac = format_mac(discovery_info[ATTR_PROPERTIES]["deviceid"]) + await self._async_start_discovery_for_host(discovery_info[CONF_HOST]) + await self._async_set_device_unique_id() + self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() async def async_step_confirm(self, user_input=None): """Handle user-confirmation of discovered node.""" if user_input is not None: - result = await self.hass.async_add_executor_job(self._try_connect) - if result != RESULT_SUCCESS: - return self.async_abort(reason=result) - return self._get_entry() + await self.hass.async_add_executor_job(self._try_connect) + return self._get_entry_from_bridge() + self._set_confirm_only() return self.async_show_form( - step_id="confirm", description_placeholders={"model": self._model} + step_id="confirm", description_placeholders={"device": self._title} ) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, data): """Handle configuration by re-auth.""" - self._host = user_input[CONF_HOST] - self._id = user_input.get(CONF_ID) - self._ip = user_input[CONF_IP_ADDRESS] - self._manufacturer = user_input.get(CONF_MANUFACTURER) - self._model = user_input.get(CONF_MODEL) - self._name = user_input.get(CONF_NAME) - self._title = self._model or self._name + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + data = self._reauth_entry.data + if data.get(CONF_MODEL) and data.get(CONF_NAME): + self._title = f"{data[CONF_NAME]} ({data[CONF_MODEL]})" + else: + self._title = data.get(CONF_NAME) or data[CONF_HOST] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Confirm reauth.""" + errors = {} + if user_input is not None: + bridge = SamsungTVBridge.get_bridge( + self._reauth_entry.data[CONF_METHOD], self._reauth_entry.data[CONF_HOST] + ) + result = bridge.try_connect() + if result == RESULT_SUCCESS: + new_data = dict(self._reauth_entry.data) + new_data[CONF_TOKEN] = bridge.token + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=new_data + ) + return self.async_abort(reason="reauth_successful") + if result not in (RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT): + return self.async_abort(reason=result) - await self.async_set_unique_id(self._ip) - self.context["title_placeholders"] = {"model": self._title} + # On websocket we will get RESULT_CANNOT_CONNECT when auth is missing + errors = {"base": RESULT_AUTH_MISSING} - return await self.async_step_confirm() + self.context["title_placeholders"] = {"device": self._title} + return self.async_show_form( + step_id="reauth_confirm", + errors=errors, + description_placeholders={"device": self._title}, + ) diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index e043c74b34768..f2571372b1f6b 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -4,7 +4,10 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "samsungtv" +ATTR_PROPERTIES = "properties" + DEFAULT_NAME = "Samsung TV" +DEFAULT_MANUFACTURER = "Samsung" VALUE_CONF_NAME = "HomeAssistant" VALUE_CONF_ID = "ha.component.samsung" @@ -18,6 +21,13 @@ RESULT_SUCCESS = "success" RESULT_CANNOT_CONNECT = "cannot_connect" RESULT_NOT_SUPPORTED = "not_supported" +RESULT_UNKNOWN_HOST = "unknown" METHOD_LEGACY = "legacy" METHOD_WEBSOCKET = "websocket" + +TIMEOUT_REQUEST = 31 +TIMEOUT_WEBSOCKET = 5 + +LEGACY_PORT = 55000 +WEBSOCKET_PORTS = (8002, 8001) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 08dc4d0c04974..4ffe940f94640 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -4,15 +4,26 @@ "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "samsungctl[websocket]==0.7.1", - "samsungtvws==1.6.0" + "samsungtvws==1.6.0", + "wakeonlan==2.0.1" ], "ssdp": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], + "zeroconf": [ + {"type":"_airplay._tcp.local.","manufacturer":"samsung*"} + ], + "dhcp": [ + { + "hostname": "tizen*" + } + ], "codeowners": [ - "@escoand" + "@escoand", + "@chemelli74" ], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index a4b61369f99a7..5822bafcc55a2 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,6 +3,7 @@ from datetime import timedelta import voluptuous as vol +from wakeonlan import send_magic_packet from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerEntity from homeassistant.components.media_player.const import ( @@ -19,22 +20,12 @@ SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import ( - CONF_HOST, - CONF_ID, - CONF_IP_ADDRESS, - CONF_METHOD, - CONF_NAME, - CONF_PORT, - CONF_TOKEN, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.script import Script from homeassistant.util import dt as dt_util -from .bridge import SamsungTVBridge from .const import ( CONF_MANUFACTURER, CONF_MODEL, @@ -60,41 +51,19 @@ ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Samsung TV from a config entry.""" - ip_address = config_entry.data[CONF_IP_ADDRESS] + bridge = hass.data[DOMAIN][entry.entry_id] + + host = entry.data[CONF_HOST] on_script = None - if ( - DOMAIN in hass.data - and ip_address in hass.data[DOMAIN] - and CONF_ON_ACTION in hass.data[DOMAIN][ip_address] - and hass.data[DOMAIN][ip_address][CONF_ON_ACTION] - ): - turn_on_action = hass.data[DOMAIN][ip_address][CONF_ON_ACTION] + data = hass.data[DOMAIN] + if turn_on_action := data.get(host, {}).get(CONF_ON_ACTION): on_script = Script( - hass, turn_on_action, config_entry.data.get(CONF_NAME, DEFAULT_NAME), DOMAIN + hass, turn_on_action, entry.data.get(CONF_NAME, DEFAULT_NAME), DOMAIN ) - # Initialize bridge - data = config_entry.data.copy() - bridge = SamsungTVBridge.get_bridge( - data[CONF_METHOD], - data[CONF_HOST], - data[CONF_PORT], - data.get(CONF_TOKEN), - ) - if bridge.port is None and bridge.default_port is not None: - # For backward compat, set default port for websocket tv - data[CONF_PORT] = bridge.default_port - hass.config_entries.async_update_entry(config_entry, data=data) - bridge = SamsungTVBridge.get_bridge( - data[CONF_METHOD], - data[CONF_HOST], - data[CONF_PORT], - data.get(CONF_TOKEN), - ) - - async_add_entities([SamsungTVDevice(bridge, config_entry, on_script)]) + async_add_entities([SamsungTVDevice(bridge, entry, on_script)], True) class SamsungTVDevice(MediaPlayerEntity): @@ -103,11 +72,13 @@ class SamsungTVDevice(MediaPlayerEntity): def __init__(self, bridge, config_entry, on_script): """Initialize the Samsung device.""" self._config_entry = config_entry + self._host = config_entry.data[CONF_HOST] + self._mac = config_entry.data.get(CONF_MAC) self._manufacturer = config_entry.data.get(CONF_MANUFACTURER) self._model = config_entry.data.get(CONF_MODEL) self._name = config_entry.data.get(CONF_NAME) self._on_script = on_script - self._uuid = config_entry.data.get(CONF_ID) + self._uuid = config_entry.unique_id # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -117,21 +88,28 @@ def __init__(self, bridge, config_entry, on_script): # sending the next command to avoid turning the TV back ON). self._end_of_power_off = None self._bridge = bridge + self._auth_failed = False self._bridge.register_reauth_callback(self.access_denied) def access_denied(self): """Access denied callback.""" LOGGER.debug("Access denied in getting remote object") + self._auth_failed = True self.hass.add_job( self.hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_REAUTH}, + context={ + "source": SOURCE_REAUTH, + "entry_id": self._config_entry.entry_id, + }, data=self._config_entry.data, ) ) def update(self): """Update state of device.""" + if self._auth_failed: + return if self._power_off_in_progress(): self._state = STATE_OFF else: @@ -165,15 +143,25 @@ def state(self): """Return the state of the device.""" return self._state + @property + def available(self): + """Return the availability of the device.""" + if self._auth_failed: + return False + return self._state == STATE_ON or self._on_script or self._mac + @property def device_info(self): """Return device specific attributes.""" - return { + info = { "name": self.name, "identifiers": {(DOMAIN, self.unique_id)}, "manufacturer": self._manufacturer, "model": self._model, } + if self._mac: + info["connections"] = {(CONNECTION_NETWORK_MAC, self._mac)} + return info @property def is_volume_muted(self): @@ -188,7 +176,7 @@ def source_list(self): @property def supported_features(self): """Flag media player features that are supported.""" - if self._on_script: + if self._on_script or self._mac: return SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON return SUPPORT_SAMSUNGTV @@ -260,10 +248,19 @@ async def async_play_media(self, media_type, media_id, **kwargs): await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop) await self.hass.async_add_executor_job(self.send_key, "KEY_ENTER") + def _wake_on_lan(self): + """Wake the device via wake on lan.""" + send_magic_packet(self._mac, ip_address=self._host) + # If the ip address changed since we last saw the device + # broadcast a packet as well + send_magic_packet(self._mac) + async def async_turn_on(self): """Turn the media player on.""" if self._on_script: await self._on_script.async_run(context=self._context) + elif self._mac: + await self.hass.async_add_executor_job(self._wake_on_lan) def select_source(self, source): """Select input source.""" diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index b326f2ab54894..f92990e6163fe 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Samsung TV: {model}", + "flow_title": "{device}", "step": { "user": { "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", @@ -10,16 +10,24 @@ } }, "confirm": { - "title": "Samsung TV", - "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization. Manual configurations for this TV will be overwritten." - } + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." + }, + "reauth_confirm": { + "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." + } + }, + "error": { + "auth_missing": "[%key:component::samsungtv::config::abort::auth_missing%]" }, "abort": { "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.", + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant.", + "id_missing": "This Samsung device doesn't have a SerialNumber.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "not_supported": "This Samsung TV device is currently not supported." + "not_supported": "This Samsung device is currently not supported.", + "unknown": "[%key:common::config_flow::error::unknown%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json index bba625bc815e8..9ccff13ae3d51 100644 --- a/homeassistant/components/samsungtv/translations/ca.json +++ b/homeassistant/components/samsungtv/translations/ca.json @@ -5,9 +5,14 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant.", "cannot_connect": "Ha fallat la connexi\u00f3", - "not_supported": "Actualment aquest televisor Samsung no \u00e9s compatible." + "not_supported": "Actualment aquest televisor Samsung no \u00e9s compatible.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unknown": "Error inesperat" }, - "flow_title": "Televisor Samsung: {model}", + "error": { + "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 del televisor per autoritzar a Home Assistant." + }, + "flow_title": "{device}", "step": { "confirm": { "description": "Vols configurar el televisior Samsung {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3. Les configuracions manuals d'aquest televisor es sobreescriuran.", diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 5c84880380ea8..91576e76ee51b 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.", + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant.", "cannot_connect": "Failed to connect", - "not_supported": "This Samsung TV device is currently not supported." + "id_missing": "This Samsung device doesn't have a SerialNumber.", + "not_supported": "This Samsung device is currently not supported.", + "reauth_successful": "Re-authentication was successful", + "unknown": "Unexpected error" }, - "flow_title": "Samsung TV: {model}", + "error": { + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant." + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization. Manual configurations for this TV will be overwritten.", + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", "title": "Samsung TV" }, + "reauth_confirm": { + "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/samsungtv/translations/et.json b/homeassistant/components/samsungtv/translations/et.json index e1c9bc9dd46bc..0cc9bf8ebccf5 100644 --- a/homeassistant/components/samsungtv/translations/et.json +++ b/homeassistant/components/samsungtv/translations/et.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", - "auth_missing": "Home Assistant-il pole selle Samsungi teleriga \u00fchenduse loomiseks luba. Koduabilise autoriseerimiseks kontrolli oma teleri seadeid.", + "auth_missing": "Home Assistantil pole selle Samsungi teleriga \u00fchenduse loomiseks luba. Home Assistanti autoriseerimiseks kontrolli oma teleri seadeid.", "cannot_connect": "\u00dchendamine nurjus", - "not_supported": "Seda Samsungi tv-seadet praegu ei toetata." + "id_missing": "Sellel Samsungi seadmel puudub seerianumber.", + "not_supported": "Seda Samsungi seadet praegu ei toetata.", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unknown": "Tundmatu t\u00f5rge" }, - "flow_title": "Samsungi teler: {model}", + "error": { + "auth_missing": "Tuvastamine nurjus" + }, + "flow_title": "{devicel}", "step": { "confirm": { - "description": "Kas soovid seadistada Samsung TV-d {model} ? Kui seda pole kunagi enne Home Assistantiga \u00fchendatud, n\u00e4ed oma teleris h\u00fcpikakent, mis k\u00fcsib tuvastamist. Selle teleri k\u00e4sitsi seadistused kirjutatakse \u00fcle.", + "description": "Kas soovid seadistada {devicel} ? Kui seda pole kunagi enne Home Assistantiga \u00fchendatud, n\u00e4ed oma teleris h\u00fcpikakent, mis k\u00fcsib tuvastamist.", "title": "" }, + "reauth_confirm": { + "description": "P\u00e4rast esitamist n\u00f5ustu {device} h\u00fcpikaknaga, mis taotleb autoriseerimist 30 sekundi jooksul." + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/samsungtv/translations/it.json b/homeassistant/components/samsungtv/translations/it.json index 605685913ad32..ee1219305d7c6 100644 --- a/homeassistant/components/samsungtv/translations/it.json +++ b/homeassistant/components/samsungtv/translations/it.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo Samsung TV. Controlla le impostazioni del tuo TV per autorizzare Home Assistant.", + "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo televisore Samsung. Controlla le impostazioni di Gestione dispositivi esterni della tua TV per autorizzare Home Assistant.", "cannot_connect": "Impossibile connettersi", - "not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato." + "id_missing": "Questo dispositivo Samsung non ha un SerialNumber.", + "not_supported": "Questo dispositivo Samsung non \u00e8 attualmente supportato.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unknown": "Errore imprevisto" }, - "flow_title": "Samsung TV: {model}", + "error": { + "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo televisore Samsung. Controlla le impostazioni di Gestione dispositivi esterni della tua TV per autorizzare Home Assistant." + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "Vuoi configurare Samsung TV {model}? Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul tuo TV in cui \u00e8 richiesta l'autorizzazione. Le configurazioni manuali per questo TV verranno sovrascritte.", + "description": "Vuoi configurare {device}? Se non hai mai collegato Home Assistant, dovresti vedere un popup sulla tua TV che chiede l'autorizzazione.", "title": "Samsung TV" }, + "reauth_confirm": { + "description": "Dopo l'invio, accetta il popup su {device} richiedendo l'autorizzazione entro 30 secondi." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index 2a6cca466ea78..3f9e61c8a8cdb 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -7,7 +7,7 @@ "cannot_connect": "Kan geen verbinding maken", "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund." }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{model}", "step": { "confirm": { "description": "Wilt u Samsung TV {model} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven", diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 5d53619a96a39..90c64e02a3ec9 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -7,7 +7,7 @@ "cannot_connect": "Tilkobling mislyktes", "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." }, - "flow_title": "", + "flow_title": "{model}", "step": { "confirm": { "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index 07751797f852b..66d6ce3c4f3ec 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -7,7 +7,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "not_supported": "Ten telewizor Samsung nie jest obecnie obs\u0142ugiwany" }, - "flow_title": "Samsung TV: {model}", + "flow_title": "{model}", "step": { "confirm": { "description": "Czy chcesz skonfigurowa\u0107 telewizor Samsung {model}? Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.", diff --git a/homeassistant/components/samsungtv/translations/ru.json b/homeassistant/components/samsungtv/translations/ru.json index 983e7417d6b57..7d4c24aba45da 100644 --- a/homeassistant/components/samsungtv/translations/ru.json +++ b/homeassistant/components/samsungtv/translations/ru.json @@ -3,16 +3,25 @@ "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", - "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430.", + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Samsung TV. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 External Device Manager \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "not_supported": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + "id_missing": "\u0423 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Samsung \u043d\u0435\u0442 \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430.", + "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Samsung \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.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Samsung TV: {model}", + "error": { + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Samsung TV. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 External Device Manager \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430." + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung {model}? \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u044b.", + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {device}? \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u0440\u0430\u043d\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u043b\u0438 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" }, + "reauth_confirm": { + "description": "\u041f\u043e\u0441\u043b\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u044d\u0442\u043e\u0439 \u0444\u043e\u0440\u043c\u044b, \u043f\u0440\u0438\u043c\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u043e \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u043c \u043e\u043a\u043d\u0435 \u043d\u0430 {device} \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 30 \u0441\u0435\u043a\u0443\u043d\u0434." + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index 00b442399c1a3..950a460965b62 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -1,18 +1,27 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u8a2d\u5b9a\u4ee5\u76e1\u8208\u9a57\u8b49\u3002", + "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u5916\u90e8\u88dd\u7f6e\u7ba1\u7406\u54e1\u8a2d\u5b9a\u4ee5\u9032\u884c\u9a57\u8b49\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u96fb\u8996\u3002" + "id_missing": "\u4e09\u661f\u88dd\u7f6e\u4e26\u672a\u5305\u542b\u5e8f\u865f\u3002", + "not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u88dd\u7f6e\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "\u4e09\u661f\u96fb\u8996\uff1a{model}", + "error": { + "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u5916\u90e8\u88dd\u7f6e\u7ba1\u7406\u54e1\u8a2d\u5b9a\u4ee5\u9032\u884c\u9a57\u8b49\u3002" + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4e09\u661f\u96fb\u8996 {model}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002\u624b\u52d5\u8a2d\u5b9a\u5c07\u6703\u8986\u84cb\u539f\u8a2d\u5b9a\u3002", + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {device}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002", "title": "\u4e09\u661f\u96fb\u8996" }, + "reauth_confirm": { + "description": "\u50b3\u9001\u5f8c\u3001\u8acb\u65bc 30 \u79d2\u5167\u540c\u610f {device} \u4e0a\u9396\u986f\u793a\u7684\u5f48\u51fa\u8996\u7a97\u6388\u6b0a\u8a31\u53ef\u3002" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index 0a157cd4debaf..6aacb3015e1a6 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -3,5 +3,6 @@ "name": "Satel Integra", "documentation": "https://www.home-assistant.io/integrations/satel_integra", "requirements": ["satel_integra==0.3.4"], - "codeowners": [] + "codeowners": [], + "iot_class": "local_push" } diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index e11934c61c380..ced56fe59056f 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -32,13 +32,11 @@ def _hass_domain_validator(config): def _platform_validator(config): """Validate it is a valid platform.""" try: - platform = importlib.import_module( - ".{}".format(config[CONF_PLATFORM]), __name__ - ) + platform = importlib.import_module(f".{config[CONF_PLATFORM]}", __name__) except ImportError: try: platform = importlib.import_module( - "homeassistant.components.{}.scene".format(config[CONF_PLATFORM]) + f"homeassistant.components.{config[CONF_PLATFORM]}.scene" ) except ImportError: raise vol.Invalid("Invalid platform specified") from None diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 9d07460379c93..eb7d6bb2ed35c 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -4,6 +4,8 @@ turn_on: name: Activate description: Activate a scene. target: + entity: + domain: scene fields: transition: name: Transition diff --git a/homeassistant/components/schluter/manifest.json b/homeassistant/components/schluter/manifest.json index 46eb2449e3d67..86f0974b6d12f 100644 --- a/homeassistant/components/schluter/manifest.json +++ b/homeassistant/components/schluter/manifest.json @@ -3,5 +3,6 @@ "name": "Schluter", "documentation": "https://www.home-assistant.io/integrations/schluter", "requirements": ["py-schluter==0.1.7"], - "codeowners": ["@prairieapps"] + "codeowners": ["@prairieapps"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index daa5a269dcf6d..c57dd14e37ddb 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": ["beautifulsoup4==4.9.3"], "after_dependencies": ["rest"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff"], + "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 3bf070a7d792a..921ab29f7142a 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -2,7 +2,7 @@ import logging from bs4 import BeautifulSoup -from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import httpx import voluptuous as vol from homeassistant.components.rest.data import RestData @@ -72,9 +72,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = HTTPDigestAuth(username, password) + auth = httpx.DigestAuth(username, password) else: - auth = HTTPBasicAuth(username, password) + auth = (username, password) else: auth = None rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index c5c082cd509d0..2225ef3d9dd13 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -1,6 +1,5 @@ """The Screenlogic integration.""" import asyncio -from collections import defaultdict from datetime import timedelta import logging @@ -25,6 +24,7 @@ from .config_flow import async_discover_gateways_by_unique_id, name_for_mac from .const import DEFAULT_SCAN_INTERVAL, DISCOVERED_GATEWAYS, DOMAIN +from .services import async_load_screenlogic_services, async_unload_screenlogic_services _LOGGER = logging.getLogger(__name__) @@ -68,58 +68,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass, config_entry=entry, gateway=gateway, api_lock=api_lock ) - device_data = defaultdict(list) + async_load_screenlogic_services(hass) await coordinator.async_config_entry_first_refresh() - for circuit in coordinator.data["circuits"]: - device_data["switch"].append(circuit) - - for sensor in coordinator.data["sensors"]: - if sensor == "chem_alarm": - device_data["binary_sensor"].append(sensor) - else: - if coordinator.data["sensors"][sensor]["value"] != 0: - device_data["sensor"].append(sensor) - - for pump in coordinator.data["pumps"]: - if ( - coordinator.data["pumps"][pump]["data"] != 0 - and "currentWatts" in coordinator.data["pumps"][pump] - ): - device_data["pump"].append(pump) - - for body in coordinator.data["bodies"]: - device_data["body"].append(body) - hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, - "devices": device_data, "listener": entry.add_update_listener(async_update_listener), } - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id]["listener"]() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + async_unload_screenlogic_services(hass) + return unload_ok @@ -137,6 +108,7 @@ def __init__(self, hass, *, config_entry, gateway, api_lock): self.gateway = gateway self.api_lock = api_lock self.screenlogic_data = {} + interval = timedelta( seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ) @@ -160,10 +132,16 @@ async def _async_update_data(self): class ScreenlogicEntity(CoordinatorEntity): """Base class for all ScreenLogic entities.""" - def __init__(self, coordinator, data_key): + def __init__(self, coordinator, data_key, enabled=True): """Initialize of the entity.""" super().__init__(coordinator) self._data_key = data_key + self._enabled_default = enabled + + @property + def entity_registry_enabled_default(self): + """Entity enabled by default.""" + return self._enabled_default @property def mac(self): diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 0001223030a68..649e692540843 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic Binary Sensor.""" import logging -from screenlogicpy.const import DEVICE_TYPE, ON_OFF +from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, ON_OFF from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, @@ -19,16 +19,47 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + # Generic binary sensor + entities.append(ScreenLogicBinarySensor(coordinator, "chem_alarm")) + + if ( + coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] + & EQUIPMENT.FLAG_INTELLICHEM + ): + # IntelliChem alarm sensors + entities.extend( + [ + ScreenlogicChemistryAlarmBinarySensor(coordinator, chem_alarm) + for chem_alarm in coordinator.data[SL_DATA.KEY_CHEMISTRY][ + SL_DATA.KEY_ALERTS + ] + ] + ) + + # Intellichem notification sensors + entities.extend( + [ + ScreenlogicChemistryNotificationBinarySensor(coordinator, chem_notif) + for chem_notif in coordinator.data[SL_DATA.KEY_CHEMISTRY][ + SL_DATA.KEY_NOTIFICATIONS + ] + ] + ) + + if ( + coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] + & EQUIPMENT.FLAG_CHLORINATOR + ): + # SCG binary sensor + entities.append(ScreenlogicSCGBinarySensor(coordinator, "scg_status")) - for binary_sensor in data["devices"]["binary_sensor"]: - entities.append(ScreenLogicBinarySensor(coordinator, binary_sensor)) async_add_entities(entities) class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity): - """Representation of a ScreenLogic binary sensor entity.""" + """Representation of the basic ScreenLogic binary sensor entity.""" @property def name(self): @@ -38,8 +69,8 @@ def name(self): @property def device_class(self): """Return the device class.""" - device_class = self.sensor.get("device_type") - return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class) + device_type = self.sensor.get("device_type") + return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) @property def is_on(self) -> bool: @@ -49,4 +80,35 @@ def is_on(self) -> bool: @property def sensor(self): """Shortcut to access the sensor data.""" - return self.coordinator.data["sensors"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_SENSORS][self._data_key] + + +class ScreenlogicChemistryAlarmBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic IntelliChem alarm binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_ALERTS][ + self._data_key + ] + + +class ScreenlogicChemistryNotificationBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic IntelliChem notification binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][SL_DATA.KEY_NOTIFICATIONS][ + self._data_key + ] + + +class ScreenlogicSCGBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic SCG binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key] diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index b50879bfd499a..b83d2fe03caba 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic heating device.""" import logging -from screenlogicpy.const import EQUIPMENT, HEAT_MODE +from screenlogicpy.const import DATA as SL_DATA, EQUIPMENT, HEAT_MODE from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -37,11 +37,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] - for body in data["devices"]["body"]: + for body in coordinator.data[SL_DATA.KEY_BODIES]: entities.append(ScreenLogicClimate(coordinator, body)) + async_add_entities(entities) @@ -89,7 +89,7 @@ def target_temperature(self) -> float: @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self.config_data["is_celcius"]["value"] == 1: + if self.config_data["is_celsius"]["value"] == 1: return TEMP_CELSIUS return TEMP_FAHRENHEIT @@ -217,4 +217,4 @@ async def async_added_to_hass(self): @property def body(self): """Shortcut to access body data.""" - return self.coordinator.data["bodies"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_BODIES][self._data_key] diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index 4f38872211712..1fc01a2e85484 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -1,7 +1,7 @@ """Config flow for ScreenLogic.""" import logging -from screenlogicpy import ScreenLogicError, discover +from screenlogicpy import ScreenLogicError, discovery from screenlogicpy.const import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT from screenlogicpy.requests import login import voluptuous as vol @@ -27,7 +27,7 @@ async def async_discover_gateways_by_unique_id(hass): """Discover gateways and return a dict of them by unique id.""" discovered_gateways = {} try: - hosts = await hass.async_add_executor_job(discover) + hosts = await discovery.async_discover() _LOGGER.debug("Discovered hosts: %s", hosts) except ScreenLogicError as ex: _LOGGER.debug(ex) @@ -71,7 +71,6 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow to setup screen logic devices.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize ScreenLogic ConfigFlow.""" @@ -89,15 +88,15 @@ async def async_step_user(self, user_input=None): self.discovered_gateways = await async_discover_gateways_by_unique_id(self.hass) return await self.async_step_gateway_select() - async def async_step_dhcp(self, dhcp_discovery): + async def async_step_dhcp(self, discovery_info): """Handle dhcp discovery.""" - mac = _extract_mac_from_name(dhcp_discovery[HOSTNAME]) + mac = _extract_mac_from_name(discovery_info[HOSTNAME]) await self.async_set_unique_id(mac) self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: dhcp_discovery[IP_ADDRESS]} + updates={CONF_IP_ADDRESS: discovery_info[IP_ADDRESS]} ) - self.discovered_ip = dhcp_discovery[IP_ADDRESS] - self.context["title_placeholders"] = {"name": dhcp_discovery[HOSTNAME]} + self.discovered_ip = discovery_info[IP_ADDRESS] + self.context["title_placeholders"] = {"name": discovery_info[HOSTNAME]} return await self.async_step_gateway_entry() async def async_step_gateway_select(self, user_input=None): @@ -189,7 +188,7 @@ async def async_step_gateway_entry(self, user_input=None): class ScreenLogicOptionsFlowHandler(config_entries.OptionsFlow): """Handles the options for the ScreenLogic integration.""" - def __init__(self, config_entry: config_entries.ConfigEntry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Init the screen logic options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index d777dc6ddc503..49a57b8d46e8e 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -1,7 +1,16 @@ """Constants for the ScreenLogic integration.""" +from screenlogicpy.const import COLOR_MODE + +from homeassistant.util import slugify DOMAIN = "screenlogic" DEFAULT_SCAN_INTERVAL = 30 MIN_SCAN_INTERVAL = 10 +SERVICE_SET_COLOR_MODE = "set_color_mode" +ATTR_COLOR_MODE = "color_mode" +SUPPORTED_COLOR_MODES = { + slugify(name): num for num, name in COLOR_MODE.NAME_FOR_NUM.items() +} + DISCOVERED_GATEWAYS = "_discovered_gateways" diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index ab3d08a0702d4..abef9ec99edef 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,9 +3,13 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.2.1"], - "codeowners": [ - "@dieselrabbit" + "requirements": ["screenlogicpy==0.4.1"], + "codeowners": ["@dieselrabbit"], + "dhcp": [ + { + "hostname": "pentair: *", + "macaddress": "00C033*" + } ], - "dhcp": [{"hostname":"pentair: *","macaddress":"00C033*"}] -} \ No newline at end of file + "iot_class": "local_polling" +} diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 38bde2afd760e..2419ee46eed62 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -1,7 +1,12 @@ """Support for a ScreenLogic Sensor.""" import logging -from screenlogicpy.const import DEVICE_TYPE +from screenlogicpy.const import ( + CHEM_DOSING_STATE, + DATA as SL_DATA, + DEVICE_TYPE, + EQUIPMENT, +) from homeassistant.components.sensor import ( DEVICE_CLASS_POWER, @@ -14,7 +19,32 @@ _LOGGER = logging.getLogger(__name__) -PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") +SUPPORTED_CHEM_SENSORS = ( + "calcium_harness", + "current_orp", + "current_ph", + "cya", + "orp_dosing_state", + "orp_last_dose_time", + "orp_last_dose_volume", + "orp_setpoint", + "ph_dosing_state", + "ph_last_dose_time", + "ph_last_dose_volume", + "ph_probe_water_temp", + "ph_setpoint", + "salt_tds_ppm", + "total_alkalinity", +) + +SUPPORTED_SCG_SENSORS = ( + "scg_level1", + "scg_level2", + "scg_salt_ppm", + "scg_super_chlor_timer", +) + +SUPPORTED_PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { DEVICE_TYPE.TEMPERATURE: DEVICE_CLASS_TEMPERATURE, @@ -25,21 +55,52 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + equipment_flags = coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] + # Generic sensors - for sensor in data["devices"]["sensor"]: - entities.append(ScreenLogicSensor(coordinator, sensor)) + for sensor_name, sensor_data in coordinator.data[SL_DATA.KEY_SENSORS].items(): + if sensor_name in ("chem_alarm", "salt_ppm"): + continue + if sensor_data["value"] != 0: + entities.append(ScreenLogicSensor(coordinator, sensor_name)) + # Pump sensors - for pump in data["devices"]["pump"]: - for pump_key in PUMP_SENSORS: - entities.append(ScreenLogicPumpSensor(coordinator, pump, pump_key)) + for pump_num, pump_data in coordinator.data[SL_DATA.KEY_PUMPS].items(): + if pump_data["data"] != 0 and "currentWatts" in pump_data: + entities.extend( + ScreenLogicPumpSensor(coordinator, pump_num, pump_key) + for pump_key in pump_data + if pump_key in SUPPORTED_PUMP_SENSORS + ) + + # IntelliChem sensors + if equipment_flags & EQUIPMENT.FLAG_INTELLICHEM: + for chem_sensor_name in coordinator.data[SL_DATA.KEY_CHEMISTRY]: + enabled = True + if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR: + if chem_sensor_name in ("salt_tds_ppm"): + enabled = False + if chem_sensor_name in SUPPORTED_CHEM_SENSORS: + entities.append( + ScreenLogicChemistrySensor(coordinator, chem_sensor_name, enabled) + ) + + # SCG sensors + if equipment_flags & EQUIPMENT.FLAG_CHLORINATOR: + entities.extend( + [ + ScreenLogicSCGSensor(coordinator, scg_sensor) + for scg_sensor in coordinator.data[SL_DATA.KEY_SCG] + if scg_sensor in SUPPORTED_SCG_SENSORS + ] + ) async_add_entities(entities) class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): - """Representation of a ScreenLogic sensor entity.""" + """Representation of the basic ScreenLogic sensor entity.""" @property def name(self): @@ -54,8 +115,8 @@ def unit_of_measurement(self): @property def device_class(self): """Device class of the sensor.""" - device_class = self.sensor.get("device_type") - return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class) + device_type = self.sensor.get("device_type") + return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) @property def state(self): @@ -66,40 +127,50 @@ def state(self): @property def sensor(self): """Shortcut to access the sensor data.""" - return self.coordinator.data["sensors"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_SENSORS][self._data_key] -class ScreenLogicPumpSensor(ScreenlogicEntity, SensorEntity): +class ScreenLogicPumpSensor(ScreenLogicSensor): """Representation of a ScreenLogic pump sensor entity.""" - def __init__(self, coordinator, pump, key): + def __init__(self, coordinator, pump, key, enabled=True): """Initialize of the pump sensor.""" - super().__init__(coordinator, f"{key}_{pump}") + super().__init__(coordinator, f"{key}_{pump}", enabled) self._pump_id = pump self._key = key @property - def name(self): - """Return the pump sensor name.""" - return f"{self.gateway_name} {self.pump_sensor['name']}" + def sensor(self): + """Shortcut to access the pump sensor data.""" + return self.coordinator.data[SL_DATA.KEY_PUMPS][self._pump_id][self._key] - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self.pump_sensor.get("unit") - @property - def device_class(self): - """Return the device class.""" - device_class = self.pump_sensor.get("device_type") - return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_class) +class ScreenLogicChemistrySensor(ScreenLogicSensor): + """Representation of a ScreenLogic IntelliChem sensor entity.""" + + def __init__(self, coordinator, key, enabled=True): + """Initialize of the pump sensor.""" + super().__init__(coordinator, f"chem_{key}", enabled) + self._key = key @property def state(self): - """State of the pump sensor.""" - return self.pump_sensor["value"] + """State of the sensor.""" + value = self.sensor["value"] + if "dosing_state" in self._key: + return CHEM_DOSING_STATE.NAME_FOR_NUM[value] + return value @property - def pump_sensor(self): + def sensor(self): + """Shortcut to access the pump sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CHEMISTRY][self._key] + + +class ScreenLogicSCGSensor(ScreenLogicSensor): + """Representation of ScreenLogic SCG sensor entity.""" + + @property + def sensor(self): """Shortcut to access the pump sensor data.""" - return self.coordinator.data["pumps"][self._pump_id][self._key] + return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key] diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py new file mode 100644 index 0000000000000..31e35788f44f7 --- /dev/null +++ b/homeassistant/components/screenlogic/services.py @@ -0,0 +1,88 @@ +"""Services for ScreenLogic integration.""" + +import logging + +from screenlogicpy import ScreenLogicError +import voluptuous as vol + +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_extract_config_entry_ids + +from .const import ( + ATTR_COLOR_MODE, + DOMAIN, + SERVICE_SET_COLOR_MODE, + SUPPORTED_COLOR_MODES, +) + +_LOGGER = logging.getLogger(__name__) + +SET_COLOR_MODE_SCHEMA = cv.make_entity_service_schema( + { + vol.Required(ATTR_COLOR_MODE): vol.In(SUPPORTED_COLOR_MODES), + }, +) + + +@callback +def async_load_screenlogic_services(hass: HomeAssistant): + """Set up services for the ScreenLogic integration.""" + if hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE): + # Integration-level services have already been added. Return. + return + + async def extract_screenlogic_config_entry_ids(service_call: ServiceCall): + return [ + entry_id + for entry_id in await async_extract_config_entry_ids(hass, service_call) + if hass.config_entries.async_get_entry(entry_id).domain == DOMAIN + ] + + async def async_set_color_mode(service_call: ServiceCall): + if not ( + screenlogic_entry_ids := await extract_screenlogic_config_entry_ids( + service_call + ) + ): + raise HomeAssistantError( + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'. Config entry for target not found" + ) + color_num = SUPPORTED_COLOR_MODES[service_call.data[ATTR_COLOR_MODE]] + for entry_id in screenlogic_entry_ids: + coordinator = hass.data[DOMAIN][entry_id]["coordinator"] + _LOGGER.debug( + "Service %s called on %s with mode %s", + SERVICE_SET_COLOR_MODE, + coordinator.gateway.name, + color_num, + ) + try: + async with coordinator.api_lock: + if not await hass.async_add_executor_job( + coordinator.gateway.set_color_lights, color_num + ): + raise HomeAssistantError( + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'" + ) + except ScreenLogicError as error: + raise HomeAssistantError(error) from error + + hass.services.async_register( + DOMAIN, SERVICE_SET_COLOR_MODE, async_set_color_mode, SET_COLOR_MODE_SCHEMA + ) + + +@callback +def async_unload_screenlogic_services(hass: HomeAssistant): + """Unload services for the ScreenLogic integration.""" + if hass.data[DOMAIN]: + # There is still another config entry for this domain, don't remove services. + return + + if not hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE): + return + + _LOGGER.info("Unloading ScreenLogic Services") + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SET_COLOR_MODE) diff --git a/homeassistant/components/screenlogic/services.yaml b/homeassistant/components/screenlogic/services.yaml new file mode 100644 index 0000000000000..7b54b9541d21a --- /dev/null +++ b/homeassistant/components/screenlogic/services.yaml @@ -0,0 +1,38 @@ +# ScreenLogic Services +set_color_mode: + name: Set Color Mode + description: Sets the color mode for all color-capable lights attached to this ScreenLogic gateway. + target: + device: + integration: screenlogic + fields: + color_mode: + name: Color Mode + description: The ScreenLogic color mode to set + required: true + example: "romance" + selector: + select: + options: + - all_off + - all_on + - color_set + - color_sync + - color_swim + - party + - romance + - caribbean + - american + - sunset + - royal + - save + - recall + - blue + - green + - red + - white + - magenta + - thumper + - next_mode + - reset + - hold diff --git a/homeassistant/components/screenlogic/strings.json b/homeassistant/components/screenlogic/strings.json index 155eeb3043e1a..8a9ec196c91a1 100644 --- a/homeassistant/components/screenlogic/strings.json +++ b/homeassistant/components/screenlogic/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, @@ -36,4 +36,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py index e0077b1d62dc6..ff73afebb575d 100644 --- a/homeassistant/components/screenlogic/switch.py +++ b/homeassistant/components/screenlogic/switch.py @@ -1,7 +1,7 @@ """Support for a ScreenLogic 'circuit' switch.""" import logging -from screenlogicpy.const import ON_OFF +from screenlogicpy.const import DATA as SL_DATA, GENERIC_CIRCUIT_NAMES, ON_OFF from homeassistant.components.switch import SwitchEntity @@ -14,11 +14,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = data["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + for circuit_num, circuit in coordinator.data[SL_DATA.KEY_CIRCUITS].items(): + enabled = circuit["name"] not in GENERIC_CIRCUIT_NAMES + entities.append(ScreenLogicSwitch(coordinator, circuit_num, enabled)) - for switch in data["devices"]["switch"]: - entities.append(ScreenLogicSwitch(coordinator, switch)) async_add_entities(entities) @@ -60,4 +61,4 @@ async def _async_set_circuit(self, circuit_value) -> None: @property def circuit(self): """Shortcut to access the circuit.""" - return self.coordinator.data["circuits"][self._data_key] + return self.coordinator.data[SL_DATA.KEY_CIRCUITS][self._data_key] diff --git a/homeassistant/components/screenlogic/translations/ca.json b/homeassistant/components/screenlogic/translations/ca.json index 68bfad1ff9457..a264ecec51ad9 100644 --- a/homeassistant/components/screenlogic/translations/ca.json +++ b/homeassistant/components/screenlogic/translations/ca.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/de.json b/homeassistant/components/screenlogic/translations/de.json index 6afe42e37ee1b..84f425be218ff 100644 --- a/homeassistant/components/screenlogic/translations/de.json +++ b/homeassistant/components/screenlogic/translations/de.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/en.json b/homeassistant/components/screenlogic/translations/en.json index 2572fdf38fa19..5d1eabed5d39d 100644 --- a/homeassistant/components/screenlogic/translations/en.json +++ b/homeassistant/components/screenlogic/translations/en.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/es.json b/homeassistant/components/screenlogic/translations/es.json index 8e9513d4f7530..c890d3bf10cac 100644 --- a/homeassistant/components/screenlogic/translations/es.json +++ b/homeassistant/components/screenlogic/translations/es.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, "flow_title": "ScreenLogic {name}", "step": { "gateway_entry": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "port": "Puerto" + }, "description": "Introduzca la informaci\u00f3n de su ScreenLogic Gateway.", "title": "ScreenLogic" }, diff --git a/homeassistant/components/screenlogic/translations/et.json b/homeassistant/components/screenlogic/translations/et.json index cf2cf19418fcb..3e0047e5be789 100644 --- a/homeassistant/components/screenlogic/translations/et.json +++ b/homeassistant/components/screenlogic/translations/et.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/fr.json b/homeassistant/components/screenlogic/translations/fr.json index 968045e059797..efd9740ac3127 100644 --- a/homeassistant/components/screenlogic/translations/fr.json +++ b/homeassistant/components/screenlogic/translations/fr.json @@ -18,7 +18,7 @@ }, "gateway_select": { "data": { - "selected_gateway": "passerelle" + "selected_gateway": "Passerelle" }, "description": "Les passerelles ScreenLogic suivantes ont \u00e9t\u00e9 d\u00e9couvertes. S\u2019il vous pla\u00eet s\u00e9lectionner un \u00e0 configurer, ou choisissez de configurer manuellement une passerelle ScreenLogic.", "title": "ScreenLogic" diff --git a/homeassistant/components/screenlogic/translations/id.json b/homeassistant/components/screenlogic/translations/id.json new file mode 100644 index 0000000000000..5af1cfbe5ef98 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/id.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "ScreenLogic {name}", + "step": { + "gateway_entry": { + "data": { + "ip_address": "Alamat IP", + "port": "Port" + }, + "description": "Masukkan informasi ScreenLogic Gateway Anda.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "Gateway ScreenLogic berikut ini ditemukan. Pilih satu untuk dikonfigurasi, atau pilih untuk mengonfigurasi gateway ScreenLogic secara manual.", + "title": "ScreenLogic" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval pemindaian dalam detik" + }, + "description": "Tentukan pengaturan untuk {gateway_name}", + "title": "ScreenLogic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/it.json b/homeassistant/components/screenlogic/translations/it.json index 8fc3c346c0f42..778f69fc530b0 100644 --- a/homeassistant/components/screenlogic/translations/it.json +++ b/homeassistant/components/screenlogic/translations/it.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/nl.json b/homeassistant/components/screenlogic/translations/nl.json index 7c752e0ae4d75..ff73cc5920c74 100644 --- a/homeassistant/components/screenlogic/translations/nl.json +++ b/homeassistant/components/screenlogic/translations/nl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/no.json b/homeassistant/components/screenlogic/translations/no.json index 0ca4827514a0f..e108572c6488a 100644 --- a/homeassistant/components/screenlogic/translations/no.json +++ b/homeassistant/components/screenlogic/translations/no.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/pl.json b/homeassistant/components/screenlogic/translations/pl.json index 64e2573ddb0a9..545e8451a4721 100644 --- a/homeassistant/components/screenlogic/translations/pl.json +++ b/homeassistant/components/screenlogic/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/ru.json b/homeassistant/components/screenlogic/translations/ru.json index a657b7360c790..957bf155c0b22 100644 --- a/homeassistant/components/screenlogic/translations/ru.json +++ b/homeassistant/components/screenlogic/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": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/screenlogic/translations/sv.json b/homeassistant/components/screenlogic/translations/sv.json new file mode 100644 index 0000000000000..7be3515deb0d7 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Kunde inte ansluta" + }, + "step": { + "gateway_entry": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/zh-Hant.json b/homeassistant/components/screenlogic/translations/zh-Hant.json index 40ca94fd779a3..575392f15c7a2 100644 --- a/homeassistant/components/screenlogic/translations/zh-Hant.json +++ b/homeassistant/components/screenlogic/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 8f2e0743f77d2..41d5e697cf19f 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -3,20 +3,21 @@ import asyncio import logging +from typing import Any, Dict, cast import voluptuous as vol +from voluptuous.humanize import humanize_error +from homeassistant.components.blueprint import BlueprintInputs from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ATTR_NAME, CONF_ALIAS, - CONF_DEFAULT, CONF_DESCRIPTION, CONF_ICON, CONF_MODE, CONF_NAME, - CONF_SELECTOR, CONF_SEQUENCE, CONF_VARIABLES, SERVICE_RELOAD, @@ -26,6 +27,7 @@ STATE_ON, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import extract_domain_configs import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity @@ -35,60 +37,26 @@ ATTR_MAX, CONF_MAX, CONF_MAX_EXCEEDED, - SCRIPT_MODE_SINGLE, Script, - make_script_schema, ) -from homeassistant.helpers.selector import validate_selector from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.trace import trace_get, trace_path from homeassistant.loader import bind_hass -from .trace import trace_script - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "script" - -ATTR_LAST_ACTION = "last_action" -ATTR_LAST_TRIGGERED = "last_triggered" -ATTR_VARIABLES = "variables" - -CONF_ADVANCED = "advanced" -CONF_EXAMPLE = "example" -CONF_FIELDS = "fields" -CONF_REQUIRED = "required" - -ENTITY_ID_FORMAT = DOMAIN + ".{}" - -EVENT_SCRIPT_STARTED = "script_started" - - -SCRIPT_ENTRY_SCHEMA = make_script_schema( - { - vol.Optional(CONF_ALIAS): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_DESCRIPTION, default=""): cv.string, - vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, - vol.Optional(CONF_FIELDS, default={}): { - cv.string: { - vol.Optional(CONF_ADVANCED, default=False): cv.boolean, - vol.Optional(CONF_DEFAULT): cv.match_all, - vol.Optional(CONF_DESCRIPTION): cv.string, - vol.Optional(CONF_EXAMPLE): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_REQUIRED, default=False): cv.boolean, - vol.Optional(CONF_SELECTOR): validate_selector, - } - }, - }, - SCRIPT_MODE_SINGLE, -) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.schema_with_slug_keys(SCRIPT_ENTRY_SCHEMA)}, extra=vol.ALLOW_EXTRA +from .config import ScriptConfig, async_validate_config_item +from .const import ( + ATTR_LAST_ACTION, + ATTR_LAST_TRIGGERED, + ATTR_VARIABLES, + CONF_FIELDS, + CONF_TRACE, + DOMAIN, + ENTITY_ID_FORMAT, + EVENT_SCRIPT_STARTED, + LOGGER, ) +from .helpers import async_get_blueprints +from .trace import trace_script SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( @@ -198,9 +166,13 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup(hass, config): """Load the scripts from the configuration.""" - hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) + + # To register scripts as valid domain for Blueprint + async_get_blueprints(hass) - await _async_process_config(hass, config, component) + if not await _async_process_config(hass, config, component): + await async_get_blueprints(hass).async_populate() async def reload_service(service): """Call a service to reload scripts.""" @@ -254,8 +226,50 @@ async def toggle_service(service): return True -async def _async_process_config(hass, config, component): - """Process script configuration.""" +async def _async_process_config(hass, config, component) -> bool: + """Process script configuration. + + Return true, if Blueprints were used. + """ + entities = [] + blueprints_used = False + + for config_key in extract_domain_configs(config, DOMAIN): + conf: dict[str, dict[str, Any] | BlueprintInputs] = config[config_key] + + for object_id, config_block in conf.items(): + raw_blueprint_inputs = None + raw_config = None + + if isinstance(config_block, BlueprintInputs): + blueprints_used = True + blueprint_inputs = config_block + raw_blueprint_inputs = blueprint_inputs.config_with_inputs + + try: + raw_config = blueprint_inputs.async_substitute() + config_block = cast( + Dict[str, Any], + await async_validate_config_item(hass, raw_config), + ) + except vol.Invalid as err: + LOGGER.error( + "Blueprint %s generated invalid script with input %s: %s", + blueprint_inputs.blueprint.name, + blueprint_inputs.inputs, + humanize_error(config_block, err), + ) + continue + else: + raw_config = cast(ScriptConfig, config_block).raw_config + + entities.append( + ScriptEntity( + hass, object_id, config_block, raw_config, raw_blueprint_inputs + ) + ) + + await component.async_add_entities(entities) async def service_handler(service): """Execute a service call to script.