diff --git a/.coveragerc b/.coveragerc index 851922e4f3a99d..c41d5afe169c6d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -219,6 +219,7 @@ omit = homeassistant/components/flic/binary_sensor.py homeassistant/components/flock/notify.py homeassistant/components/flume/* + homeassistant/components/flunearyou/__init__.py homeassistant/components/flunearyou/sensor.py homeassistant/components/flux_led/light.py homeassistant/components/folder/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 4d4c7d3d900736..5cbf0d411a0f23 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -409,6 +409,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 +homeassistant/components/vera/* @vangorra homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index bee7364489007b..e733bbd8abbe01 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -48,7 +48,7 @@ async def async_added_to_hass(self): ) signal = f"abode_camera_capture_{self.entity_id}" - async_dispatcher_connect(self.hass, signal, self.capture) + self.async_on_remove(async_dispatcher_connect(self.hass, signal, self.capture)) def capture(self): """Request a new image capture.""" diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index b57f3fbe1430b9..bbd90442cd9e91 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -53,7 +53,7 @@ async def async_added_to_hass(self): await super().async_added_to_hass() signal = f"abode_trigger_automation_{self.entity_id}" - async_dispatcher_connect(self.hass, signal, self.trigger) + self.async_on_remove(async_dispatcher_connect(self.hass, signal, self.trigger)) def turn_on(self, **kwargs): """Enable the automation.""" diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index eb0236cf3be798..9b8a5637de20d2 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -145,14 +145,16 @@ def icon(self): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self._force_update + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self._force_update + ) ) async def _force_update(self): """Force update of data.""" await self.async_update(no_throttle=True) - await self.async_update_ha_state() + self.async_write_ha_state() @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self, **kwargs): diff --git a/homeassistant/components/airvisual/.translations/ko.json b/homeassistant/components/airvisual/.translations/ko.json index 8f1155aa5f915f..bb01114a5e3e4f 100644 --- a/homeassistant/components/airvisual/.translations/ko.json +++ b/homeassistant/components/airvisual/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc774 API \ud0a4\ub294 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + "already_configured": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_api_key": "\uc798\ubabb\ub41c API \ud0a4" diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 57004191064b4e..5625204c762fdd 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -96,8 +96,10 @@ def __init__(self, auto_bypass, code_arm_required): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_PANEL_MESSAGE, self._message_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_PANEL_MESSAGE, self._message_callback + ) ) def _message_callback(self, message): diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 13a7913e190817..b34c90bc35add4 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -79,20 +79,28 @@ def __init__( async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_ZONE_FAULT, self._fault_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_ZONE_FAULT, self._fault_callback + ) ) - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_ZONE_RESTORE, self._restore_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_ZONE_RESTORE, self._restore_callback + ) ) - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_RFX_MESSAGE, self._rfx_message_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_RFX_MESSAGE, self._rfx_message_callback + ) ) - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_REL_MESSAGE, self._rel_message_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_REL_MESSAGE, self._rel_message_callback + ) ) @property diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 196e8d704e1586..96e5feb532d313 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -29,8 +29,10 @@ def __init__(self, hass): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_PANEL_MESSAGE, self._message_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_PANEL_MESSAGE, self._message_callback + ) ) def _message_callback(self, message): diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index 12268d48bb7121..1193f90ff8e608 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -1,9 +1,6 @@ alarm_keypress: description: Send custom keypresses to the alarm. fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' keypress: description: 'String to send to the alarm panel.' example: '*71' @@ -11,9 +8,6 @@ alarm_keypress: alarm_toggle_chime: description: Send the alarm the toggle chime command. fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' code: description: A required code to toggle the alarm control panel chime with. example: 1234 diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 1f8176968d80db..aa8d19dc40fe72 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -1,5 +1,4 @@ """Support for repeating alerts when conditions are met.""" -import asyncio from datetime import timedelta import logging @@ -144,9 +143,8 @@ async def async_handle_alert_service(service_call): DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA ) - tasks = [alert.async_update_ha_state() for alert in entities] - if tasks: - await asyncio.wait(tasks) + for alert in entities: + alert.async_write_ha_state() return True @@ -318,13 +316,13 @@ async def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" _LOGGER.debug("Reset Alert: %s", self._name) self._ack = False - await self.async_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Async Acknowledge alert.""" _LOGGER.debug("Acknowledged Alert: %s", self._name) self._ack = True - await self.async_update_ha_state() + self.async_write_ha_state() async def async_toggle(self, **kwargs): """Async toggle alert.""" diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index 1f9df527c28203..333da7dceea8b1 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -309,7 +309,9 @@ def async_ipcam_update(host): return self.async_schedule_update_ha_state(True) - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) + self.async_on_remove( + async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) + ) @property def should_poll(self): diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index f4efd0de355265..b7df82961c7962 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -22,6 +22,10 @@ ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) _LOGGER = logging.getLogger(__name__) @@ -60,7 +64,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def async_anthemav_update_callback(message): """Receive notification from transport that new data exists.""" _LOGGER.debug("Received update callback from AVR: %s", message) - hass.async_create_task(device.async_update_ha_state()) + async_dispatcher_send(hass, DOMAIN) avr = await anthemav.Connection.create( host=host, port=port, update_callback=async_anthemav_update_callback @@ -87,6 +91,12 @@ def __init__(self, avr, name): def _lookup(self, propname, dval=None): return getattr(self.avr.protocol, propname, dval) + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) + ) + @property def supported_features(self): """Flag media player features that are supported.""" diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 002b032fa92b96..ce2ecb89d8f2a7 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -99,8 +99,10 @@ def icon(self): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_update_callback + ) ) @callback diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index e54fcff139d507..c00510b563a2b4 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -64,7 +64,6 @@ def __init__(self, processor, switch_type): "aux_6": States.AUX_6, "aux_7": States.AUX_7, }[switch_type] - self._unsub_disp = None @property def name(self): @@ -101,11 +100,8 @@ def turn_off(self, **kwargs): async def async_added_to_hass(self): """Register callbacks.""" - self._unsub_disp = self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_write_ha_state + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_write_ha_state + ) ) - - async def async_will_remove_from_hass(self): - """When entity will be removed from hass.""" - self._unsub_disp() - self._unsub_disp = None diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index a49802ea96fae1..92e07a0547e93b 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -142,14 +142,22 @@ def _stopped(host): if host == self._state.client.host: self.async_schedule_update_ha_state(force_refresh=True) - self.hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_CLIENT_DATA, _data) + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CLIENT_DATA, _data + ) + ) - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_CLIENT_STARTED, _started + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CLIENT_STARTED, _started + ) ) - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_CLIENT_STOPPED, _stopped + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_CLIENT_STOPPED, _stopped + ) ) async def async_update(self): diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index 49a1bced577946..5e5597f50dab5f 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -84,7 +84,11 @@ def icon(self): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback + ) + ) @callback def _update_callback(self): diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index e2bb85c9f8404e..6f7e3796309d21 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -61,7 +61,6 @@ def __init__(self, hass, camera, device_info): self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._last_refresh = None self.attrs = {} - self._unsub_disp = None def camera_image(self): """Return a still image response from the camera.""" @@ -69,15 +68,12 @@ def camera_image(self): async def async_added_to_hass(self): """Register callbacks.""" - self._unsub_disp = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ARLO, self.async_write_ha_state + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self.async_write_ha_state + ) ) - async def async_will_remove_from_hass(self): - """When entity will be removed from hass.""" - self._unsub_disp() - self._unsub_disp = None - async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" video = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index 03e4437b257e64..10f8d6701089bf 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -92,7 +92,11 @@ def name(self): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self._update_callback + ) + ) @callback def _update_callback(self): diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index a0eee38c3f8afa..446fe898aaaf5b 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -14,6 +14,7 @@ ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.event import async_call_later _LOGGER = logging.getLogger(__name__) @@ -31,6 +32,9 @@ DEFAULT_INTERFACE = "eth0" DEFAULT_DNSMASQ = "/var/lib/misc" +FIRST_RETRY_TIME = 60 +MAX_RETRY_TIME = 900 + SECRET_GROUP = "Password or SSH Key" SENSOR_TYPES = ["upload_speed", "download_speed", "download", "upload"] @@ -59,7 +63,7 @@ ) -async def async_setup(hass, config): +async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME): """Set up the asuswrt component.""" conf = config[DOMAIN] @@ -77,9 +81,29 @@ async def async_setup(hass, config): dnsmasq=conf[CONF_DNSMASQ], ) - await api.connection.async_connect() + try: + await api.connection.async_connect() + except OSError as ex: + _LOGGER.warning( + "Error [%s] connecting %s to %s. Will retry in %s seconds...", + str(ex), + DOMAIN, + conf[CONF_HOST], + retry_delay, + ) + + async def retry_setup(now): + """Retry setup if a error happens on asuswrt API.""" + await async_setup( + hass, config, retry_delay=min(2 * retry_delay, MAX_RETRY_TIME) + ) + + async_call_later(hass, retry_delay, retry_setup) + + return True + if not api.is_connected: - _LOGGER.error("Unable to setup component") + _LOGGER.error("Error connecting %s to %s.", DOMAIN, conf[CONF_HOST]) return False hass.data[DATA_ASUSWRT] = api diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c19a0033f86336..76fe619cc1cc64 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -389,7 +389,7 @@ async def async_trigger(self, variables, skip_condition=False, context=None): pass self._last_triggered = utcnow() - await self.async_update_ha_state() + self.async_write_ha_state() async def async_will_remove_from_hass(self): """Remove listeners when removing automation from Home Assistant.""" diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index e61c4cea6b03d2..2e848168b49293 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -13,21 +13,15 @@ class AxisEntityBase(Entity): def __init__(self, device): """Initialize the Axis event.""" self.device = device - self.unsub_dispatcher = [] async def async_added_to_hass(self): """Subscribe device events.""" - self.unsub_dispatcher.append( + self.async_on_remove( async_dispatcher_connect( self.hass, self.device.event_reachable, self.update_callback ) ) - async def async_will_remove_from_hass(self) -> None: - """Unsubscribe device events when removed.""" - for unsub_dispatcher in self.unsub_dispatcher: - unsub_dispatcher() - @property def available(self): """Return True if device is available.""" diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index c914319aa424b8..ca76552a4ccdc5 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -57,7 +57,7 @@ def __init__(self, config, device): async def async_added_to_hass(self): """Subscribe camera events.""" - self.unsub_dispatcher.append( + self.async_on_remove( async_dispatcher_connect( self.hass, self.device.event_new_address, self._new_address ) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 5d709ab1e63f6f..afa5013b339e26 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -33,6 +33,7 @@ TIME_HOURS, UNIT_PERCENTAGE, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util @@ -260,7 +261,14 @@ def uid(self, coordinates): self.type, ) - def load_data(self, data): + @callback + def data_updated(self, data): + """Update data.""" + if self._load_data(data) and self.hass: + self.async_write_ha_state() + + @callback + def _load_data(self, data): """Load the sensor with relevant data.""" # Find sensor diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 7d16f072b98e4e..900b1caaf9797e 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -67,15 +67,12 @@ def __init__(self, hass, coordinates, timeframe, devices): async def update_devices(self): """Update all devices/sensors.""" - if self.devices: - tasks = [] - # Update all devices - for dev in self.devices: - if dev.load_data(self.data): - tasks.append(dev.async_update_ha_state()) - - if tasks: - await asyncio.wait(tasks) + if not self.devices: + return + + # Update all devices + for dev in self.devices: + dev.data_updated(self.data) async def schedule_update(self, minute=1): """Schedule an update after minute minutes.""" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 6bbf30b000e9dd..0fc3e55a5876da 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -271,7 +271,7 @@ def update_tokens(time): """Update tokens of the entities.""" for entity in component.entities: entity.async_update_token() - hass.async_create_task(entity.async_update_ha_state()) + entity.async_write_ha_state() hass.helpers.event.async_track_time_interval(update_tokens, TOKEN_CHANGE_INTERVAL) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 432b25ac602b05..b5eac4f9afea26 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -44,10 +44,12 @@ def __init__(self, name, ccb: ComfoConnectBridge) -> None: async def async_added_to_hass(self): """Register for sensor updates.""" _LOGGER.debug("Registering for fan speed") - async_dispatcher_connect( - self.hass, - SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(SENSOR_FAN_SPEED_MODE), - self._handle_update, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(SENSOR_FAN_SPEED_MODE), + self._handle_update, + ) ) await self.hass.async_add_executor_job( self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 5c8c0d6a75c9d6..cea09e97dbaeb8 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -234,10 +234,12 @@ async def async_added_to_hass(self): _LOGGER.debug( "Registering for sensor %s (%d)", self._sensor_type, self._sensor_id ) - async_dispatcher_connect( - self.hass, - SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(self._sensor_id), - self._handle_update, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(self._sensor_id), + self._handle_update, + ) ) await self.hass.async_add_executor_job( self._ccb.comfoconnect.register_sensor, self._sensor_id diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index b14592d1b78dd0..67dc07f68df790 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -197,7 +197,9 @@ def __init__(self, receiver): async def async_added_to_hass(self): """Register signal handler.""" - async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler) + self.async_on_remove( + async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler) + ) def signal_handler(self, data): """Handle domain-specific signal by calling appropriate method.""" diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 515b7cbc6141db..3157a00aa9f085 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -175,7 +175,7 @@ async def async_see( consider_home, ) if device.track: - await device.async_update_ha_state() + device.async_write_ha_state() return # Guard from calling see on entity registry entities. @@ -212,7 +212,7 @@ async def async_see( ) if device.track: - await device.async_update_ha_state() + device.async_write_ha_state() self.hass.bus.async_fire( EVENT_NEW_DEVICE, @@ -259,7 +259,7 @@ async def async_setup_tracked_device(self): async def async_init_single_device(dev): """Init a single device_tracker entity.""" await dev.async_added_to_hass() - await dev.async_update_ha_state() + dev.async_write_ha_state() tasks = [] for device in self.devices.values(): diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 0be5957a29a26e..677487945be875 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -32,7 +32,7 @@ extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["media_player"] +PLATFORMS = ["media_player", "remote"] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index 4a712ba053ec8b..8474849bdaa576 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -2,7 +2,7 @@ "domain": "directv", "name": "DirecTV", "documentation": "https://www.home-assistant.io/integrations/directv", - "requirements": ["directv==0.2.0"], + "requirements": ["directv==0.3.0"], "dependencies": [], "codeowners": ["@ctalkington"], "quality_scale": "gold", diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py new file mode 100644 index 00000000000000..8bc7c2208338b4 --- /dev/null +++ b/homeassistant/components/directv/remote.py @@ -0,0 +1,106 @@ +"""Support for the DIRECTV remote.""" +from datetime import timedelta +import logging +from typing import Any, Callable, Iterable, List + +from directv import DIRECTV, DIRECTVError + +from homeassistant.components.remote import RemoteDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import DIRECTVEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=2) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List, bool], None], +) -> bool: + """Load DirecTV remote based on a config entry.""" + dtv = hass.data[DOMAIN][entry.entry_id] + entities = [] + + for location in dtv.device.locations: + entities.append( + DIRECTVRemote( + dtv=dtv, name=str.title(location.name), address=location.address, + ) + ) + + async_add_entities(entities, True) + + +class DIRECTVRemote(DIRECTVEntity, RemoteDevice): + """Device that sends commands to a DirecTV receiver.""" + + def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: + """Initialize DirecTV remote.""" + super().__init__( + dtv=dtv, name=name, address=address, + ) + + self._available = False + self._is_on = True + + @property + def available(self): + """Return if able to retrieve information from device or not.""" + return self._available + + @property + def unique_id(self): + """Return a unique ID.""" + if self._address == "0": + return self.dtv.device.info.receiver_id + + return self._address + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._is_on + + async def async_update(self) -> None: + """Update device state.""" + status = await self.dtv.status(self._address) + + if status in ("active", "standby"): + self._available = True + self._is_on = status == "active" + else: + self._available = False + self._is_on = False + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self.dtv.remote("poweron", self._address) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self.dtv.remote("poweroff", self._address) + + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send a command to a device. + + Supported keys: power, poweron, poweroff, format, + pause, rew, replay, stop, advance, ffwd, record, + play, guide, active, list, exit, back, menu, info, + up, down, left, right, select, red, green, yellow, + blue, chanup, chandown, prev, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, dash, enter + """ + for single_command in command: + try: + await self.dtv.remote(single_command, self._address) + except DIRECTVError: + _LOGGER.exception( + "Sending command %s to device %s failed", + single_command, + self._device_id, + ) diff --git a/homeassistant/components/doorbird/.translations/en.json b/homeassistant/components/doorbird/.translations/en.json index 27c16fac3a17d5..f933b9c9929583 100644 --- a/homeassistant/components/doorbird/.translations/en.json +++ b/homeassistant/components/doorbird/.translations/en.json @@ -21,7 +21,8 @@ "title": "Connect to the DoorBird" } }, - "title": "DoorBird" + "title": "DoorBird", + "flow_title" : "DoorBird {name} ({host})" }, "options": { "step": { @@ -33,4 +34,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/doorbird/.translations/ko.json b/homeassistant/components/doorbird/.translations/ko.json index 121262065fd6e8..fcdee98a74d33f 100644 --- a/homeassistant/components/doorbird/.translations/ko.json +++ b/homeassistant/components/doorbird/.translations/ko.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\uc774 DoorBird \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc774 DoorBird \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "not_doorbird_device": "\uc774 \uae30\uae30\ub294 DoorBird \uac00 \uc544\ub2d9\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index 9b2c95dd7c9898..e4fb72db91baf9 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -27,6 +27,7 @@ "not_doorbird_device": "This device is not a DoorBird" }, "title" : "DoorBird", + "flow_title" : "DoorBird {name} ({host})", "error" : { "invalid_auth" : "Invalid authentication", "unknown" : "Unexpected error", diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 257407bb763870..484bd708489caf 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -15,8 +15,12 @@ EVENT_HOMEASSISTANT_STOP, TIME_HOURS, ) -from homeassistant.core import CoreState -import homeassistant.helpers.config_validation as cv +from homeassistant.core import CoreState, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -109,12 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(devices) - def update_entities_telegram(telegram): - """Update entities with latest telegram and trigger state update.""" - # Make all device entities aware of new telegram - for device in devices: - device.telegram = telegram - hass.async_create_task(device.async_update_ha_state()) + update_entities_telegram = partial(async_dispatcher_send, hass, DOMAIN) # Creates an asyncio.Protocol factory for reading DSMR telegrams from # serial and calls update_entities_telegram to update entities on arrival @@ -188,6 +187,18 @@ def __init__(self, name, obis, config): self._config = config self.telegram = {} + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + async_dispatcher_connect(self.hass, DOMAIN, self.update_data) + ) + + @callback + def update_data(self, telegram): + """Update data.""" + self.telegram = telegram + self.async_write_ha_state() + def get_dsmr_object_attr(self, attribute): """Read attribute from last received telegram for this DSMR object.""" # Make sure telegram contains an object for this entities obis diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 973d09a384f137..2f2ed8f2fa2293 100755 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -1,43 +1,55 @@ """Support for the Dynalite networks.""" import asyncio +from typing import Any, Dict, Union import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST +from homeassistant.components.cover import DEVICE_CLASSES_SCHEMA +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv # Loading the config flow file will register the flow from .bridge import DynaliteBridge from .const import ( + ACTIVE_INIT, + ACTIVE_OFF, + ACTIVE_ON, CONF_ACTIVE, - CONF_ACTIVE_INIT, - CONF_ACTIVE_OFF, - CONF_ACTIVE_ON, CONF_AREA, CONF_AUTO_DISCOVER, CONF_BRIDGES, CONF_CHANNEL, - CONF_CHANNEL_TYPE, + CONF_CHANNEL_COVER, + CONF_CLOSE_PRESET, CONF_DEFAULT, + CONF_DEVICE_CLASS, + CONF_DURATION, CONF_FADE, - CONF_NAME, CONF_NO_DEFAULT, - CONF_POLLTIMER, - CONF_PORT, + CONF_OPEN_PRESET, + CONF_POLL_TIMER, CONF_PRESET, + CONF_ROOM_OFF, + CONF_ROOM_ON, + CONF_STOP_PRESET, + CONF_TEMPLATE, + CONF_TILT_TIME, DEFAULT_CHANNEL_TYPE, DEFAULT_NAME, DEFAULT_PORT, + DEFAULT_TEMPLATES, DOMAIN, ENTITY_PLATFORMS, LOGGER, ) -def num_string(value): +def num_string(value: Union[int, str]) -> str: """Test if value is a string of digits, aka an integer.""" new_value = str(value) if new_value.isdigit(): @@ -49,7 +61,7 @@ def num_string(value): { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_FADE): vol.Coerce(float), - vol.Optional(CONF_CHANNEL_TYPE, default=DEFAULT_CHANNEL_TYPE): vol.Any( + vol.Optional(CONF_TYPE, default=DEFAULT_CHANNEL_TYPE): vol.Any( "light", "switch" ), } @@ -63,15 +75,66 @@ def num_string(value): PRESET_SCHEMA = vol.Schema({num_string: vol.Any(PRESET_DATA_SCHEMA, None)}) +TEMPLATE_ROOM_SCHEMA = vol.Schema( + {vol.Optional(CONF_ROOM_ON): num_string, vol.Optional(CONF_ROOM_OFF): num_string} +) -AREA_DATA_SCHEMA = vol.Schema( +TEMPLATE_TIMECOVER_SCHEMA = vol.Schema( { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_FADE): vol.Coerce(float), - vol.Optional(CONF_NO_DEFAULT): vol.Coerce(bool), - vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA, - vol.Optional(CONF_PRESET): PRESET_SCHEMA, - }, + vol.Optional(CONF_CHANNEL_COVER): num_string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_OPEN_PRESET): num_string, + vol.Optional(CONF_CLOSE_PRESET): num_string, + vol.Optional(CONF_STOP_PRESET): num_string, + vol.Optional(CONF_DURATION): vol.Coerce(float), + vol.Optional(CONF_TILT_TIME): vol.Coerce(float), + } +) + +TEMPLATE_DATA_SCHEMA = vol.Any(TEMPLATE_ROOM_SCHEMA, TEMPLATE_TIMECOVER_SCHEMA) + +TEMPLATE_SCHEMA = vol.Schema({str: TEMPLATE_DATA_SCHEMA}) + + +def validate_area(config: Dict[str, Any]) -> Dict[str, Any]: + """Validate that template parameters are only used if area is using the relevant template.""" + conf_set = set() + for template in DEFAULT_TEMPLATES: + for conf in DEFAULT_TEMPLATES[template]: + conf_set.add(conf) + if config.get(CONF_TEMPLATE): + for conf in DEFAULT_TEMPLATES[config[CONF_TEMPLATE]]: + conf_set.remove(conf) + for conf in conf_set: + if config.get(conf): + raise vol.Invalid( + f"{conf} should not be part of area {config[CONF_NAME]} config" + ) + return config + + +AREA_DATA_SCHEMA = vol.Schema( + vol.All( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_TEMPLATE): cv.string, + vol.Optional(CONF_FADE): vol.Coerce(float), + vol.Optional(CONF_NO_DEFAULT): cv.boolean, + vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA, + vol.Optional(CONF_PRESET): PRESET_SCHEMA, + # the next ones can be part of the templates + vol.Optional(CONF_ROOM_ON): num_string, + vol.Optional(CONF_ROOM_OFF): num_string, + vol.Optional(CONF_CHANNEL_COVER): num_string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_OPEN_PRESET): num_string, + vol.Optional(CONF_CLOSE_PRESET): num_string, + vol.Optional(CONF_STOP_PRESET): num_string, + vol.Optional(CONF_DURATION): vol.Coerce(float), + vol.Optional(CONF_TILT_TIME): vol.Coerce(float), + }, + validate_area, + ) ) AREA_SCHEMA = vol.Schema({num_string: vol.Any(AREA_DATA_SCHEMA, None)}) @@ -85,13 +148,14 @@ def num_string(value): vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, vol.Optional(CONF_AUTO_DISCOVER, default=False): vol.Coerce(bool), - vol.Optional(CONF_POLLTIMER, default=1.0): vol.Coerce(float), + vol.Optional(CONF_POLL_TIMER, default=1.0): vol.Coerce(float), vol.Optional(CONF_AREA): AREA_SCHEMA, vol.Optional(CONF_DEFAULT): PLATFORM_DEFAULTS_SCHEMA, vol.Optional(CONF_ACTIVE, default=False): vol.Any( - CONF_ACTIVE_ON, CONF_ACTIVE_OFF, CONF_ACTIVE_INIT, cv.boolean + ACTIVE_ON, ACTIVE_OFF, ACTIVE_INIT, cv.boolean ), vol.Optional(CONF_PRESET): PRESET_SCHEMA, + vol.Optional(CONF_TEMPLATE): TEMPLATE_SCHEMA, } ) @@ -105,7 +169,7 @@ def num_string(value): ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: """Set up the Dynalite platform.""" conf = config.get(DOMAIN) @@ -137,7 +201,7 @@ async def async_setup(hass, config): return True -async def async_entry_changed(hass, entry): +async def async_entry_changed(hass: HomeAssistant, entry: ConfigEntry) -> None: """Reload entry since the data has changed.""" LOGGER.debug("Reconfiguring entry %s", entry.data) bridge = hass.data[DOMAIN][entry.entry_id] @@ -145,7 +209,7 @@ async def async_entry_changed(hass, entry): LOGGER.debug("Reconfiguring entry finished %s", entry.data) -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a bridge from a config entry.""" LOGGER.debug("Setting up entry %s", entry.data) bridge = DynaliteBridge(hass, entry.data) @@ -163,7 +227,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" LOGGER.debug("Unloading entry %s", entry.data) hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py index fa0a91bfab1ca5..09cf8e25a10575 100755 --- a/homeassistant/components/dynalite/bridge.py +++ b/homeassistant/components/dynalite/bridge.py @@ -1,17 +1,26 @@ """Code to handle a Dynalite bridge.""" +from typing import TYPE_CHECKING, Any, Callable, Dict, List + from dynalite_devices_lib.dynalite_devices import DynaliteDevices -from homeassistant.core import callback +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import CONF_ALL, CONF_HOST, ENTITY_PLATFORMS, LOGGER +from .const import CONF_ALL, ENTITY_PLATFORMS, LOGGER +from .convert_config import convert_config + +if TYPE_CHECKING: # pragma: no cover + from dynalite_devices_lib.dynalite_devices import ( # pylint: disable=ungrouped-imports + DynaliteBaseDevice, + ) class DynaliteBridge: """Manages a single Dynalite bridge.""" - def __init__(self, hass, config): + def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: """Initialize the system based on host parameter.""" self.hass = hass self.area = {} @@ -23,20 +32,20 @@ def __init__(self, hass, config): new_device_func=self.add_devices_when_registered, update_device_func=self.update_device, ) - self.dynalite_devices.configure(config) + self.dynalite_devices.configure(convert_config(config)) - async def async_setup(self): + async def async_setup(self) -> bool: """Set up a Dynalite bridge.""" # Configure the dynalite devices LOGGER.debug("Setting up bridge - host %s", self.host) return await self.dynalite_devices.async_setup() - def reload_config(self, config): + def reload_config(self, config: Dict[str, Any]) -> None: """Reconfigure a bridge when config changes.""" LOGGER.debug("Reloading bridge - host %s, config %s", self.host, config) - self.dynalite_devices.configure(config) + self.dynalite_devices.configure(convert_config(config)) - def update_signal(self, device=None): + def update_signal(self, device: "DynaliteBaseDevice" = None) -> str: """Create signal to use to trigger entity update.""" if device: signal = f"dynalite-update-{self.host}-{device.unique_id}" @@ -45,12 +54,12 @@ def update_signal(self, device=None): return signal @callback - def update_device(self, device): + def update_device(self, device: "DynaliteBaseDevice") -> None: """Call when a device or all devices should be updated.""" if device == CONF_ALL: # This is used to signal connection or disconnection, so all devices may become available or not. log_string = ( - "Connected" if self.dynalite_devices.available else "Disconnected" + "Connected" if self.dynalite_devices.connected else "Disconnected" ) LOGGER.info("%s to dynalite host", log_string) async_dispatcher_send(self.hass, self.update_signal()) @@ -58,13 +67,13 @@ def update_device(self, device): async_dispatcher_send(self.hass, self.update_signal(device)) @callback - def register_add_devices(self, platform, async_add_devices): + def register_add_devices(self, platform: str, async_add_devices: Callable) -> None: """Add an async_add_entities for a category.""" self.async_add_devices[platform] = async_add_devices if platform in self.waiting_devices: self.async_add_devices[platform](self.waiting_devices[platform]) - def add_devices_when_registered(self, devices): + def add_devices_when_registered(self, devices: List["DynaliteBaseDevice"]) -> None: """Add the devices to HA if the add devices callback was registered, otherwise queue until it is.""" for platform in ENTITY_PLATFORMS: platform_devices = [ diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index ca95c0754a6f70..4c5b2ceb7d88a4 100755 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure Dynalite hub.""" +from typing import Any, Dict + from homeassistant import config_entries from homeassistant.const import CONF_HOST @@ -12,11 +14,11 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - def __init__(self): + def __init__(self) -> None: """Initialize the Dynalite flow.""" self.host = None - async def async_step_import(self, import_info): + async def async_step_import(self, import_info: Dict[str, Any]) -> Any: """Import a new bridge as a config entry.""" LOGGER.debug("Starting async_step_import - %s", import_info) host = import_info[CONF_HOST] diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index 267b5727b83dec..ade167e1b3e7a7 100755 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -1,31 +1,54 @@ """Constants for the Dynalite component.""" import logging +from homeassistant.components.cover import DEVICE_CLASS_SHUTTER +from homeassistant.const import CONF_ROOM + LOGGER = logging.getLogger(__package__) DOMAIN = "dynalite" -ENTITY_PLATFORMS = ["light", "switch"] +ENTITY_PLATFORMS = ["light", "switch", "cover"] CONF_ACTIVE = "active" -CONF_ACTIVE_INIT = "init" -CONF_ACTIVE_OFF = "off" -CONF_ACTIVE_ON = "on" +ACTIVE_INIT = "init" +ACTIVE_OFF = "off" +ACTIVE_ON = "on" CONF_ALL = "ALL" CONF_AREA = "area" CONF_AUTO_DISCOVER = "autodiscover" CONF_BRIDGES = "bridges" CONF_CHANNEL = "channel" -CONF_CHANNEL_TYPE = "type" +CONF_CHANNEL_COVER = "channel_cover" +CONF_CLOSE_PRESET = "close" CONF_DEFAULT = "default" +CONF_DEVICE_CLASS = "class" +CONF_DURATION = "duration" CONF_FADE = "fade" -CONF_HOST = "host" -CONF_NAME = "name" CONF_NO_DEFAULT = "nodefault" -CONF_POLLTIMER = "polltimer" -CONF_PORT = "port" +CONF_OPEN_PRESET = "open" +CONF_POLL_TIMER = "polltimer" CONF_PRESET = "preset" +CONF_ROOM_OFF = "room_off" +CONF_ROOM_ON = "room_on" +CONF_STOP_PRESET = "stop" +CONF_TEMPLATE = "template" +CONF_TILT_TIME = "tilt" +CONF_TIME_COVER = "time_cover" DEFAULT_CHANNEL_TYPE = "light" +DEFAULT_COVER_CLASS = DEVICE_CLASS_SHUTTER DEFAULT_NAME = "dynalite" DEFAULT_PORT = 12345 +DEFAULT_TEMPLATES = { + CONF_ROOM: [CONF_ROOM_ON, CONF_ROOM_OFF], + CONF_TIME_COVER: [ + CONF_CHANNEL_COVER, + CONF_DEVICE_CLASS, + CONF_OPEN_PRESET, + CONF_CLOSE_PRESET, + CONF_STOP_PRESET, + CONF_DURATION, + CONF_TILT_TIME, + ], +} diff --git a/homeassistant/components/dynalite/convert_config.py b/homeassistant/components/dynalite/convert_config.py new file mode 100644 index 00000000000000..03ece744d413be --- /dev/null +++ b/homeassistant/components/dynalite/convert_config.py @@ -0,0 +1,78 @@ +"""Convert the HA config to the dynalite config.""" + +from typing import Any, Dict + +from dynalite_devices_lib import const as dyn_const + +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM, CONF_TYPE + +from .const import ( + ACTIVE_INIT, + ACTIVE_OFF, + ACTIVE_ON, + CONF_ACTIVE, + CONF_AREA, + CONF_AUTO_DISCOVER, + CONF_CHANNEL, + CONF_CHANNEL_COVER, + CONF_CLOSE_PRESET, + CONF_DEFAULT, + CONF_DEVICE_CLASS, + CONF_DURATION, + CONF_FADE, + CONF_NO_DEFAULT, + CONF_OPEN_PRESET, + CONF_POLL_TIMER, + CONF_PRESET, + CONF_ROOM_OFF, + CONF_ROOM_ON, + CONF_STOP_PRESET, + CONF_TEMPLATE, + CONF_TILT_TIME, + CONF_TIME_COVER, +) + +CONF_MAP = { + CONF_ACTIVE: dyn_const.CONF_ACTIVE, + ACTIVE_INIT: dyn_const.CONF_ACTIVE_INIT, + ACTIVE_OFF: dyn_const.CONF_ACTIVE_OFF, + ACTIVE_ON: dyn_const.CONF_ACTIVE_ON, + CONF_AREA: dyn_const.CONF_AREA, + CONF_AUTO_DISCOVER: dyn_const.CONF_AUTO_DISCOVER, + CONF_CHANNEL: dyn_const.CONF_CHANNEL, + CONF_CHANNEL_COVER: dyn_const.CONF_CHANNEL_COVER, + CONF_TYPE: dyn_const.CONF_CHANNEL_TYPE, + CONF_CLOSE_PRESET: dyn_const.CONF_CLOSE_PRESET, + CONF_DEFAULT: dyn_const.CONF_DEFAULT, + CONF_DEVICE_CLASS: dyn_const.CONF_DEVICE_CLASS, + CONF_DURATION: dyn_const.CONF_DURATION, + CONF_FADE: dyn_const.CONF_FADE, + CONF_HOST: dyn_const.CONF_HOST, + CONF_NAME: dyn_const.CONF_NAME, + CONF_NO_DEFAULT: dyn_const.CONF_NO_DEFAULT, + CONF_OPEN_PRESET: dyn_const.CONF_OPEN_PRESET, + CONF_POLL_TIMER: dyn_const.CONF_POLL_TIMER, + CONF_PORT: dyn_const.CONF_PORT, + CONF_PRESET: dyn_const.CONF_PRESET, + CONF_ROOM: dyn_const.CONF_ROOM, + CONF_ROOM_OFF: dyn_const.CONF_ROOM_OFF, + CONF_ROOM_ON: dyn_const.CONF_ROOM_ON, + CONF_STOP_PRESET: dyn_const.CONF_STOP_PRESET, + CONF_TEMPLATE: dyn_const.CONF_TEMPLATE, + CONF_TILT_TIME: dyn_const.CONF_TILT_TIME, + CONF_TIME_COVER: dyn_const.CONF_TIME_COVER, +} + + +def convert_config(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert a config dict by replacing component consts with library consts.""" + result = {} + for (key, value) in config.items(): + if isinstance(value, dict): + new_value = convert_config(value) + elif isinstance(value, str): + new_value = CONF_MAP.get(value, value) + else: + new_value = value + result[CONF_MAP.get(key, key)] = new_value + return result diff --git a/homeassistant/components/dynalite/cover.py b/homeassistant/components/dynalite/cover.py new file mode 100644 index 00000000000000..dcf16ede58c34a --- /dev/null +++ b/homeassistant/components/dynalite/cover.py @@ -0,0 +1,98 @@ +"""Support for the Dynalite channels as covers.""" +from typing import Callable + +from homeassistant.components.cover import DEVICE_CLASSES, CoverDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback + +from .const import DEFAULT_COVER_CLASS +from .dynalitebase import DynaliteBase, async_setup_entry_base + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Record the async_add_entities function to add them later when received from Dynalite.""" + + @callback + def cover_from_device(device, bridge): + if device.has_tilt: + return DynaliteCoverWithTilt(device, bridge) + return DynaliteCover(device, bridge) + + async_setup_entry_base( + hass, config_entry, async_add_entities, "cover", cover_from_device + ) + + +class DynaliteCover(DynaliteBase, CoverDevice): + """Representation of a Dynalite Channel as a Home Assistant Cover.""" + + @property + def device_class(self) -> str: + """Return the class of the device.""" + dev_cls = self._device.device_class + if dev_cls in DEVICE_CLASSES: + return dev_cls + return DEFAULT_COVER_CLASS + + @property + def current_cover_position(self) -> int: + """Return the position of the cover from 0 to 100.""" + return self._device.current_cover_position + + @property + def is_opening(self) -> bool: + """Return true if cover is opening.""" + return self._device.is_opening + + @property + def is_closing(self) -> bool: + """Return true if cover is closing.""" + return self._device.is_closing + + @property + def is_closed(self) -> bool: + """Return true if cover is closed.""" + return self._device.is_closed + + async def async_open_cover(self, **kwargs) -> None: + """Open the cover.""" + await self._device.async_open_cover(**kwargs) + + async def async_close_cover(self, **kwargs) -> None: + """Close the cover.""" + await self._device.async_close_cover(**kwargs) + + async def async_set_cover_position(self, **kwargs) -> None: + """Set the cover position.""" + await self._device.async_set_cover_position(**kwargs) + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the cover.""" + await self._device.async_stop_cover(**kwargs) + + +class DynaliteCoverWithTilt(DynaliteCover): + """Representation of a Dynalite Channel as a Home Assistant Cover that uses up and down for tilt.""" + + @property + def current_cover_tilt_position(self) -> int: + """Return the current tilt position.""" + return self._device.current_cover_tilt_position + + async def async_open_cover_tilt(self, **kwargs) -> None: + """Open cover tilt.""" + await self._device.async_open_cover_tilt(**kwargs) + + async def async_close_cover_tilt(self, **kwargs) -> None: + """Close cover tilt.""" + await self._device.async_close_cover_tilt(**kwargs) + + async def async_set_cover_tilt_position(self, **kwargs) -> None: + """Set the cover tilt position.""" + await self._device.async_set_cover_tilt_position(**kwargs) + + async def async_stop_cover_tilt(self, **kwargs) -> None: + """Stop the cover tilt.""" + await self._device.async_stop_cover_tilt(**kwargs) diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index 8bb1ab2dc42ab7..31879c5c118f6b 100755 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -1,5 +1,9 @@ """Support for the Dynalite devices as entities.""" -from homeassistant.core import callback +from typing import Any, Callable, Dict + +from homeassistant.components.dynalite.bridge import DynaliteBridge +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -7,8 +11,12 @@ def async_setup_entry_base( - hass, config_entry, async_add_entities, platform, entity_from_device -): + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable, + platform: str, + entity_from_device: Callable, +) -> None: """Record the async_add_entities function to add them later when received from Dynalite.""" LOGGER.debug("Setting up %s entry = %s", platform, config_entry.data) bridge = hass.data[DOMAIN][config_entry.entry_id] @@ -18,8 +26,7 @@ def async_add_entities_platform(devices): # assumes it is called with a single platform added_entities = [] for device in devices: - if device.category == platform: - added_entities.append(entity_from_device(device, bridge)) + added_entities.append(entity_from_device(device, bridge)) if added_entities: async_add_entities(added_entities) @@ -29,29 +36,29 @@ def async_add_entities_platform(devices): class DynaliteBase(Entity): """Base class for the Dynalite entities.""" - def __init__(self, device, bridge): + def __init__(self, device: Any, bridge: DynaliteBridge) -> None: """Initialize the base class.""" self._device = device self._bridge = bridge self._unsub_dispatchers = [] @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._device.name @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the entity.""" return self._device.unique_id @property - def available(self): + def available(self) -> bool: """Return if entity is available.""" return self._device.available @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Device info for this entity.""" return { "identifiers": {(DOMAIN, self._device.unique_id)}, @@ -59,7 +66,7 @@ def device_info(self): "manufacturer": "Dynalite", } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Added to hass so need to register to dispatch.""" # register for device specific update self._unsub_dispatchers.append( @@ -78,7 +85,7 @@ async def async_added_to_hass(self): ) ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unregister signal dispatch listeners when being removed.""" for unsub in self._unsub_dispatchers: unsub() diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index a5b7139803c174..283b1ee2286191 100755 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -1,10 +1,16 @@ """Support for Dynalite channels as lights.""" +from typing import Callable + from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from .dynalitebase import DynaliteBase, async_setup_entry_base -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Record the async_add_entities function to add them later when received from Dynalite.""" async_setup_entry_base( @@ -16,24 +22,24 @@ class DynaliteLight(DynaliteBase, Light): """Representation of a Dynalite Channel as a Home Assistant Light.""" @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" return self._device.brightness @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._device.is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" await self._device.async_turn_on(**kwargs) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the light off.""" await self._device.async_turn_off(**kwargs) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index d6351db17b2439..a6ae0e96c459b6 100755 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/dynalite", "dependencies": [], "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.32"] + "requirements": ["dynalite_devices==0.1.39"] } diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py index 84be74cee36e6c..45e24d8193ab1f 100755 --- a/homeassistant/components/dynalite/switch.py +++ b/homeassistant/components/dynalite/switch.py @@ -1,10 +1,16 @@ """Support for the Dynalite channels and presets as switches.""" +from typing import Callable + from homeassistant.components.switch import SwitchDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from .dynalitebase import DynaliteBase, async_setup_entry_base -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: """Record the async_add_entities function to add them later when received from Dynalite.""" async_setup_entry_base( @@ -16,14 +22,14 @@ class DynaliteSwitch(DynaliteBase, SwitchDevice): """Representation of a Dynalite Channel as a Home Assistant Switch.""" @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" return self._device.is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the switch on.""" await self._device.async_turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the switch off.""" await self._device.async_turn_off() diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 595144013b6b16..022878c8276dcd 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -214,7 +214,11 @@ def async_eight_user_update(): """Update callback.""" self.async_schedule_update_ha_state(True) - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_USER, async_eight_user_update) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_USER, async_eight_user_update + ) + ) @property def should_poll(self): @@ -237,7 +241,11 @@ def async_eight_heat_update(): """Update callback.""" self.async_schedule_update_ha_state(True) - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update + ) + ) @property def should_poll(self): diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py index 876c7a1f05baab..90ab408775414a 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -71,8 +71,10 @@ def __init__(self, dev_id, dev_name="EnOcean device"): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_RECEIVE_MESSAGE, self._message_received_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_RECEIVE_MESSAGE, self._message_received_callback + ) ) def _message_received_callback(self, packet): diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index beb1c1cda822ba..62c57daf19d727 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -112,9 +112,15 @@ def __init__( async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) - async_dispatcher_connect( - self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback + ) ) @callback diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index a6eaa21ae35ece..fe131e4dab48d5 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -55,15 +55,18 @@ def should_poll(self): async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + ) + state = await self.async_get_last_state() if not state: return self._state = state.state - async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update - ) - def update(self): """Get the latest data and update the states.""" data = self.speedtest_client.data diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index ad0c590b87db08..f109103a99c9b3 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -140,12 +140,20 @@ async def async_added_to_hass(self): This method is a coroutine. """ - async_dispatcher_connect( - self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg + ) ) - async_dispatcher_connect(self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg) - async_dispatcher_connect( - self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg + ) ) # register start/stop diff --git a/homeassistant/components/flunearyou/.translations/en.json b/homeassistant/components/flunearyou/.translations/en.json new file mode 100644 index 00000000000000..ca868b8ebd91d6 --- /dev/null +++ b/homeassistant/components/flunearyou/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "These coordinates are already registered." + }, + "error": { + "general_error": "There was an unknown error." + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Monitor user-based and CDC repots for a pair of coordinates.", + "title": "Configure Flu Near You" + } + }, + "title": "Flu Near You" + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 5657e646be509b..ce59c959133f5d 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -1 +1,216 @@ """The flunearyou component.""" +import asyncio +from datetime import timedelta + +from pyflunearyou import Client +from pyflunearyou.errors import FluNearYouError +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + CATEGORY_CDC_REPORT, + CATEGORY_USER_REPORT, + DATA_CLIENT, + DOMAIN, + LOGGER, + SENSORS, + TOPIC_UPDATE, +) + +DATA_LISTENER = "listener" + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(DOMAIN): vol.Schema( + { + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +@callback +def async_get_api_category(sensor_type): + """Get the category that a particular sensor type belongs to.""" + try: + return next( + ( + category + for category, sensors in SENSORS.items() + for sensor in sensors + if sensor[0] == sensor_type + ) + ) + except StopIteration: + raise ValueError(f"Can't find category sensor type: {sensor_type}") + + +async def async_setup(hass, config): + """Set up the Flu Near You component.""" + hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}} + + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_LATITUDE: config[DOMAIN].get(CONF_LATITUDE, hass.config.latitude), + CONF_LONGITUDE: config[DOMAIN].get( + CONF_LATITUDE, hass.config.longitude + ), + }, + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Flu Near You as config entry.""" + websession = aiohttp_client.async_get_clientsession(hass) + + fny = FluNearYouData( + hass, + Client(websession), + config_entry.data.get(CONF_LATITUDE, hass.config.latitude), + config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), + ) + + try: + await fny.async_update() + except FluNearYouError as err: + LOGGER.error("Error while setting up integration: %s", err) + raise ConfigEntryNotReady + + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = fny + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + ) + + async def refresh(event_time): + """Refresh data from Flu Near You.""" + await fny.async_update() + + hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval( + hass, refresh, DEFAULT_SCAN_INTERVAL + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an Flu Near You config entry.""" + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + + remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) + remove_listener() + + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + + return True + + +class FluNearYouData: + """Define a data object to retrieve info from Flu Near You.""" + + def __init__(self, hass, client, latitude, longitude): + """Initialize.""" + self._async_cancel_time_interval_listener = None + self._client = client + self._hass = hass + self.data = {} + self.latitude = latitude + self.longitude = longitude + + self._api_coros = { + CATEGORY_CDC_REPORT: self._client.cdc_reports.status_by_coordinates( + latitude, longitude + ), + CATEGORY_USER_REPORT: self._client.user_reports.status_by_coordinates( + latitude, longitude + ), + } + + self._api_category_count = { + CATEGORY_CDC_REPORT: 0, + CATEGORY_USER_REPORT: 0, + } + + self._api_category_locks = { + CATEGORY_CDC_REPORT: asyncio.Lock(), + CATEGORY_USER_REPORT: asyncio.Lock(), + } + + async def _async_get_data_from_api(self, api_category): + """Update and save data for a particular API category.""" + if self._api_category_count[api_category] == 0: + return + + try: + self.data[api_category] = await self._api_coros[api_category] + except FluNearYouError as err: + LOGGER.error("Unable to get %s data: %s", api_category, err) + self.data[api_category] = None + + async def _async_update_listener_action(self, now): + """Define an async_track_time_interval action to update data.""" + await self.async_update() + + @callback + def async_deregister_api_interest(self, sensor_type): + """Decrement the number of entities with data needs from an API category.""" + # If this deregistration should leave us with no registration at all, remove the + # time interval: + if sum(self._api_category_count.values()) == 0: + if self._async_cancel_time_interval_listener: + self._async_cancel_time_interval_listener() + self._async_cancel_time_interval_listener = None + return + + api_category = async_get_api_category(sensor_type) + self._api_category_count[api_category] -= 1 + + async def async_register_api_interest(self, sensor_type): + """Increment the number of entities with data needs from an API category.""" + # If this is the first registration we have, start a time interval: + if not self._async_cancel_time_interval_listener: + self._async_cancel_time_interval_listener = async_track_time_interval( + self._hass, self._async_update_listener_action, DEFAULT_SCAN_INTERVAL, + ) + + api_category = async_get_api_category(sensor_type) + self._api_category_count[api_category] += 1 + + # If a sensor registers interest in a particular API call and the data doesn't + # exist for it yet, make the API call and grab the data: + async with self._api_category_locks[api_category]: + if api_category not in self.data: + await self._async_get_data_from_api(api_category) + + async def async_update(self): + """Update Flu Near You data.""" + tasks = [ + self._async_get_data_from_api(api_category) + for api_category in self._api_coros + ] + + await asyncio.gather(*tasks) + + LOGGER.debug("Received new data") + async_dispatcher_send(self._hass, TOPIC_UPDATE) diff --git a/homeassistant/components/flunearyou/config_flow.py b/homeassistant/components/flunearyou/config_flow.py new file mode 100644 index 00000000000000..2c48b14bb03329 --- /dev/null +++ b/homeassistant/components/flunearyou/config_flow.py @@ -0,0 +1,60 @@ +"""Define a config flow manager for flunearyou.""" +from pyflunearyou import Client +from pyflunearyou.errors import FluNearYouError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN, LOGGER # pylint: disable=unused-import + + +class FluNearYouFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an FluNearYou config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @property + def data_schema(self): + """Return the data schema for integration.""" + return vol.Schema( + { + vol.Required( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + } + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + 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=self.data_schema) + + unique_id = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + websession = aiohttp_client.async_get_clientsession(self.hass) + client = Client(websession) + + try: + await client.cdc_reports.status_by_coordinates( + user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE] + ) + except FluNearYouError as err: + LOGGER.error("Error while setting up integration: %s", err) + return self.async_show_form( + step_id="user", errors={"base": "general_error"} + ) + + return self.async_create_entry(title=unique_id, data=user_input) diff --git a/homeassistant/components/flunearyou/const.py b/homeassistant/components/flunearyou/const.py new file mode 100644 index 00000000000000..9693d59fc6e79a --- /dev/null +++ b/homeassistant/components/flunearyou/const.py @@ -0,0 +1,38 @@ +"""Define flunearyou constants.""" +import logging + +DOMAIN = "flunearyou" +LOGGER = logging.getLogger("homeassistant.components.flunearyou") + +DATA_CLIENT = "client" + +CATEGORY_CDC_REPORT = "cdc_report" +CATEGORY_USER_REPORT = "user_report" + +TOPIC_UPDATE = "flunearyou_update" + +TYPE_CDC_LEVEL = "level" +TYPE_CDC_LEVEL2 = "level2" +TYPE_USER_CHICK = "chick" +TYPE_USER_DENGUE = "dengue" +TYPE_USER_FLU = "flu" +TYPE_USER_LEPTO = "lepto" +TYPE_USER_NO_SYMPTOMS = "none" +TYPE_USER_SYMPTOMS = "symptoms" +TYPE_USER_TOTAL = "total" + +SENSORS = { + CATEGORY_CDC_REPORT: [ + (TYPE_CDC_LEVEL, "CDC Level", "mdi:biohazard", None), + (TYPE_CDC_LEVEL2, "CDC Level 2", "mdi:biohazard", None), + ], + CATEGORY_USER_REPORT: [ + (TYPE_USER_CHICK, "Avian Flu Symptoms", "mdi:alert", "reports"), + (TYPE_USER_DENGUE, "Dengue Fever Symptoms", "mdi:alert", "reports"), + (TYPE_USER_FLU, "Flu Symptoms", "mdi:alert", "reports"), + (TYPE_USER_LEPTO, "Leptospirosis Symptoms", "mdi:alert", "reports"), + (TYPE_USER_NO_SYMPTOMS, "No Symptoms", "mdi:alert", "reports"), + (TYPE_USER_SYMPTOMS, "Flu-like Symptoms", "mdi:alert", "reports"), + (TYPE_USER_TOTAL, "Total Symptoms", "mdi:alert", "reports"), + ], +} diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index e7394356c64d1b..1a28c3076e71c9 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -1,8 +1,9 @@ { "domain": "flunearyou", "name": "Flu Near You", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flunearyou", - "requirements": ["pyflunearyou==1.0.3"], + "requirements": ["pyflunearyou==1.0.7"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index e06eb3a8ef49dc..6868d21ce1fcb9 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -1,25 +1,24 @@ """Support for user- and CDC-based flu info sensors from Flu Near You.""" -from datetime import timedelta -import logging - -from pyflunearyou import Client -from pyflunearyou.errors import FluNearYouError -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_STATE, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, -) -from homeassistant.helpers import aiohttp_client -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_STATE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -_LOGGER = logging.getLogger(__name__) +from .const import ( + CATEGORY_CDC_REPORT, + CATEGORY_USER_REPORT, + DATA_CLIENT, + DOMAIN, + SENSORS, + TOPIC_UPDATE, + TYPE_USER_CHICK, + TYPE_USER_DENGUE, + TYPE_USER_FLU, + TYPE_USER_LEPTO, + TYPE_USER_NO_SYMPTOMS, + TYPE_USER_SYMPTOMS, + TYPE_USER_TOTAL, +) ATTR_CITY = "city" ATTR_REPORTED_DATE = "reported_date" @@ -31,94 +30,46 @@ DEFAULT_ATTRIBUTION = "Data provided by Flu Near You" -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) -SCAN_INTERVAL = timedelta(minutes=30) - -CATEGORY_CDC_REPORT = "cdc_report" -CATEGORY_USER_REPORT = "user_report" - -TYPE_CDC_LEVEL = "level" -TYPE_CDC_LEVEL2 = "level2" -TYPE_USER_CHICK = "chick" -TYPE_USER_DENGUE = "dengue" -TYPE_USER_FLU = "flu" -TYPE_USER_LEPTO = "lepto" -TYPE_USER_NO_SYMPTOMS = "none" -TYPE_USER_SYMPTOMS = "symptoms" -TYPE_USER_TOTAL = "total" - EXTENDED_TYPE_MAPPING = { TYPE_USER_FLU: "ili", TYPE_USER_NO_SYMPTOMS: "no_symptoms", TYPE_USER_TOTAL: "total_surveys", } -SENSORS = { - CATEGORY_CDC_REPORT: [ - (TYPE_CDC_LEVEL, "CDC Level", "mdi:biohazard", None), - (TYPE_CDC_LEVEL2, "CDC Level 2", "mdi:biohazard", None), - ], - CATEGORY_USER_REPORT: [ - (TYPE_USER_CHICK, "Avian Flu Symptoms", "mdi:alert", "reports"), - (TYPE_USER_DENGUE, "Dengue Fever Symptoms", "mdi:alert", "reports"), - (TYPE_USER_FLU, "Flu Symptoms", "mdi:alert", "reports"), - (TYPE_USER_LEPTO, "Leptospirosis Symptoms", "mdi:alert", "reports"), - (TYPE_USER_NO_SYMPTOMS, "No Symptoms", "mdi:alert", "reports"), - (TYPE_USER_SYMPTOMS, "Flu-like Symptoms", "mdi:alert", "reports"), - (TYPE_USER_TOTAL, "Total Symptoms", "mdi:alert", "reports"), - ], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( - cv.ensure_list, [vol.In(SENSORS)] - ), - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Configure the platform and add the sensors.""" - websession = aiohttp_client.async_get_clientsession(hass) - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Flu Near You sensors based on a config entry.""" + fny = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] - fny = FluNearYouData( - Client(websession), latitude, longitude, config[CONF_MONITORED_CONDITIONS] + async_add_entities( + [ + FluNearYouSensor(fny, sensor_type, name, category, icon, unit) + for category, sensors in SENSORS.items() + for sensor_type, name, icon, unit in sensors + ], + True, ) - await fny.async_update() - - sensors = [ - FluNearYouSensor(fny, kind, name, category, icon, unit) - for category in config[CONF_MONITORED_CONDITIONS] - for kind, name, icon, unit in SENSORS[category] - ] - - async_add_entities(sensors, True) class FluNearYouSensor(Entity): """Define a base Flu Near You sensor.""" - def __init__(self, fny, kind, name, category, icon, unit): + def __init__(self, fny, sensor_type, name, category, icon, unit): """Initialize the sensor.""" + self._async_unsub_dispatcher_connect = None self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._category = category + self._fny = fny self._icon = icon - self._kind = kind self._name = name + self._sensor_type = sensor_type self._state = None self._unit = unit - self.fny = fny @property def available(self): """Return True if entity is available.""" - return bool(self.fny.data[self._category]) + return bool(self._fny.data[self._category]) @property def device_state_attributes(self): @@ -143,19 +94,43 @@ def state(self): @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self.fny.latitude},{self.fny.longitude}_{self._kind}" + return f"{self._fny.latitude},{self._fny.longitude}_{self._sensor_type}" @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit - async def async_update(self): - """Update the sensor.""" - await self.fny.async_update() + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def update(): + """Update the state.""" + self.update_from_latest_data() + self.async_write_ha_state() + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update + ) - cdc_data = self.fny.data.get(CATEGORY_CDC_REPORT) - user_data = self.fny.data.get(CATEGORY_USER_REPORT) + await self._fny.async_register_api_interest(self._sensor_type) + + self.update_from_latest_data() + + async def async_will_remove_from_hass(self) -> None: + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() + self._async_unsub_dispatcher_connect = None + + self._fny.async_deregister_api_interest(self._sensor_type) + + @callback + def update_from_latest_data(self): + """Update the sensor.""" + cdc_data = self._fny.data.get(CATEGORY_CDC_REPORT) + user_data = self._fny.data.get(CATEGORY_USER_REPORT) if self._category == CATEGORY_CDC_REPORT and cdc_data: self._attrs.update( @@ -164,7 +139,7 @@ async def async_update(self): ATTR_STATE: cdc_data["name"], } ) - self._state = cdc_data[self._kind] + self._state = cdc_data[self._sensor_type] elif self._category == CATEGORY_USER_REPORT and user_data: self._attrs.update( { @@ -176,10 +151,10 @@ async def async_update(self): } ) - if self._kind in user_data["state"]["data"]: - states_key = self._kind - elif self._kind in EXTENDED_TYPE_MAPPING: - states_key = EXTENDED_TYPE_MAPPING[self._kind] + if self._sensor_type in user_data["state"]["data"]: + states_key = self._sensor_type + elif self._sensor_type in EXTENDED_TYPE_MAPPING: + states_key = EXTENDED_TYPE_MAPPING[self._sensor_type] self._attrs[ATTR_STATE_REPORTS_THIS_WEEK] = user_data["state"]["data"][ states_key @@ -188,7 +163,7 @@ async def async_update(self): "last_week_data" ][states_key] - if self._kind == TYPE_USER_TOTAL: + if self._sensor_type == TYPE_USER_TOTAL: self._state = sum( v for k, v in user_data["local"].items() @@ -202,32 +177,4 @@ async def async_update(self): ) ) else: - self._state = user_data["local"][self._kind] - - -class FluNearYouData: - """Define a data object to retrieve info from Flu Near You.""" - - def __init__(self, client, latitude, longitude, sensor_types): - """Initialize.""" - self._client = client - self._sensor_types = sensor_types - self.data = {} - self.latitude = latitude - self.longitude = longitude - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """Update Flu Near You data.""" - for key, method in [ - (CATEGORY_CDC_REPORT, self._client.cdc_reports.status_by_coordinates), - (CATEGORY_USER_REPORT, self._client.user_reports.status_by_coordinates), - ]: - if key in self._sensor_types: - try: - self.data[key] = await method(self.latitude, self.longitude) - except FluNearYouError as err: - _LOGGER.error('There was an error with "%s" data: %s', key, err) - self.data[key] = {} - - _LOGGER.debug("New data stored: %s", self.data) + self._state = user_data["local"][self._sensor_type] diff --git a/homeassistant/components/flunearyou/strings.json b/homeassistant/components/flunearyou/strings.json new file mode 100644 index 00000000000000..5539c6f82df7d6 --- /dev/null +++ b/homeassistant/components/flunearyou/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Flu Near You", + "step": { + "user": { + "title": "Configure Flu Near You", + "description": "Monitor user-based and CDC repots for a pair of coordinates.", + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "error": { + "general_error": "There was an unknown error." + }, + "abort": { + "already_configured": "These coordinates are already registered." + } + } +} diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 3abeab322620eb..768ef108969b8e 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -132,7 +132,9 @@ def fetch(): ) return response.content except requests.exceptions.RequestException as error: - _LOGGER.error("Error getting camera image: %s", error) + _LOGGER.error( + "Error getting new camera image from %s: %s", self._name, error + ) return self._last_image self._last_image = await self.hass.async_add_job(fetch) @@ -146,10 +148,12 @@ def fetch(): response = await websession.get(url, auth=self._auth) self._last_image = await response.read() except asyncio.TimeoutError: - _LOGGER.error("Timeout getting image from: %s", self._name) + _LOGGER.error("Timeout getting camera image from %s", self._name) return self._last_image except aiohttp.ClientError as err: - _LOGGER.error("Error getting new camera image: %s", err) + _LOGGER.error( + "Error getting new camera image from %s: %s", self._name, err + ) return self._last_image self._last_url = url diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 09af5ad5c44fb1..9a8d6177214861 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -334,7 +334,7 @@ async def async_set_temperature(self, **kwargs): return self._target_temp = temperature await self._async_control_heating(force=True) - await self.async_update_ha_state() + self.async_write_ha_state() @property def min_temp(self): @@ -361,7 +361,7 @@ async def _async_sensor_changed(self, entity_id, old_state, new_state): self._async_update_temp(new_state) await self._async_control_heating() - await self.async_update_ha_state() + self.async_write_ha_state() @callback def _async_switch_changed(self, entity_id, old_state, new_state): @@ -468,4 +468,4 @@ async def async_set_preset_mode(self, preset_mode: str): self._target_temp = self._saved_target_temp await self._async_control_heating(force=True) - await self.async_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index bb25d2d619d175..0b99224bf7f22f 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -204,7 +204,7 @@ def __init__(self) -> None: async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh)) async def _refresh(self, payload: Optional[dict] = None) -> None: """Process any signals.""" diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 0a7973709c1539..c7e708e3207ef0 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -3,7 +3,7 @@ from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout -from gios import ApiError, Gios, NoStationError +from gios import ApiError, Gios, InvalidSensorsData, NoStationError from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -63,7 +63,12 @@ async def _async_update_data(self): try: with timeout(30): await self.gios.update() - except (ApiError, NoStationError, ClientConnectorError) as error: + except ( + ApiError, + NoStationError, + ClientConnectorError, + InvalidSensorsData, + ) as error: raise UpdateFailed(error) if not self.gios.data: raise UpdateFailed("Invalid sensors data") diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index 368d610c22640e..5741af47a072eb 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -3,10 +3,10 @@ from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout -from gios import ApiError, Gios, NoStationError +from gios import ApiError, Gios, InvalidSensorsData, NoStationError import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import config_entries from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -43,9 +43,6 @@ async def async_step_user(self, user_input=None): gios = Gios(user_input[CONF_STATION_ID], websession) await gios.update() - if not gios.available: - raise InvalidSensorsData() - return self.async_create_entry( title=user_input[CONF_STATION_ID], data=user_input, ) @@ -59,7 +56,3 @@ async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - - -class InvalidSensorsData(exceptions.HomeAssistantError): - """Error to indicate invalid sensors data.""" diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 67fcbebe9a2cd9..3e3d63965d3f0d 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gios", "dependencies": [], "codeowners": ["@bieniu"], - "requirements": ["gios==0.0.5"], + "requirements": ["gios==0.1.1"], "config_flow": true } diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 1c47be45651c7c..2bc5f5040d413e 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1247,7 +1247,11 @@ class OpenCloseTrait(_Trait): """ # Cover device classes that require 2FA - COVER_2FA = (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE) + COVER_2FA = ( + cover.DEVICE_CLASS_DOOR, + cover.DEVICE_CLASS_GARAGE, + cover.DEVICE_CLASS_GATE, + ) name = TRAIT_OPENCLOSE commands = [COMMAND_OPENCLOSE] diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index f8a10017cab1af..53ce1c0634b9d3 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -289,7 +289,7 @@ async def groups_service_handler(service): need_update = True if need_update: - await group.async_update_ha_state() + group.async_write_ha_state() return @@ -538,7 +538,7 @@ async def _async_state_changed_listener(self, entity_id, old_state, new_state): return self._async_update_group_state(new_state) - await self.async_update_ha_state() + self.async_write_ha_state() @property def _tracking_states(self): diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index ff7b47d60107e4..9d9c9dfb8e97ac 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -2,11 +2,9 @@ import logging from urllib.parse import urlparse -import aioharmony.exceptions as harmony_exceptions -from aioharmony.harmonyapi import HarmonyAPI import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, exceptions from homeassistant.components import ssdp from homeassistant.components.remote import ( ATTR_ACTIVITY, @@ -17,7 +15,11 @@ from homeassistant.core import callback from .const import DOMAIN, UNIQUE_ID -from .util import find_unique_id_for_remote +from .util import ( + find_best_name_for_remote, + find_unique_id_for_remote, + get_harmony_client_if_available, +) _LOGGER = logging.getLogger(__name__) @@ -26,43 +28,19 @@ ) -async def get_harmony_client_if_available(hass: core.HomeAssistant, ip_address): - """Connect to a harmony hub and fetch info.""" - harmony = HarmonyAPI(ip_address=ip_address) - - try: - if not await harmony.connect(): - await harmony.close() - return None - except harmony_exceptions.TimeOut: - return None - - await harmony.close() - - return harmony - - -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ - harmony = await get_harmony_client_if_available(hass, data[CONF_HOST]) + harmony = await get_harmony_client_if_available(data[CONF_HOST]) if not harmony: raise CannotConnect - unique_id = find_unique_id_for_remote(harmony) - - # As a last resort we get the name from the harmony client - # in the event a name was not provided. harmony.name is - # usually the ip address but it can be an empty string. - if CONF_NAME not in data or data[CONF_NAME] is None or data[CONF_NAME] == "": - data[CONF_NAME] = harmony.name - return { - CONF_NAME: data[CONF_NAME], + CONF_NAME: find_best_name_for_remote(data, harmony), CONF_HOST: data[CONF_HOST], - UNIQUE_ID: unique_id, + UNIQUE_ID: find_unique_id_for_remote(harmony), } @@ -82,7 +60,7 @@ async def async_step_user(self, user_input=None): if user_input is not None: try: - validated = await validate_input(self.hass, user_input) + validated = await validate_input(user_input) except CannotConnect: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except @@ -116,9 +94,7 @@ async def async_step_ssdp(self, discovery_info): CONF_NAME: friendly_name, } - harmony = await get_harmony_client_if_available( - self.hass, self.harmony_config[CONF_HOST] - ) + harmony = await get_harmony_client_if_available(parsed_url.hostname) if harmony: unique_id = find_unique_id_for_remote(harmony) @@ -150,9 +126,15 @@ async def async_step_link(self, user_input=None): }, ) - async def async_step_import(self, user_input): + async def async_step_import(self, validated_input): """Handle import.""" - return await self.async_step_user(user_input) + await self.async_set_unique_id(validated_input[UNIQUE_ID]) + self._abort_if_unique_id_configured() + # Everything was validated in remote async_setup_platform + # all we do now is create. + return await self._async_create_entry_from_valid_input( + validated_input, validated_input + ) @staticmethod @callback diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 024af9b15804bd..5af8d1eb65a31a 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -24,6 +24,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -33,6 +34,13 @@ HARMONY_OPTIONS_UPDATE, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, + UNIQUE_ID, +) +from .util import ( + find_best_name_for_remote, + find_matching_config_entries_for_host, + find_unique_id_for_remote, + get_harmony_client_if_available, ) _LOGGER = logging.getLogger(__name__) @@ -51,6 +59,7 @@ extra=vol.ALLOW_EXTRA, ) + HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( @@ -68,9 +77,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Now handled by ssdp in the config flow return + if find_matching_config_entries_for_host(hass, config[CONF_HOST]): + return + + # We do the validation to verify we can connect + # so we can raise PlatformNotReady to force + # a retry so we can avoid a scenario where the config + # entry cannot be created via import because hub + # is not yet ready. + harmony = await get_harmony_client_if_available(config[CONF_HOST]) + if not harmony: + raise PlatformNotReady + + validated_config = config.copy() + validated_config[UNIQUE_ID] = find_unique_id_for_remote(harmony) + validated_config[CONF_NAME] = find_best_name_for_remote(config, harmony) + hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config + DOMAIN, context={"source": SOURCE_IMPORT}, data=validated_config ) ) diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py index 5f7e46510f9f95..69ed44cb7dac10 100644 --- a/homeassistant/components/harmony/util.py +++ b/homeassistant/components/harmony/util.py @@ -1,8 +1,13 @@ """The Logitech Harmony Hub integration utils.""" -from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient +import aioharmony.exceptions as harmony_exceptions +from aioharmony.harmonyapi import HarmonyAPI +from homeassistant.const import CONF_HOST, CONF_NAME -def find_unique_id_for_remote(harmony: HarmonyClient): +from .const import DOMAIN + + +def find_unique_id_for_remote(harmony: HarmonyAPI): """Find the unique id for both websocket and xmpp clients.""" websocket_unique_id = harmony.hub_config.info.get("activeRemoteId") if websocket_unique_id is not None: @@ -10,3 +15,38 @@ def find_unique_id_for_remote(harmony: HarmonyClient): # fallback to the xmpp unique id if websocket is not available return harmony.config["global"]["timeStampHash"].split(";")[-1] + + +def find_best_name_for_remote(data: dict, harmony: HarmonyAPI): + """Find the best name from config or fallback to the remote.""" + # As a last resort we get the name from the harmony client + # in the event a name was not provided. harmony.name is + # usually the ip address but it can be an empty string. + if CONF_NAME not in data or data[CONF_NAME] is None or data[CONF_NAME] == "": + return harmony.name + + return data[CONF_NAME] + + +async def get_harmony_client_if_available(ip_address: str): + """Connect to a harmony hub and fetch info.""" + harmony = HarmonyAPI(ip_address=ip_address) + + try: + if not await harmony.connect(): + await harmony.close() + return None + except harmony_exceptions.TimeOut: + return None + + await harmony.close() + + return harmony + + +def find_matching_config_entries_for_host(hass, host): + """Search existing config entries for one matching the host.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == host: + return entry + return None diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 88103ec94c1452..98d625cbb1d264 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -182,17 +182,11 @@ def __init__(self, session, hive_device): self.session = session self.attributes = {} self._unique_id = f"{self.node_id}-{self.device_type}" - self._unsub_disp = None async def async_added_to_hass(self): """When entity is added to Home Assistant.""" - self._unsub_disp = async_dispatcher_connect( - self.hass, DOMAIN, self.async_write_ha_state + self.async_on_remove( + async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) ) if self.device_type in SERVICES: self.session.entity_lookup[self.entity_id] = self.node_id - - async def async_will_remove_from_hass(self): - """When entity will be removed from hass.""" - self._unsub_disp() - self._unsub_disp = None diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index 52a82184dccf3b..3319ce6bee7bf4 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -164,8 +164,10 @@ async def async_added_to_hass(self): self.handle_event_callback, self._device_port ) self._is_on = await self._client.status(self._device_port) - async_dispatcher_connect( - self.hass, - f"hlk_sw16_device_available_{self._device_id}", - self._availability_callback, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"hlk_sw16_device_available_{self._device_id}", + self._availability_callback, + ) ) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index bbbc6561a878ca..eb8d16d0c0ada2 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,7 +2,7 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.7.0"], + "requirements": ["HAP-python==2.8.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 734568606b21b3..1720c2c58c8e90 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -25,7 +25,7 @@ ) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import HomeAccessory from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, @@ -52,15 +52,6 @@ class Light(HomeAccessory): def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) - self._flag = { - CHAR_ON: False, - CHAR_BRIGHTNESS: False, - CHAR_HUE: False, - CHAR_SATURATION: False, - CHAR_COLOR_TEMPERATURE: False, - RGB_COLOR: False, - } - self._state = 0 self.chars = [] self._features = self.hass.states.get(self.entity_id).attributes.get( @@ -82,17 +73,14 @@ def __init__(self, *args): self.chars.append(CHAR_COLOR_TEMPERATURE) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) - self.char_on = serv_light.configure_char( - CHAR_ON, value=self._state, setter_callback=self.set_state - ) + + self.char_on = serv_light.configure_char(CHAR_ON, value=0) if CHAR_BRIGHTNESS in self.chars: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by update_state # to set to the correct initial value. - self.char_brightness = serv_light.configure_char( - CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness - ) + self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id).attributes.get( @@ -105,133 +93,98 @@ def __init__(self, *args): CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, - setter_callback=self.set_color_temperature, ) if CHAR_HUE in self.chars: - self.char_hue = serv_light.configure_char( - CHAR_HUE, value=0, setter_callback=self.set_hue - ) + self.char_hue = serv_light.configure_char(CHAR_HUE, value=0) if CHAR_SATURATION in self.chars: - self.char_saturation = serv_light.configure_char( - CHAR_SATURATION, value=75, setter_callback=self.set_saturation - ) + self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) - def set_state(self, value): - """Set state if call came from HomeKit.""" - if self._state == value: - return + serv_light.setter_callback = self._set_chars - _LOGGER.debug("%s: Set state to %d", self.entity_id, value) - self._flag[CHAR_ON] = True + def _set_chars(self, char_values): + _LOGGER.debug("_set_chars: %s", char_values) + events = [] + service = SERVICE_TURN_ON params = {ATTR_ENTITY_ID: self.entity_id} - service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF - self.call_service(DOMAIN, service, params) - - @debounce - def set_brightness(self, value): - """Set brightness if call came from HomeKit.""" - _LOGGER.debug("%s: Set brightness to %d", self.entity_id, value) - self._flag[CHAR_BRIGHTNESS] = True - if value == 0: - self.set_state(0) # Turn off light - return - params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} - self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"brightness at {value}%") - - def set_color_temperature(self, value): - """Set color temperature if call came from HomeKit.""" - _LOGGER.debug("%s: Set color temp to %s", self.entity_id, value) - self._flag[CHAR_COLOR_TEMPERATURE] = True - params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} - self.call_service( - DOMAIN, SERVICE_TURN_ON, params, f"color temperature at {value}" - ) + if CHAR_ON in char_values: + if not char_values[CHAR_ON]: + service = SERVICE_TURN_OFF + events.append(f"Set state to {char_values[CHAR_ON]}") + + if CHAR_BRIGHTNESS in char_values: + if char_values[CHAR_BRIGHTNESS] == 0: + events[-1] = f"Set state to 0" + service = SERVICE_TURN_OFF + else: + params[ATTR_BRIGHTNESS_PCT] = char_values[CHAR_BRIGHTNESS] + events.append(f"brightness at {char_values[CHAR_BRIGHTNESS]}%") + + if CHAR_COLOR_TEMPERATURE in char_values: + params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] + events.append(f"color temperature at {char_values[CHAR_COLOR_TEMPERATURE]}") - def set_saturation(self, value): - """Set saturation if call came from HomeKit.""" - _LOGGER.debug("%s: Set saturation to %d", self.entity_id, value) - self._flag[CHAR_SATURATION] = True - self._saturation = value - self.set_color() - - def set_hue(self, value): - """Set hue if call came from HomeKit.""" - _LOGGER.debug("%s: Set hue to %d", self.entity_id, value) - self._flag[CHAR_HUE] = True - self._hue = value - self.set_color() - - def set_color(self): - """Set color if call came from HomeKit.""" if ( self._features & SUPPORT_COLOR - and self._flag[CHAR_HUE] - and self._flag[CHAR_SATURATION] + and CHAR_HUE in char_values + and CHAR_SATURATION in char_values ): - color = (self._hue, self._saturation) + color = (char_values[CHAR_HUE], char_values[CHAR_SATURATION]) _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, color) - self._flag.update( - {CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True} - ) - params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} - self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"set color at {color}") + params[ATTR_HS_COLOR] = color + events.append(f"set color at {color}") + + self.call_service(DOMAIN, service, params, ", ".join(events)) def update_state(self, new_state): """Update light after state change.""" # Handle State state = new_state.state - if state in (STATE_ON, STATE_OFF): - self._state = 1 if state == STATE_ON else 0 - if not self._flag[CHAR_ON] and self.char_on.value != self._state: - self.char_on.set_value(self._state) - self._flag[CHAR_ON] = False + if state == STATE_ON and self.char_on.value != 1: + self.char_on.set_value(1) + elif state == STATE_OFF and self.char_on.value != 0: + self.char_on.set_value(0) # Handle Brightness if CHAR_BRIGHTNESS in self.chars: brightness = new_state.attributes.get(ATTR_BRIGHTNESS) - if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): + if isinstance(brightness, int): brightness = round(brightness / 255 * 100, 0) + # The homeassistant component might report its brightness as 0 but is + # not off. But 0 is a special value in homekit. When you turn on a + # homekit accessory it will try to restore the last brightness state + # which will be the last value saved by char_brightness.set_value. + # But if it is set to 0, HomeKit will update the brightness to 100 as + # it thinks 0 is off. + # + # Therefore, if the the brightness is 0 and the device is still on, + # the brightness is mapped to 1 otherwise the update is ignored in + # order to avoid this incorrect behavior. + if brightness == 0 and state == STATE_ON: + brightness = 1 if self.char_brightness.value != brightness: - # The homeassistant component might report its brightness as 0 but is - # not off. But 0 is a special value in homekit. When you turn on a - # homekit accessory it will try to restore the last brightness state - # which will be the last value saved by char_brightness.set_value. - # But if it is set to 0, HomeKit will update the brightness to 100 as - # it thinks 0 is off. - # - # Therefore, if the the brightness is 0 and the device is still on, - # the brightness is mapped to 1 otherwise the update is ignored in - # order to avoid this incorrect behavior. - if brightness == 0: - if state == STATE_ON: - self.char_brightness.set_value(1) - else: - self.char_brightness.set_value(brightness) - self._flag[CHAR_BRIGHTNESS] = False + self.char_brightness.set_value(brightness) # Handle color temperature if CHAR_COLOR_TEMPERATURE in self.chars: color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) if ( - not self._flag[CHAR_COLOR_TEMPERATURE] - and isinstance(color_temperature, int) + isinstance(color_temperature, int) and self.char_color_temperature.value != color_temperature ): self.char_color_temperature.set_value(color_temperature) - self._flag[CHAR_COLOR_TEMPERATURE] = False # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None)) if ( - not self._flag[RGB_COLOR] - and (hue != self._hue or saturation != self._saturation) - and isinstance(hue, (int, float)) + isinstance(hue, (int, float)) and isinstance(saturation, (int, float)) + and ( + hue != self.char_hue.value + or saturation != self.char_saturation.value + ) ): self.char_hue.set_value(hue) self.char_saturation.set_value(saturation) - self._hue, self._saturation = (hue, saturation) - self._flag[RGB_COLOR] = False diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 4cfb2b0a26d811..db72c87a4a33fa 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -42,7 +42,9 @@ async def async_added_to_hass(self): """Call when entity is added to hass.""" signal = f"homeworks_entity_{self._addr}" _LOGGER.debug("connecting %s", signal) - async_dispatcher_connect(self.hass, signal, self._update_callback) + self.async_on_remove( + async_dispatcher_connect(self.hass, signal, self._update_callback) + ) self._controller.request_dimmer_level(self._addr) @property diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index 1907d9d23ca545..a4ab9123b48a83 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Erste Taste", + "button_2": "Zweite Taste", + "button_3": "Dritte Taste", + "button_4": "Vierte Taste", + "dim_down": "Dimmer runter", + "dim_up": "Dimmer hoch", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", + "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" Taste losgelassen" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json index bc41d3d2df0fc1..6a5074c6e4a2e1 100644 --- a/homeassistant/components/hue/.translations/es.json +++ b/homeassistant/components/hue/.translations/es.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "dim_down": "Bajar la intensidad", + "dim_up": "Subir la intensidad", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", + "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json index 7e837ca5ff9473..8b1c413b2053f1 100644 --- a/homeassistant/components/hue/.translations/ko.json +++ b/homeassistant/components/hue/.translations/ko.json @@ -27,5 +27,22 @@ } }, "title": "\ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\uccab \ubc88\uc9f8 \ubc84\ud2bc", + "button_2": "\ub450 \ubc88\uc9f8 \ubc84\ud2bc", + "button_3": "\uc138 \ubc88\uc9f8 \ubc84\ud2bc", + "button_4": "\ub124 \ubc88\uc9f8 \ubc84\ud2bc", + "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", + "dim_up": "\ubc1d\uac8c \ud558\uae30", + "turn_off": "\ub044\uae30", + "turn_on": "\ucf1c\uae30" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json index 6bbe75a8019b33..0aa75438f7bbbf 100644 --- a/homeassistant/components/hue/.translations/zh-Hant.json +++ b/homeassistant/components/hue/.translations/zh-Hant.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "dim_down": "\u8abf\u6697", + "dim_up": "\u8abf\u4eae", + "turn_off": "\u95dc\u9589", + "turn_on": "\u958b\u555f" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e" + } } } \ No newline at end of file diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 65b7b1f6f6e71d..28b577354d29ea 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -127,8 +127,10 @@ def name(self): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback + ) ) @callback diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 214bdf302ea277..97ddd95f50c22a 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -202,19 +202,13 @@ class AqualinkEntity(Entity): def __init__(self, dev: AqualinkDevice): """Initialize the entity.""" self.dev = dev - self._unsub_disp = None async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" - self._unsub_disp = async_dispatcher_connect( - self.hass, DOMAIN, self.async_write_ha_state + self.async_on_remove( + async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) ) - async def async_will_remove_from_hass(self): - """When entity will be removed from hass.""" - self._unsub_disp() - self._unsub_disp = None - @property def should_poll(self) -> bool: """Return False as entities shouldn't be polled. diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index ceef8acf7c3f27..a824d5f8ee9c0e 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -130,7 +130,7 @@ async def idle_loop(self): try: if await self.connection(): await self.refresh_email_count() - await self.async_update_ha_state() + self.async_write_ha_state() idle = await self._connection.idle_start() await self._connection.wait_server_push() @@ -138,7 +138,7 @@ async def idle_loop(self): with async_timeout.timeout(10): await idle else: - await self.async_update_ha_state() + self.async_write_ha_state() except (AioImapException, asyncio.TimeoutError): self.disconnected() diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index bb11565006148c..cec550d24d95e2 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -83,7 +83,7 @@ class IncomfortChild(IncomfortEntity): async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh)) @callback def _refresh(self) -> None: diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 603aa82612301a..88fe94eac48a01 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -198,12 +198,12 @@ async def async_added_to_hass(self): async def async_turn_on(self, **kwargs): """Turn the entity on.""" self._state = True - await self.async_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" self._state = False - await self.async_update_ha_state() + self.async_write_ha_state() async def async_update_config(self, config: typing.Dict) -> None: """Handle when the config is updated.""" diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index e411cd82045d11..19f3344fb81ea0 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -93,9 +93,13 @@ async def async_added_to_hass(self): self._insteon_device_state.register_updates(self.async_entity_update) self.hass.data[DOMAIN][INSTEON_ENTITIES].add(self.entity_id) load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" - async_dispatcher_connect(self.hass, load_signal, self._load_aldb) + self.async_on_remove( + async_dispatcher_connect(self.hass, load_signal, self._load_aldb) + ) print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" - async_dispatcher_connect(self.hass, print_signal, self._print_aldb) + self.async_on_remove( + async_dispatcher_connect(self.hass, print_signal, self._print_aldb) + ) def _load_aldb(self, reload=False): """Load the device All-Link Database.""" diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index 70a15a0dac51ea..749a3e83217a65 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -73,15 +73,18 @@ def should_poll(self): async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + ) + state = await self.async_get_last_state() if not state: return self._state = state.state - async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update - ) - def update(self): """Get the latest data and update the states.""" data = self._iperf3_data.data.get(self._sensor_type) diff --git a/homeassistant/components/ipp/.translations/ko.json b/homeassistant/components/ipp/.translations/ko.json new file mode 100644 index 00000000000000..ab556519e07812 --- /dev/null +++ b/homeassistant/components/ipp/.translations/ko.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 \ud504\ub9b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "connection_error": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud558\ub824\uba74 \uc5f0\uacb0\uc744 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4." + }, + "error": { + "connection_error": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. SSL/TLS \uc635\uc158\uc744 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "flow_title": "\ud504\ub9b0\ud130: {name}", + "step": { + "user": { + "data": { + "base_path": "\ud504\ub9b0\ud130\uc758 \uc0c1\ub300 \uacbd\ub85c", + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8", + "ssl": "\ud504\ub9b0\ud130\ub294 SSL/TLS \ub97c \ud1b5\ud55c \ud1b5\uc2e0\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4", + "verify_ssl": "\ud504\ub9b0\ud130\ub294 \uc62c\ubc14\ub978 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4" + }, + "description": "\uc778\ud130\ub137 \uc778\uc1c4 \ud504\ub85c\ud1a0\ucf5c (IPP) \ub97c \ud1b5\ud574 \ud504\ub9b0\ud130\ub97c \uc124\uc815\ud558\uc5ec Home Assistant \uc640 \uc5f0\ub3d9\ud569\ub2c8\ub2e4.", + "title": "\ud504\ub9b0\ud130 \uc5f0\uacb0" + }, + "zeroconf_confirm": { + "description": "Home Assistant \uc5d0 `{name}` \ud504\ub9b0\ud130\ub97c \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c \ud504\ub9b0\ud130" + } + }, + "title": "\uc778\ud130\ub137 \uc778\uc1c4 \ud504\ub85c\ud1a0\ucf5c (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index beb6679e308d2c..0cb788eeee7f88 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -2,7 +2,7 @@ "domain": "ipp", "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", - "requirements": ["pyipp==0.8.1"], + "requirements": ["pyipp==0.8.3"], "dependencies": [], "codeowners": ["@ctalkington"], "config_flow": true, diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index 1de1a4bd6c5b05..ae5df387884fb5 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -113,6 +113,8 @@ def device_state_attributes(self): async def async_added_to_hass(self): """Register callback.""" - async_dispatcher_connect( - self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + ) ) diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index e86d6f7d836443..d9500c7a00064d 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -88,6 +88,8 @@ def unit_of_measurement(self): async def async_added_to_hass(self): """Register callback.""" - async_dispatcher_connect( - self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + ) ) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 94a171d9c2a75e..95e7e2cc400f7a 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -116,7 +116,7 @@ def async_register_callbacks(self): async def after_update_callback(device): """Call after device was updated.""" - await self.async_update_ha_state() + self.async_write_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 554ae59f3972c4..919a20d0e4c756 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -210,7 +210,7 @@ async def async_added_to_hass(self) -> None: async def after_update_callback(device): """Call after device was updated.""" - await self.async_update_ha_state() + self.async_write_ha_state() self.device.register_device_updated_cb(after_update_callback) self.device.mode.register_device_updated_cb(after_update_callback) @@ -266,7 +266,7 @@ async def async_set_temperature(self, **kwargs) -> None: if temperature is None: return await self.device.set_target_temperature(temperature) - await self.async_update_ha_state() + self.async_write_ha_state() @property def hvac_mode(self) -> Optional[str]: @@ -304,7 +304,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: elif self.device.mode.supports_operation_mode: knx_operation_mode = HVACOperationMode(OPERATION_MODES_INV.get(hvac_mode)) await self.device.mode.set_operation_mode(knx_operation_mode) - await self.async_update_ha_state() + self.async_write_ha_state() @property def preset_mode(self) -> Optional[str]: @@ -334,4 +334,4 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: if self.device.mode.supports_operation_mode: knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) await self.device.mode.set_operation_mode(knx_operation_mode) - await self.async_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 3c67e2fd55895c..5c4aa762b5c235 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -108,7 +108,7 @@ def async_register_callbacks(self): async def after_update_callback(device): """Call after device was updated.""" - await self.async_update_ha_state() + self.async_write_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 6eb539c19ce41b..efd1a74f5a2ec9 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -154,7 +154,7 @@ def async_register_callbacks(self): async def after_update_callback(device): """Call after device was updated.""" - await self.async_update_ha_state() + self.async_write_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index a0a0f6ea18d016..2679170b03dd8d 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -69,7 +69,7 @@ def async_register_callbacks(self): async def after_update_callback(device): """Call after device was updated.""" - await self.async_update_ha_state() + self.async_write_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index e9a0df5c983688..ae798bf4c08237 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -65,7 +65,7 @@ def async_register_callbacks(self): async def after_update_callback(device): """Call after device was updated.""" - await self.async_update_ha_state() + self.async_write_ha_state() self.device.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/konnected/.translations/ko.json b/homeassistant/components/konnected/.translations/ko.json index 0c5e213ea0d10f..34dd01d06b62de 100644 --- a/homeassistant/components/konnected/.translations/ko.json +++ b/homeassistant/components/konnected/.translations/ko.json @@ -33,6 +33,9 @@ "abort": { "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" }, + "error": { + "bad_host": "API \ud638\uc2a4\ud2b8 URL \uc7ac\uc815\uc758\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "step": { "options_binary": { "data": { @@ -82,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "\uc0c1\ud0dc \ubcc0\uacbd\uc744 \ubcf4\ub0bc \ub54c \uae5c\ubc15\uc784 \ud328\ub110 LED \ub97c \ucf2d\ub2c8\ub2e4" + "api_host": "API \ud638\uc2a4\ud2b8 URL \uc7ac\uc815\uc758 (\uc120\ud0dd \uc0ac\ud56d)", + "blink": "\uc0c1\ud0dc \ubcc0\uacbd\uc744 \ubcf4\ub0bc \ub54c \uae5c\ubc15\uc784 \ud328\ub110 LED \ub97c \ucf2d\ub2c8\ub2e4", + "override_api_host": "\uae30\ubcf8 Home Assistant API \ud638\uc2a4\ud2b8 \ud328\ub110 URL \uc7ac\uc815\uc758" }, "description": "\ud328\ub110\uc5d0 \uc6d0\ud558\ub294 \ub3d9\uc791\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "title": "\uae30\ud0c0 \uad6c\uc131" @@ -91,11 +96,12 @@ "data": { "activation": "\uc2a4\uc704\uce58\uac00 \ucf1c\uc9c8 \ub54c \ucd9c\ub825", "momentary": "\ud384\uc2a4 \uc9c0\uc18d\uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", + "more_states": "\uc774 \uad6c\uc5ed\uc5d0 \ub300\ud55c \ucd94\uac00 \uc0c1\ud0dc \uad6c\uc131", "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", "pause": "\ud384\uc2a4 \uac04 \uc77c\uc2dc\uc815\uc9c0 \uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", "repeat": "\ubc18\ubcf5 \uc2dc\uac04 (-1 = \ubb34\ud55c) (\uc120\ud0dd \uc0ac\ud56d)" }, - "description": "{zone} \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "description": "{zone} \uad6c\uc5ed\uc5d0 \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694: \uc0c1\ud0dc {state}", "title": "\uc2a4\uc704\uce58 \ucd9c\ub825 \uad6c\uc131" } }, diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index f2f79f5ed7d6ba..5cd270d500833e 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -79,8 +79,10 @@ def device_info(self): async def async_added_to_hass(self): """Store entity_id and register state change callback.""" self._data[ATTR_ENTITY_ID] = self.entity_id - async_dispatcher_connect( - self.hass, f"konnected.{self.entity_id}.update", self.async_set_state + self.async_on_remove( + async_dispatcher_connect( + self.hass, f"konnected.{self.entity_id}.update", self.async_set_state + ) ) @callback diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index ba831c3a1a92bc..61ae05fa010d8f 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -74,21 +74,21 @@ async def async_close_cover(self, **kwargs): state = pypck.lcn_defs.MotorStateModifier.DOWN self.address_connection.control_motors_outputs(state) - await self.async_update_ha_state() + self.async_write_ha_state() async def async_open_cover(self, **kwargs): """Open the cover.""" self._closed = False state = pypck.lcn_defs.MotorStateModifier.UP self.address_connection.control_motors_outputs(state, self.reverse_time) - await self.async_update_ha_state() + self.async_write_ha_state() async def async_stop_cover(self, **kwargs): """Stop the cover.""" self._closed = None state = pypck.lcn_defs.MotorStateModifier.STOP self.address_connection.control_motors_outputs(state, self.reverse_time) - await self.async_update_ha_state() + self.async_write_ha_state() def input_received(self, input_obj): """Set cover states when LCN input object (command) is received.""" @@ -140,7 +140,7 @@ async def async_close_cover(self, **kwargs): states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.DOWN self.address_connection.control_motors_relays(states) - await self.async_update_ha_state() + self.async_write_ha_state() async def async_open_cover(self, **kwargs): """Open the cover.""" @@ -148,7 +148,7 @@ async def async_open_cover(self, **kwargs): states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP self.address_connection.control_motors_relays(states) - await self.async_update_ha_state() + self.async_write_ha_state() async def async_stop_cover(self, **kwargs): """Stop the cover.""" @@ -156,7 +156,7 @@ async def async_stop_cover(self, **kwargs): states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.STOP self.address_connection.control_motors_relays(states) - await self.async_update_ha_state() + self.async_write_ha_state() def input_received(self, input_obj): """Set cover states when LCN input object (command) is received.""" diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 40a592f89c92b4..7f1cd547c02c93 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -102,7 +102,7 @@ async def async_turn_on(self, **kwargs): transition = self._transition self.address_connection.dim_output(self.output.value, percent, transition) - await self.async_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" @@ -117,7 +117,7 @@ async def async_turn_off(self, **kwargs): self._is_dimming_to_zero = bool(transition) self.address_connection.dim_output(self.output.value, 0, transition) - await self.async_update_ha_state() + self.async_write_ha_state() def input_received(self, input_obj): """Set light state when LCN input object (command) is received.""" @@ -164,7 +164,7 @@ async def async_turn_on(self, **kwargs): states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON self.address_connection.control_relays(states) - await self.async_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" @@ -174,7 +174,7 @@ async def async_turn_off(self, **kwargs): states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF self.address_connection.control_relays(states) - await self.async_update_ha_state() + self.async_write_ha_state() def input_received(self, input_obj): """Set light state when LCN input object (command) is received.""" diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index b0e16e412b325d..a2adda95b3b0ef 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -59,13 +59,13 @@ async def async_turn_on(self, **kwargs): """Turn the entity on.""" self._is_on = True self.address_connection.dim_output(self.output.value, 100, 0) - await self.async_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" self._is_on = False self.address_connection.dim_output(self.output.value, 0, 0) - await self.async_update_ha_state() + self.async_write_ha_state() def input_received(self, input_obj): """Set switch state when LCN input object (command) is received.""" @@ -107,7 +107,7 @@ async def async_turn_on(self, **kwargs): states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON self.address_connection.control_relays(states) - await self.async_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" @@ -116,7 +116,7 @@ async def async_turn_off(self, **kwargs): states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF self.address_connection.control_relays(states) - await self.async_update_ha_state() + self.async_write_ha_state() def input_received(self, input_obj): """Set switch state when LCN input object (command) is received.""" diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 5bc0c1bc53b1ac..e5bbe88edfb846 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -435,7 +435,7 @@ def unregister(self, bulb): entity = self.entities[bulb.mac_addr] _LOGGER.debug("%s unregister", entity.who) entity.registered = False - self.hass.async_create_task(entity.async_update_ha_state()) + entity.async_write_ha_state() class AwaitAioLIFX: @@ -573,7 +573,7 @@ async def update_hass(self, now=None): """Request new status and push it to hass.""" self.postponed_update = None await self.async_update() - await self.async_update_ha_state() + self.async_write_ha_state() async def update_during_transition(self, when): """Update state at the start and end of a transition.""" diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 59a4b0306d08ef..9a6b22b51a2db5 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -64,7 +64,10 @@ ATTR_TRANSITION, ] -DEPRECATION_WARNING = "The use of other attributes than device state attributes is deprecated and will be removed in a future release. Read the logs for further details: https://www.home-assistant.io/integrations/scene/" +DEPRECATION_WARNING = ( + "The use of other attributes than device state attributes is deprecated and will be removed in a future release. " + "Invalid attributes are %s. Read the logs for further details: https://www.home-assistant.io/integrations/scene/" +) async def _async_reproduce_state( @@ -84,8 +87,9 @@ async def _async_reproduce_state( return # Warn if deprecated attributes are used - if any(attr in DEPRECATED_GROUP for attr in state.attributes): - _LOGGER.warning(DEPRECATION_WARNING) + deprecated_attrs = [attr for attr in state.attributes if attr in DEPRECATED_GROUP] + if deprecated_attrs: + _LOGGER.warning(DEPRECATION_WARNING, deprecated_attrs) # Return if we are already at the right state. if cur_state.state == state.state and all( diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index 13fa67a8b6b300..e6e9110b33a591 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -3,7 +3,7 @@ "name": "Luftdaten", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/luftdaten", - "requirements": ["luftdaten==0.6.3"], + "requirements": ["luftdaten==0.6.4"], "dependencies": [], "codeowners": ["@fabaff"], "quality_scale": "gold" diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index c6430546bf23c7..b73b749cddf368 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -2,7 +2,7 @@ "domain": "mastodon", "name": "Mastodon", "documentation": "https://www.home-assistant.io/integrations/mastodon", - "requirements": ["Mastodon.py==1.5.0"], + "requirements": ["Mastodon.py==1.5.1"], "dependencies": [], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 8db9cb6fa37c21..dd67cc287832e8 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -178,7 +178,11 @@ async def async_notify_received(notify): self._available = True self.async_write_ha_state() - async_dispatcher_connect(self.hass, SIGNAL_STB_NOTIFY, async_notify_received) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_STB_NOTIFY, async_notify_received + ) + ) async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index c661b1a59ade6c..df3d9e893923cc 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -11,6 +11,7 @@ PROPERTY_ZONE_2_OPERATION_MODE, Zone, ) +import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -27,11 +28,23 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.temperature import convert as convert_temperature from . import MelCloudDevice -from .const import ATTR_STATUS, DOMAIN, TEMP_UNIT_LOOKUP +from .const import ( + ATTR_STATUS, + ATTR_VANE_HORIZONTAL, + ATTR_VANE_HORIZONTAL_POSITIONS, + ATTR_VANE_VERTICAL, + ATTR_VANE_VERTICAL_POSITIONS, + CONF_POSITION, + DOMAIN, + SERVICE_SET_VANE_HORIZONTAL, + SERVICE_SET_VANE_VERTICAL, + TEMP_UNIT_LOOKUP, +) SCAN_INTERVAL = timedelta(seconds=60) @@ -73,6 +86,18 @@ async def async_setup_entry( True, ) + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_SET_VANE_HORIZONTAL, + {vol.Required(CONF_POSITION): cv.string}, + "async_set_vane_horizontal", + ) + platform.async_register_entity_service( + SERVICE_SET_VANE_VERTICAL, + {vol.Required(CONF_POSITION): cv.string}, + "async_set_vane_vertical", + ) + class MelCloudClimate(ClimateDevice): """Base climate device.""" @@ -116,6 +141,30 @@ def name(self): """Return the display name of this entity.""" return self._name + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the optional state attributes with device specific additions.""" + attr = {} + + vane_horizontal = self._device.vane_horizontal + if vane_horizontal: + attr.update( + { + ATTR_VANE_HORIZONTAL: vane_horizontal, + ATTR_VANE_HORIZONTAL_POSITIONS: self._device.vane_horizontal_positions, + } + ) + + vane_vertical = self._device.vane_vertical + if vane_horizontal: + attr.update( + { + ATTR_VANE_VERTICAL: vane_vertical, + ATTR_VANE_VERTICAL_POSITIONS: self._device.vane_vertical_positions, + } + ) + return attr + @property def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" @@ -181,6 +230,22 @@ def fan_modes(self) -> Optional[List[str]]: """Return the list of available fan modes.""" return self._device.fan_speeds + async def async_set_vane_horizontal(self, position: str) -> None: + """Set horizontal vane position.""" + if position not in self._device.vane_horizontal_positions: + raise ValueError( + f"Invalid horizontal vane position {position}. Valid positions: [{self._device.vane_horizontal_positions}]." + ) + await self._device.set({ata.PROPERTY_VANE_HORIZONTAL: position}) + + async def async_set_vane_vertical(self, position: str) -> None: + """Set vertical vane position.""" + if position not in self._device.vane_vertical_positions: + raise ValueError( + f"Invalid vertical vane position {position}. Valid positions: [{self._device.vane_vertical_positions}]." + ) + await self._device.set({ata.PROPERTY_VANE_VERTICAL: position}) + @property def supported_features(self) -> int: """Return the list of supported features.""" diff --git a/homeassistant/components/melcloud/const.py b/homeassistant/components/melcloud/const.py index c6ce4391294a43..d58f483d441eee 100644 --- a/homeassistant/components/melcloud/const.py +++ b/homeassistant/components/melcloud/const.py @@ -5,7 +5,16 @@ DOMAIN = "melcloud" +CONF_POSITION = "position" + ATTR_STATUS = "status" +ATTR_VANE_HORIZONTAL = "vane_horizontal" +ATTR_VANE_HORIZONTAL_POSITIONS = "vane_horizontal_positions" +ATTR_VANE_VERTICAL = "vane_vertical" +ATTR_VANE_VERTICAL_POSITIONS = "vane_vertical_positions" + +SERVICE_SET_VANE_HORIZONTAL = "set_vane_horizontal" +SERVICE_SET_VANE_VERTICAL = "set_vane_vertical" TEMP_UNIT_LOOKUP = { UNIT_TEMP_CELSIUS: TEMP_CELSIUS, diff --git a/homeassistant/components/melcloud/services.yaml b/homeassistant/components/melcloud/services.yaml new file mode 100644 index 00000000000000..40faa097d9b6d4 --- /dev/null +++ b/homeassistant/components/melcloud/services.yaml @@ -0,0 +1,23 @@ +set_vane_horizontal: + description: Sets horizontal vane position. + fields: + entity_id: + description: Name of the target entity + example: "climate.ac_1" + position: + description: > + Horizontal vane position. Possible options can be found in the + vane_horizontal_positions state attribute. + example: "auto" + +set_vane_vertical: + description: Sets vertical vane position. + fields: + entity_id: + description: Name of the target entity + example: "climate.ac_1" + position: + description: > + Vertical vane position. Possible options can be found in the + vane_vertical_positions state attribute. + example: "auto" diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 1bc9c116cda036..16b25e4f85de46 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -96,7 +96,7 @@ async def async_create_group(service): face.store[g_id] = {} entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name) - await entities[g_id].async_update_ha_state() + entities[g_id].async_write_ha_state() except HomeAssistantError as err: _LOGGER.error("Can't create group '%s' with error: %s", g_id, err) @@ -145,7 +145,7 @@ async def async_create_person(service): ) face.store[g_id][name] = user_data["personId"] - await entities[g_id].async_update_ha_state() + entities[g_id].async_write_ha_state() except HomeAssistantError as err: _LOGGER.error("Can't create person '%s' with error: %s", name, err) @@ -163,7 +163,7 @@ async def async_delete_person(service): await face.call_api("delete", f"persongroups/{g_id}/persons/{p_id}") face.store[g_id].pop(name) - await entities[g_id].async_update_ha_state() + entities[g_id].async_write_ha_state() except HomeAssistantError as err: _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index ab0409694d1b12..c42901cd6c5a6b 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -122,10 +122,10 @@ async def async_camera_image(self): return image except asyncio.TimeoutError: - _LOGGER.error("Timeout getting camera image") + _LOGGER.error("Timeout getting camera image from %s", self._name) except aiohttp.ClientError as err: - _LOGGER.error("Error getting new camera image: %s", err) + _LOGGER.error("Error getting new camera image from %s: %s", self._name, err) def camera_image(self): """Return a still image response from the camera.""" diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index bc59be0d1f3e8d..734f67906ceb10 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1162,7 +1162,7 @@ def available(self) -> bool: async def cleanup_device_registry(hass, device_id): - """Remove device registry entry if there are no entities or triggers.""" + """Remove device registry entry if there are no remaining entities or triggers.""" # Local import to avoid circular dependencies from . import device_trigger @@ -1196,8 +1196,12 @@ async def async_added_to_hass(self) -> None: self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None ) - async def async_remove_from_registry(self) -> None: - """Remove entity from entity registry.""" + async def _async_remove_state_and_registry_entry(self) -> None: + """Remove entity's state and entity registry entry. + + Remove entity from entity registry if it is registered, this also removes the state. + If the entity is not in the entity registry, just remove the state. + """ entity_registry = ( await self.hass.helpers.entity_registry.async_get_registry() ) @@ -1205,6 +1209,8 @@ async def async_remove_from_registry(self) -> None: entity_entry = entity_registry.async_get(self.entity_id) entity_registry.async_remove(self.entity_id) await cleanup_device_registry(self.hass, entity_entry.device_id) + else: + await self.async_remove() @callback async def discovery_callback(payload): @@ -1216,9 +1222,8 @@ async def discovery_callback(payload): if not payload: # Empty payload: Remove component _LOGGER.info("Removing component: %s", self.entity_id) - self._cleanup_on_remove() - await async_remove_from_registry(self) - await self.async_remove() + self._cleanup_discovery_on_remove() + await _async_remove_state_and_registry_entry(self) elif self._discovery_update: # Non-empty payload: Notify component _LOGGER.info("Updating component: %s", self.entity_id) @@ -1246,9 +1251,9 @@ async def async_removed_from_registry(self) -> None: async def async_will_remove_from_hass(self) -> None: """Stop listening to signal and cleanup discovery data..""" - self._cleanup_on_remove() + self._cleanup_discovery_on_remove() - def _cleanup_on_remove(self) -> None: + def _cleanup_discovery_on_remove(self) -> None: """Stop listening to signal and cleanup discovery data.""" if self._discovery_data and not self._removed_from_hass: debug_info.remove_entity_data(self.hass, self.entity_id) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index cbaf8d43a77e3f..254d841aebcda8 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -36,12 +36,7 @@ SUPPORT_STOP, StateVacuumDevice, ) -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, - CONF_DEVICE, - CONF_NAME, - CONF_VALUE_TEMPLATE, -) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_DEVICE, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -104,7 +99,6 @@ CONF_PAYLOAD_LOCATE = "payload_locate" CONF_PAYLOAD_START = "payload_start" CONF_PAYLOAD_PAUSE = "payload_pause" -CONF_STATE_TEMPLATE = "state_template" CONF_SET_FAN_SPEED_TOPIC = "set_fan_speed_topic" CONF_FAN_SPEED_LIST = "fan_speed_list" CONF_SEND_COMMAND_TOPIC = "send_command_topic" @@ -141,7 +135,6 @@ vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_publish_topic, vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS @@ -241,20 +234,13 @@ async def async_will_remove_from_hass(self): async def _subscribe_topics(self): """(Re)Subscribe to topics.""" - template = self._config.get(CONF_VALUE_TEMPLATE) - if template is not None: - template.hass = self.hass topics = {} @callback @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle state MQTT message.""" - payload = msg.payload - if template is not None: - payload = template.async_render_with_possible_json_value(payload) - else: - payload = json.loads(payload) + payload = json.loads(msg.payload) if STATE in payload and payload[STATE] in POSSIBLE_STATES: self._state = POSSIBLE_STATES[payload[STATE]] del payload[STATE] diff --git a/homeassistant/components/mychevy/binary_sensor.py b/homeassistant/components/mychevy/binary_sensor.py index 822a7988d0d252..702f3146f8e407 100644 --- a/homeassistant/components/mychevy/binary_sensor.py +++ b/homeassistant/components/mychevy/binary_sensor.py @@ -64,8 +64,10 @@ def _car(self): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_update_callback + ) ) @callback diff --git a/homeassistant/components/mychevy/sensor.py b/homeassistant/components/mychevy/sensor.py index 9f8ea5607d9dcd..96e0eef68ad21b 100644 --- a/homeassistant/components/mychevy/sensor.py +++ b/homeassistant/components/mychevy/sensor.py @@ -58,11 +58,17 @@ def __init__(self): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.success + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.success + ) ) - self.hass.helpers.dispatcher.async_dispatcher_connect(ERROR_TOPIC, self.error) + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + ERROR_TOPIC, self.error + ) + ) @callback def success(self): diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index e5853fce5ca7f1..9c1c4b54367646 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -137,11 +137,15 @@ async def async_added_to_hass(self): """Register update callback.""" gateway_id = id(self.gateway) dev_id = gateway_id, self.node_id, self.child_id, self.value_type - async_dispatcher_connect( - self.hass, CHILD_CALLBACK.format(*dev_id), self.async_update_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, CHILD_CALLBACK.format(*dev_id), self.async_update_callback + ) ) - async_dispatcher_connect( - self.hass, - NODE_CALLBACK.format(gateway_id, self.node_id), - self.async_update_callback, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + NODE_CALLBACK.format(gateway_id, self.node_id), + self.async_update_callback, + ) ) diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index 8b7867fdc062de..8181e54640d3bf 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -45,8 +45,10 @@ def __init__(self, client, name): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_ARMING_STATE_CHANGED, self._handle_arming_state_change + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_ARMING_STATE_CHANGED, self._handle_arming_state_change + ) ) @property diff --git a/homeassistant/components/ness_alarm/binary_sensor.py b/homeassistant/components/ness_alarm/binary_sensor.py index 69acc97130d2e5..c719febdb58f07 100644 --- a/homeassistant/components/ness_alarm/binary_sensor.py +++ b/homeassistant/components/ness_alarm/binary_sensor.py @@ -50,8 +50,10 @@ def __init__(self, zone_id, name, zone_type): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_ZONE_CHANGED, self._handle_zone_change + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_CHANGED, self._handle_zone_change + ) ) @property diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 73a28aa121f289..b486e907ee3b16 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -437,4 +437,6 @@ async def async_update_state(): """Update sensor state.""" await self.async_update_ha_state(True) - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) + self.async_on_remove( + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) + ) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index f75e3a692f3112..92442479091d81 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -151,7 +151,9 @@ async def async_update_state(): """Update device state.""" await self.async_update_ha_state(True) - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) + self.async_on_remove( + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) + ) @property def supported_features(self): diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1f1b7088b295af..fe6526a16eb0a7 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -56,6 +56,7 @@ STATE_NETATMO_AWAY = PRESET_AWAY STATE_NETATMO_OFF = STATE_OFF STATE_NETATMO_MANUAL = "manual" +STATE_NETATMO_HOME = "home" PRESET_MAP_NETATMO = { PRESET_FROST_GUARD: STATE_NETATMO_HG, @@ -173,8 +174,11 @@ def __init__(self, data, room_id): self._support_flags = SUPPORT_FLAGS self._hvac_mode = None self._battery_level = None + self._connected = None self.update_without_throttle = False - self._module_type = self._data.room_status.get(room_id, {}).get("module_type") + self._module_type = self._data.room_status.get(room_id, {}).get( + "module_type", NA_VALVE + ) if self._module_type == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) @@ -252,25 +256,20 @@ def hvac_action(self) -> Optional[str]: def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - mode = None - if hvac_mode == HVAC_MODE_OFF: - mode = STATE_NETATMO_OFF + self.turn_off() elif hvac_mode == HVAC_MODE_AUTO: - mode = PRESET_SCHEDULE + if self.hvac_mode == HVAC_MODE_OFF: + self.turn_on() + self.set_preset_mode(PRESET_SCHEDULE) elif hvac_mode == HVAC_MODE_HEAT: - mode = PRESET_BOOST - - self.set_preset_mode(mode) + self.set_preset_mode(PRESET_BOOST) def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if self.target_temperature == 0: self._data.homestatus.setroomThermpoint( - self._data.home_id, - self._room_id, - STATE_NETATMO_MANUAL, - DEFAULT_MIN_TEMP, + self._data.home_id, self._room_id, STATE_NETATMO_HOME, ) if ( @@ -283,7 +282,7 @@ def set_preset_mode(self, preset_mode: str) -> None: STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) - elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX, STATE_NETATMO_OFF]: + elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]: self._data.homestatus.setroomThermpoint( self._data.home_id, self._room_id, PRESET_MAP_NETATMO[preset_mode] ) @@ -293,6 +292,7 @@ def set_preset_mode(self, preset_mode: str) -> None: ) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) + self.update_without_throttle = True self.schedule_update_ha_state() @@ -328,6 +328,35 @@ def device_state_attributes(self): return attr + def turn_off(self): + """Turn the entity off.""" + if self._module_type == NA_VALVE: + self._data.homestatus.setroomThermpoint( + self._data.home_id, + self._room_id, + STATE_NETATMO_MANUAL, + DEFAULT_MIN_TEMP, + ) + elif self.hvac_mode != HVAC_MODE_OFF: + self._data.homestatus.setroomThermpoint( + self._data.home_id, self._room_id, STATE_NETATMO_OFF + ) + self.update_without_throttle = True + self.schedule_update_ha_state() + + def turn_on(self): + """Turn the entity on.""" + self._data.homestatus.setroomThermpoint( + self._data.home_id, self._room_id, STATE_NETATMO_HOME + ) + self.update_without_throttle = True + self.schedule_update_ha_state() + + @property + def available(self) -> bool: + """If the device hasn't been able to connect, mark as unavailable.""" + return bool(self._connected) + def update(self): """Get the latest data from NetAtmo API and updates the states.""" try: @@ -355,12 +384,14 @@ def update(self): self._battery_level = self._data.room_status[self._room_id].get( "battery_level" ) + self._connected = True except KeyError as err: - _LOGGER.error( + _LOGGER.debug( "The thermostat in room %s seems to be out of reach. (%s)", self._room_name, err, ) + self._connected = False self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index ac36cc1eb44dee..aedfe9018f7c44 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -352,8 +352,10 @@ def _init_unique_id(self): async def async_added_to_hass(self): """Register callback.""" - async_dispatcher_connect( - self.hass, DISPATCHER_NETGEAR_LTE, self.async_write_ha_state + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCHER_NETGEAR_LTE, self.async_write_ha_state + ) ) async def async_update(self): diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 4c9b6343f2b905..6baa3a63f9fad4 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -92,7 +92,6 @@ class NetioApiView(HomeAssistantView): @callback def get(self, request, host): """Request handler.""" - hass = request.app["hass"] data = request.query states, consumptions, cumulated_consumptions, start_dates = [], [], [], [] @@ -121,7 +120,7 @@ def get(self, request, host): ndev.start_dates = start_dates for dev in DEVICES[host].entities: - hass.async_create_task(dev.async_update_ha_state()) + dev.async_write_ha_state() return self.json(True) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 57b9bdb61fa6a0..f5f23f2f114043 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -467,8 +467,10 @@ def device_state_attributes(self): async def async_added_to_hass(self): """Register callbacks.""" self.log_registration() - async_dispatcher_connect( - self.car.hass, SIGNAL_UPDATE_LEAF, self._update_callback + self.async_on_remove( + async_dispatcher_connect( + self.car.hass, SIGNAL_UPDATE_LEAF, self._update_callback + ) ) @callback diff --git a/homeassistant/components/nut/.translations/ko.json b/homeassistant/components/nut/.translations/ko.json new file mode 100644 index 00000000000000..f9fa46b6667beb --- /dev/null +++ b/homeassistant/components/nut/.translations/ko.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "alias": "\ubcc4\uba85", + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "resources": "\ub9ac\uc18c\uc2a4", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "NUT \uc11c\ubc84\uc5d0 UPS \uac00 \uc5ec\ub7ec \uac1c \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294 \uacbd\uc6b0 '\ubcc4\uba85' \uc785\ub825\ub780\uc5d0 \uc870\ud68c\ud560 UPS \uc774\ub984\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "NUT \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "\ub124\ud2b8\uc6cc\ud06c UPS \ub3c4\uad6c (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "\ub9ac\uc18c\uc2a4" + }, + "description": "\uc13c\uc11c \ub9ac\uc18c\uc2a4 \uc120\ud0dd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 89d2c1c01da4e0..a0f1dc57c94bda 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -89,8 +89,10 @@ def available(self): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update + self.async_on_remove( + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) ) @callback diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index b52641105e4445..dc1b943686f252 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -2,6 +2,7 @@ import asyncio import pyotgw +from pyotgw import vars as gw_vars from serial import SerialException import voluptuous as vol @@ -53,7 +54,7 @@ async def test_connection(): otgw = pyotgw.pyotgw() status = await otgw.connect(self.hass.loop, device) await otgw.disconnect() - return status.get(pyotgw.OTGW_ABOUT) + return status.get(gw_vars.OTGW_ABOUT) try: res = await asyncio.wait_for(test_connection(), timeout=10) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 81a41b8fb47b04..d0cbb4351c73b2 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==0.5b1"], + "requirements": ["pyotgw==0.6b1"], "dependencies": [], "codeowners": ["@mvn23"], "config_flow": true diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 42f1f62d10a965..8814c8968a0320 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -314,7 +314,7 @@ async def async_handle_waypoint(hass, name_base, waypoint): ) zone.hass = hass zone.entity_id = entity_id - await zone.async_update_ha_state() + zone.async_write_ha_state() @HANDLERS.register("waypoint") diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index 34a2a1a42b669e..07b0453fca69fb 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -157,6 +157,8 @@ def should_poll(self): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - f"{PLAATO_DOMAIN}_{self.unique_id}", self.async_schedule_update_ha_state + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + f"{PLAATO_DOMAIN}_{self.unique_id}", self.async_write_ha_state + ) ) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index d5cb3db3aba745..44bb25b3fd9844 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -38,3 +38,6 @@ X_PLEX_PLATFORM = "Home Assistant" X_PLEX_PRODUCT = "Home Assistant" X_PLEX_VERSION = __version__ + +COMMAND_MEDIA_TYPE_MUSIC = "music" +COMMAND_MEDIA_TYPE_VIDEO = "video" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1be06876baf0ca..aea8ecadaff2aa 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -27,6 +27,8 @@ from homeassistant.util import dt as dt_util from .const import ( + COMMAND_MEDIA_TYPE_MUSIC, + COMMAND_MEDIA_TYPE_VIDEO, COMMON_PLAYERS, CONF_SERVER_IDENTIFIER, DISPATCHERS, @@ -575,9 +577,11 @@ def play_media(self, media_type, media_id, **kwargs): shuffle = src.get("shuffle", 0) media = None + command_media_type = COMMAND_MEDIA_TYPE_VIDEO if media_type == "MUSIC": media = self._get_music_media(library, src) + command_media_type = COMMAND_MEDIA_TYPE_MUSIC elif media_type == "EPISODE": media = self._get_tv_media(library, src) elif media_type == "PLAYLIST": @@ -591,7 +595,7 @@ def play_media(self, media_type, media_id, **kwargs): playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) try: - self.device.playMedia(playqueue) + self.device.playMedia(playqueue, type=command_media_type) except ParseError: # Temporary workaround for Plexamp / plexapi issue pass diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index c2d938f6ed77f2..6ad030078b1177 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -91,8 +91,10 @@ def update_packet(self, packet): async def async_added_to_hass(self): """Listen for updates from QSUSb via dispatcher.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - self.qsid, self.update_packet + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + self.qsid, self.update_packet + ) ) diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 2c439407c71d92..587cd85a2a5001 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -35,6 +35,7 @@ KEY_ZONE_NUMBER = "zoneNumber" KEY_ZONES = "zones" KEY_SCHEDULES = "scheduleRules" +KEY_FLEX_SCHEDULES = "flexScheduleRules" KEY_SCHEDULE_ID = "scheduleId" KEY_CUSTOM_SHADE = "customShade" KEY_CUSTOM_CROP = "customCrop" diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index fbf49ffc67f123..7ff47f7a221544 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -9,6 +9,7 @@ KEY_DEVICES, KEY_ENABLED, KEY_EXTERNAL_ID, + KEY_FLEX_SCHEDULES, KEY_ID, KEY_MAC_ADDRESS, KEY_MODEL, @@ -92,6 +93,7 @@ def __init__(self, hass, rachio, data, webhooks): self.model = data[KEY_MODEL] self._zones = data[KEY_ZONES] self._schedules = data[KEY_SCHEDULES] + self._flex_schedules = data[KEY_FLEX_SCHEDULES] self._init_data = data self._webhooks = webhooks _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) @@ -177,9 +179,13 @@ def get_zone(self, zone_id) -> Optional[dict]: return None def list_schedules(self) -> list: - """Return a list of schedules.""" + """Return a list of fixed schedules.""" return self._schedules + def list_flex_schedules(self) -> list: + """Return a list of flex schedules.""" + return self._flex_schedules + def stop_watering(self) -> None: """Stop watering all zones connected to this controller.""" self.rachio.device.stopWater(self.controller_id) diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index a4ba1a41fee0b0..2b9a959deb2c9e 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -4,6 +4,7 @@ import logging from homeassistant.components.switch import SwitchDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( @@ -68,13 +69,13 @@ def _create_entities(hass, config_entry): entities.append(RachioStandbySwitch(controller)) zones = controller.list_zones() schedules = controller.list_schedules() + flex_schedules = controller.list_flex_schedules() current_schedule = controller.current_schedule for zone in zones: - _LOGGER.debug("Rachio setting up zone: %s", zone) entities.append(RachioZone(person, controller, zone, current_schedule)) - for sched in schedules: - _LOGGER.debug("Added schedule: %s", sched) + for sched in schedules + flex_schedules: entities.append(RachioSchedule(person, controller, sched, current_schedule)) + _LOGGER.debug("Added %s", entities) return entities @@ -104,17 +105,18 @@ def is_on(self) -> bool: def _poll_update(self, data=None) -> bool: """Poll the API.""" - def _handle_any_update(self, *args, **kwargs) -> None: + @callback + def _async_handle_any_update(self, *args, **kwargs) -> None: """Determine whether an update event applies to this device.""" if args[0][KEY_DEVICE_ID] != self._controller.controller_id: # For another device return # For this device - self._handle_update(args, kwargs) + self._async_handle_update(args, kwargs) @abstractmethod - def _handle_update(self, *args, **kwargs) -> None: + def _async_handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook data.""" @@ -148,14 +150,15 @@ def _poll_update(self, data=None) -> bool: return not data[KEY_ON] - def _handle_update(self, *args, **kwargs) -> None: + @callback + def _async_handle_update(self, *args, **kwargs) -> None: """Update the state using webhook data.""" if args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: self._state = True elif args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: self._state = False - self.schedule_update_ha_state() + self.async_write_ha_state() def turn_on(self, **kwargs) -> None: """Put the controller in standby mode.""" @@ -167,8 +170,12 @@ def turn_off(self, **kwargs) -> None: async def async_added_to_hass(self): """Subscribe to updates.""" - async_dispatcher_connect( - self.hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + self._async_handle_any_update, + ) ) @@ -178,7 +185,6 @@ class RachioZone(RachioSwitch): def __init__(self, person, controller, data, current_schedule): """Initialize a new Rachio Zone.""" self._id = data[KEY_ID] - _LOGGER.debug("zone_data: %s", data) self._zone_name = data[KEY_NAME] self._zone_number = data[KEY_ZONE_NUMBER] self._zone_enabled = data[KEY_ENABLED] @@ -190,7 +196,6 @@ def __init__(self, person, controller, data, current_schedule): self._current_schedule = current_schedule super().__init__(controller, poll=False) self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID) - self._undo_dispatcher = None def __str__(self): """Display the zone as a string.""" @@ -227,7 +232,7 @@ def entity_picture(self): return self._entity_picture @property - def state_attributes(self) -> dict: + def device_state_attributes(self) -> dict: """Return the optional state attributes.""" props = {ATTR_ZONE_NUMBER: self._zone_number, ATTR_ZONE_SUMMARY: self._summary} if self._shade_type: @@ -264,7 +269,8 @@ def _poll_update(self, data=None) -> bool: self._current_schedule = self._controller.current_schedule return self.zone_id == self._current_schedule.get(KEY_ZONE_ID) - def _handle_update(self, *args, **kwargs) -> None: + @callback + def _async_handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook zone data.""" if args[0][KEY_ZONE_ID] != self.zone_id: return @@ -276,39 +282,30 @@ def _handle_update(self, *args, **kwargs) -> None: elif args[0][KEY_SUBTYPE] in [SUBTYPE_ZONE_STOPPED, SUBTYPE_ZONE_COMPLETED]: self._state = False - self.schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Subscribe to updates.""" - self._undo_dispatcher = async_dispatcher_connect( - self.hass, SIGNAL_RACHIO_ZONE_UPDATE, self._handle_update + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_RACHIO_ZONE_UPDATE, self._async_handle_update + ) ) - async def async_will_remove_from_hass(self): - """Unsubscribe from updates.""" - if self._undo_dispatcher: - self._undo_dispatcher() - class RachioSchedule(RachioSwitch): """Representation of one fixed schedule on the Rachio Iro.""" def __init__(self, person, controller, data, current_schedule): """Initialize a new Rachio Schedule.""" - self._id = data[KEY_ID] + self._schedule_id = data[KEY_ID] self._schedule_name = data[KEY_NAME] self._duration = data[KEY_DURATION] self._schedule_enabled = data[KEY_ENABLED] self._summary = data[KEY_SUMMARY] self._current_schedule = current_schedule super().__init__(controller, poll=False) - self._state = self.schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) - self._undo_dispatcher = None - - @property - def schedule_id(self) -> str: - """How the Rachio API refers to the schedule.""" - return self._id + self._state = self._schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) @property def name(self) -> str: @@ -318,7 +315,7 @@ def name(self) -> str: @property def unique_id(self) -> str: """Return a unique id by combining controller id and schedule.""" - return f"{self._controller.controller_id}-schedule-{self.schedule_id}" + return f"{self._controller.controller_id}-schedule-{self._schedule_id}" @property def icon(self) -> str: @@ -331,7 +328,7 @@ def device_state_attributes(self) -> dict: return { ATTR_SCHEDULE_SUMMARY: self._summary, ATTR_SCHEDULE_ENABLED: self.schedule_is_enabled, - ATTR_SCHEDULE_DURATION: self._duration / 60, + ATTR_SCHEDULE_DURATION: f"{round(self._duration / 60)} minutes", } @property @@ -342,7 +339,7 @@ def schedule_is_enabled(self) -> bool: def turn_on(self, **kwargs) -> None: """Start this schedule.""" - self._controller.rachio.schedulerule.start(self.schedule_id) + self._controller.rachio.schedulerule.start(self._schedule_id) _LOGGER.debug( "Schedule %s started on %s", self.name, self._controller.name, ) @@ -354,13 +351,14 @@ def turn_off(self, **kwargs) -> None: def _poll_update(self, data=None) -> bool: """Poll the API to check whether the schedule is running.""" self._current_schedule = self._controller.current_schedule - return self.schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) + return self._schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) - def _handle_update(self, *args, **kwargs) -> None: + @callback + def _async_handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook schedule data.""" # Schedule ID not passed when running individual zones, so we catch that error try: - if args[0][KEY_SCHEDULE_ID] == self.schedule_id: + if args[0][KEY_SCHEDULE_ID] == self._schedule_id: if args[0][KEY_SUBTYPE] in [SUBTYPE_SCHEDULE_STARTED]: self._state = True elif args[0][KEY_SUBTYPE] in [ @@ -371,15 +369,12 @@ def _handle_update(self, *args, **kwargs) -> None: except KeyError: pass - self.schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Subscribe to updates.""" - self._undo_dispatcher = async_dispatcher_connect( - self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._handle_update + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._async_handle_update + ) ) - - async def async_will_remove_from_hass(self): - """Unsubscribe from updates.""" - if self._undo_dispatcher: - self._undo_dispatcher() diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index 4f9ae7fb73330b..971b8174993a41 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -152,8 +152,10 @@ def name(self): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RAINCLOUD, self._update_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RAINCLOUD, self._update_callback + ) ) def _update_callback(self): diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 5936b5c33436c4..e342b2d341ed97 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -102,7 +102,9 @@ def update_callback(self): async def async_added_to_hass(self): """Connect update callbacks.""" - async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback) + self.async_on_remove( + async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback) + ) def _get_data(self): """Return new data from the api cache.""" diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 9ba008a56ef5d1..76b225bd93ac90 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -404,13 +404,17 @@ async def async_added_to_hass(self): self.hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND][_id].append( self.entity_id ) - async_dispatcher_connect( - self.hass, SIGNAL_AVAILABILITY, self._availability_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_AVAILABILITY, self._availability_callback + ) ) - async_dispatcher_connect( - self.hass, - SIGNAL_HANDLE_EVENT.format(self.entity_id), - self.handle_event_callback, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_HANDLE_EVENT.format(self.entity_id), + self.handle_event_callback, + ) ) # Process the initial event now that the entity is created @@ -494,7 +498,7 @@ async def _async_handle_command(self, command, *args): await self._async_send_command(cmd, self._signal_repetitions) # Update state of entity - await self.async_update_ha_state() + self.async_write_ha_state() def cancel_queued_send_commands(self): """Cancel queued signal repetition commands. diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index bc736a1ede6195..9394c568a73bca 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -139,13 +139,17 @@ async def async_added_to_hass(self): self.hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_SENSOR][_id].append( self.entity_id ) - async_dispatcher_connect( - self.hass, SIGNAL_AVAILABILITY, self._availability_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_AVAILABILITY, self._availability_callback + ) ) - async_dispatcher_connect( - self.hass, - SIGNAL_HANDLE_EVENT.format(self.entity_id), - self.handle_event_callback, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_HANDLE_EVENT.format(self.entity_id), + self.handle_event_callback, + ) ) # Process the initial event now that the entity is created diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 21ac9eefdb29a5..b232e2e63c58fb 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -37,7 +37,11 @@ def __init__(self, sensor_type, sabnzbd_api_data, client_name): async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - async_dispatcher_connect(self.hass, SIGNAL_SABNZBD_UPDATED, self.update_state) + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_SABNZBD_UPDATED, self.update_state + ) + ) @property def name(self): diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 55c2371aabbf0b..fb0df36d4cb48a 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -1,5 +1,4 @@ """SAJ solar inverter interface.""" -import asyncio from datetime import date import logging @@ -100,8 +99,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_saj(): """Update all the SAJ sensors.""" - tasks = [] - values = await saj.read(sensor_def) for sensor in hass_sensors: @@ -118,11 +115,8 @@ async def async_saj(): not sensor.per_day_basis and not sensor.per_total_basis ): state_unknown = True - task = sensor.async_update_values(unknown_state=state_unknown) - if task: - tasks.append(task) - if tasks: - await asyncio.wait(tasks) + sensor.async_update_values(unknown_state=state_unknown) + return values def start_update_interval(event): @@ -237,7 +231,8 @@ def async_update_values(self, unknown_state=False): update = True self._state = None - return self.async_update_ha_state() if update else None + if update: + self.async_write_ha_state() @property def unique_id(self): diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index 6034c24e31a745..8a240794580ce8 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -67,8 +67,10 @@ async def async_added_to_hass(self): """Update alarm status and register callbacks for future updates.""" _LOGGER.debug("Starts listening for panel messages") self._update_alarm_status() - async_dispatcher_connect( - self.hass, SIGNAL_PANEL_MESSAGE, self._update_alarm_status + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_PANEL_MESSAGE, self._update_alarm_status + ) ) @callback diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 5b268266dda1af..4a9be339a1cdd1 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -75,8 +75,10 @@ async def async_added_to_hass(self): self._state = 1 else: self._state = 0 - async_dispatcher_connect( - self.hass, self._react_to_signal, self._devices_updated + self.async_on_remove( + async_dispatcher_connect( + self.hass, self._react_to_signal, self._devices_updated + ) ) @property diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 9384c58db81783..6efd4c849aa5c5 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -243,7 +243,7 @@ def __init__(self, hass, object_id, name, icon, sequence): self.icon = icon self.entity_id = ENTITY_ID_FORMAT.format(object_id) self.script = Script( - hass, sequence, name, self.async_update_ha_state, logger=_LOGGER + hass, sequence, name, self.async_write_ha_state, logger=_LOGGER ) @property diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index 5ad59da5dee347..841fbb68178a50 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -99,18 +99,23 @@ def name(self): async def get_table(self): """Return the Table held by this holder, connecting to it if needed.""" + if self._table: + return self._table + if not self._table_task: self._table_task = self._hass.async_create_task(self._connect_table()) return await self._table_task async def _connect_table(self): - - self._table = await Table.connect(self._host, self._session) - if self._name is None: - self._name = self._table.name - _LOGGER.debug("Connected to %s at %s", self._name, self._host) - return self._table + try: + self._table = await Table.connect(self._host, self._session) + if self._name is None: + self._name = self._table.name + _LOGGER.debug("Connected to %s at %s", self._name, self._host) + return self._table + finally: + self._table_task = None async def close(self): """Close the table held by this holder, if any.""" diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 40ec4179cd1cee..e5afa272c40ee9 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -1,5 +1,4 @@ """SMA Solar Webconnect interface.""" -import asyncio from datetime import timedelta import logging @@ -163,13 +162,8 @@ async def async_sma(event): return backoff_step = 0 - tasks = [] for sensor in hass_sensors: - task = sensor.async_update_values() - if task: - tasks.append(task) - if tasks: - await asyncio.wait(tasks) + sensor.async_update_values() interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=5) async_track_time_interval(hass, async_sma, interval) @@ -226,7 +220,8 @@ def async_update_values(self): update = True self._state = self._sensor.value - return self.async_update_ha_state() if update else None + if update: + self.async_write_ha_state() @property def unique_id(self): diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 27a81b2a667d5b..d11ff84a73ca1a 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -159,21 +159,21 @@ async def _volume_changed(volume: VolumeChange): _LOGGER.debug("Volume changed: %s", volume) self._volume = volume.volume self._is_muted = volume.mute - await self.async_update_ha_state() + self.async_write_ha_state() async def _source_changed(content: ContentChange): _LOGGER.debug("Source changed: %s", content) if content.is_input: self._active_source = self._sources[content.source] _LOGGER.debug("New active source: %s", self._active_source) - await self.async_update_ha_state() + self.async_write_ha_state() else: _LOGGER.debug("Got non-handled content change: %s", content) async def _power_changed(power: PowerChange): _LOGGER.debug("Power changed: %s", power) self._state = power.status - await self.async_update_ha_state() + self.async_write_ha_state() async def _try_reconnect(connect: ConnectChange): _LOGGER.error( @@ -181,7 +181,7 @@ async def _try_reconnect(connect: ConnectChange): ) self._available = False self.dev.clear_notification_callbacks() - await self.async_update_ha_state() + self.async_write_ha_state() # Try to reconnect forever, a successful reconnect will initialize # the websocket connection again. diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index ca5d77b2a828a5..982c0fe2bab52d 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -57,8 +57,12 @@ def __init__(self, area, api): async def async_added_to_hass(self): """Call for adding new entities.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ALARM.format(self._area.id), self._update_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_ALARM.format(self._area.id), + self._update_callback, + ) ) @callback diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 34689c4dccfd42..3149ae560638d1 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -46,8 +46,12 @@ def __init__(self, zone): async def async_added_to_hass(self): """Call for adding new entities.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_SENSOR.format(self._zone.id), self._update_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_SENSOR.format(self._zone.id), + self._update_callback, + ) ) @callback diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index c1f51ab269ac7d..41db6c26930f90 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -91,8 +91,10 @@ async def async_added_to_hass(self): return self._state = state.state - async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update + self.async_on_remove( + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) ) def update(self): diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 4ace2c6eea1fcc..ea32183b511bf6 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -109,8 +109,10 @@ def available(self) -> bool: async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" - async_dispatcher_connect( - self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data + ) ) async def async_update_data(self, device_data: "SwitcherV2Device") -> None: diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index e7f341c90b26ed..db37f4669d3bc8 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -182,8 +182,10 @@ def __init__(self, tellcore_device, signal_repetitions): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_TELLCORE_CALLBACK, self.update_from_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_TELLCORE_CALLBACK, self.update_from_callback + ) ) @property diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 52560ea27329c3..c6d6656f1343e8 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -378,6 +378,9 @@ def update_brightness(self): return try: brightness = self._level_template.async_render() + if brightness in ("None", ""): + self._brightness = None + return if 0 <= int(brightness) <= 255: self._brightness = int(brightness) else: @@ -385,9 +388,15 @@ def update_brightness(self): "Received invalid brightness : %s. Expected: 0-255", brightness ) self._brightness = None - except TemplateError as ex: - _LOGGER.error(ex) - self._state = None + except ValueError: + _LOGGER.error( + "Template must supply an integer brightness from 0-255, or 'None'", + exc_info=True, + ) + self._brightness = None + except TemplateError: + _LOGGER.error("Invalid template", exc_info=True) + self._brightness = None @callback def update_state(self): @@ -415,7 +424,11 @@ def update_temperature(self): if self._temperature_template is None: return try: - temperature = int(self._temperature_template.async_render()) + render = self._temperature_template.async_render() + if render in ("None", ""): + self._temperature = None + return + temperature = int(render) if self.min_mireds <= temperature <= self.max_mireds: self._temperature = temperature else: @@ -425,6 +438,12 @@ def update_temperature(self): self.max_mireds, ) self._temperature = None + except ValueError: + _LOGGER.error( + "Template must supply an integer temperature within the range for this light, or 'None'", + exc_info=True, + ) + self._temperature = None except TemplateError: _LOGGER.error("Cannot evaluate temperature template", exc_info=True) self._temperature = None @@ -435,10 +454,11 @@ def update_color(self): if self._color_template is None: return - self._color = None - try: render = self._color_template.async_render() + if render in ("None", ""): + self._color = None + return h_str, s_str = map( float, render.replace("(", "").replace(")", "").split(",", 1) ) @@ -455,7 +475,10 @@ def update_color(self): h_str, s_str, ) + self._color = None else: _LOGGER.error("Received invalid hs_color : (%s)", render) - except TemplateError as ex: - _LOGGER.error(ex) + self._color = None + except TemplateError: + _LOGGER.error("Cannot evaluate hs_color template", exc_info=True) + self._color = None diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 5172322a63d22e..e47ac69be5bdda 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -201,6 +201,11 @@ def should_poll(self): """If entity should be polled.""" return False + @property + def force_update(self) -> bool: + """Return True to fix restart issues.""" + return True + @property def name(self): """Return name of the timer.""" diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 3a456dec531a38..d9d513198ce4ca 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -133,7 +133,7 @@ async def async_setup_platform(p_type, p_config=None, discovery_info=None): hass, p_config, discovery_info ) else: - provider = await hass.async_add_job( + provider = await hass.async_add_executor_job( platform.get_engine, hass, p_config, discovery_info ) @@ -226,41 +226,17 @@ async def async_init_cache(self, use_cache, cache_dir, time_memory, base_url): self.time_memory = time_memory self.base_url = base_url - def init_tts_cache_dir(cache_dir): - """Init cache folder.""" - if not os.path.isabs(cache_dir): - cache_dir = self.hass.config.path(cache_dir) - if not os.path.isdir(cache_dir): - _LOGGER.info("Create cache dir %s.", cache_dir) - os.mkdir(cache_dir) - return cache_dir - try: - self.cache_dir = await self.hass.async_add_job( - init_tts_cache_dir, cache_dir + self.cache_dir = await self.hass.async_add_executor_job( + _init_tts_cache_dir, self.hass, cache_dir ) except OSError as err: raise HomeAssistantError(f"Can't init cache dir {err}") - def get_cache_files(): - """Return a dict of given engine files.""" - cache = {} - - folder_data = os.listdir(self.cache_dir) - for file_data in folder_data: - record = _RE_VOICE_FILE.match(file_data) - if record: - key = KEY_PATTERN.format( - record.group(1), - record.group(2), - record.group(3), - record.group(4), - ) - cache[key.lower()] = file_data.lower() - return cache - try: - cache_files = await self.hass.async_add_job(get_cache_files) + cache_files = await self.hass.async_add_executor_job( + _get_cache_files, self.cache_dir + ) except OSError as err: raise HomeAssistantError(f"Can't read cache dir {err}") @@ -273,13 +249,13 @@ async def async_clear_cache(self): def remove_files(): """Remove files from filesystem.""" - for _, filename in self.file_cache.items(): + for filename in self.file_cache.values(): try: os.remove(os.path.join(self.cache_dir, filename)) except OSError as err: _LOGGER.warning("Can't remove cache file '%s': %s", filename, err) - await self.hass.async_add_job(remove_files) + await self.hass.async_add_executor_job(remove_files) self.file_cache = {} @callback @@ -312,6 +288,7 @@ async def async_get_url( merged_options.update(options) options = merged_options options = options or provider.default_options + if options is not None: invalid_opts = [ opt_name @@ -378,10 +355,10 @@ def save_speech(): speech.write(data) try: - await self.hass.async_add_job(save_speech) + await self.hass.async_add_executor_job(save_speech) self.file_cache[key] = filename - except OSError: - _LOGGER.error("Can't write %s", filename) + except OSError as err: + _LOGGER.error("Can't write %s: %s", filename, err) async def async_file_to_mem(self, key): """Load voice from file cache into memory. @@ -400,7 +377,7 @@ def load_speech(): return speech.read() try: - data = await self.hass.async_add_job(load_speech) + data = await self.hass.async_add_executor_job(load_speech) except OSError: del self.file_cache[key] raise HomeAssistantError(f"Can't read {voice_file}") @@ -506,11 +483,36 @@ async def async_get_tts_audio(self, message, language, options=None): Return a tuple of file extension and data as bytes. """ - return await self.hass.async_add_job( + return await self.hass.async_add_executor_job( ft.partial(self.get_tts_audio, message, language, options=options) ) +def _init_tts_cache_dir(hass, cache_dir): + """Init cache folder.""" + if not os.path.isabs(cache_dir): + cache_dir = hass.config.path(cache_dir) + if not os.path.isdir(cache_dir): + _LOGGER.info("Create cache dir %s", cache_dir) + os.mkdir(cache_dir) + return cache_dir + + +def _get_cache_files(cache_dir): + """Return a dict of given engine files.""" + cache = {} + + folder_data = os.listdir(cache_dir) + for file_data in folder_data: + record = _RE_VOICE_FILE.match(file_data) + if record: + key = KEY_PATTERN.format( + record.group(1), record.group(2), record.group(3), record.group(4), + ) + cache[key.lower()] = file_data.lower() + return cache + + class TextToSpeechUrlView(HomeAssistantView): """TTS view to get a url to a generated speech file.""" diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index 655000662ec8c7..afdea87956b375 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -32,7 +32,8 @@ "client_control": { "data": { "block_client": "Clients mit Netzwerkzugriffskontrolle", - "new_client": "F\u00fcgen Sie einen neuen Client f\u00fcr die Netzwerkzugangskontrolle hinzu" + "new_client": "F\u00fcgen Sie einen neuen Client f\u00fcr die Netzwerkzugangskontrolle hinzu", + "poe_clients": "POE-Kontrolle von Clients zulassen" }, "description": "Konfigurieren Sie Client-Steuerelemente \n\nErstellen Sie Switches f\u00fcr Seriennummern, f\u00fcr die Sie den Netzwerkzugriff steuern m\u00f6chten.", "title": "UniFi-Optionen 2/3" diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 8fdde34470b088..d42a647c82fe2c 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -32,7 +32,8 @@ "client_control": { "data": { "block_client": "Network access controlled clients", - "new_client": "Add new client for network access control" + "new_client": "Add new client for network access control", + "poe_clients": "Allow POE control of clients" }, "description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.", "title": "UniFi options 2/3" diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json index 5c45e272e9174c..d57d80c7911475 100644 --- a/homeassistant/components/unifi/.translations/ko.json +++ b/homeassistant/components/unifi/.translations/ko.json @@ -32,7 +32,8 @@ "client_control": { "data": { "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8", - "new_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4\ub97c \uc704\ud55c \uc0c8\ub85c\uc6b4 \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uac00" + "new_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4\ub97c \uc704\ud55c \uc0c8\ub85c\uc6b4 \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uac00", + "poe_clients": "\ud074\ub77c\uc774\uc5b8\ud2b8\uc758 POE \uc81c\uc5b4 \ud5c8\uc6a9" }, "description": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ucee8\ud2b8\ub864 \uad6c\uc131 \n\n\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4\ub97c \uc81c\uc5b4\ud558\ub824\ub294 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc5d0 \ub300\ud55c \uc2a4\uc704\uce58\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.", "title": "UniFi \uc635\uc158 2/3" diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index e0bb1c3bb9fcf3..781fcdeae828e8 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -19,12 +19,14 @@ CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, + DEFAULT_POE_CLIENTS, DOMAIN, LOGGER, ) @@ -262,6 +264,10 @@ async def async_step_client_control(self, user_input=None): step_id="client_control", data_schema=vol.Schema( { + vol.Optional( + CONF_POE_CLIENTS, + default=self.options.get(CONF_POE_CLIENTS, DEFAULT_POE_CLIENTS), + ): bool, vol.Optional( CONF_BLOCK_CLIENT, default=self.options[CONF_BLOCK_CLIENT] ): cv.multi_select(clients_to_block), diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index fd94601db50b2e..4acf75fbdd9f56 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -14,12 +14,14 @@ CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" +CONF_POE_CLIENTS = "poe_clients" CONF_TRACK_CLIENTS = "track_clients" CONF_TRACK_DEVICES = "track_devices" CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" CONF_SSID_FILTER = "ssid_filter" DEFAULT_ALLOW_BANDWIDTH_SENSORS = False +DEFAULT_POE_CLIENTS = True DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True DEFAULT_TRACK_WIRED_CLIENTS = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 50b758f01af6e5..03e079c017070c 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -24,6 +24,7 @@ CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, @@ -32,6 +33,7 @@ CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_DETECTION_TIME, + DEFAULT_POE_CLIENTS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, @@ -98,6 +100,11 @@ def option_block_clients(self): """Config entry option with list of clients to control network access.""" return self.config_entry.options.get(CONF_BLOCK_CLIENT, []) + @property + def option_poe_clients(self): + """Config entry option to control poe clients.""" + return self.config_entry.options.get(CONF_POE_CLIENTS, DEFAULT_POE_CLIENTS) + @property def option_track_clients(self): """Config entry option to not track clients.""" diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 58728225de7df9..61116adfb60ab5 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -43,7 +43,8 @@ "client_control": { "data": { "block_client": "Network access controlled clients", - "new_client": "Add new client for network access control" + "new_client": "Add new client for network access control", + "poe_clients": "Allow POE control of clients" }, "description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.", "title": "UniFi options 2/3" diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 0df019de02ccf6..0547e35f064bb0 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -30,6 +30,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): switches_off = [] option_block_clients = controller.option_block_clients + option_poe_clients = controller.option_poe_clients entity_registry = await hass.helpers.entity_registry.async_get_registry() @@ -66,6 +67,7 @@ def update_controller(): def options_updated(): """Manage entities affected by config entry options.""" nonlocal option_block_clients + nonlocal option_poe_clients update = set() remove = set() @@ -82,16 +84,26 @@ def options_updated(): else: remove.add(block_client_id) - for block_client_id in remove: - entity = switches.pop(block_client_id) + if option_poe_clients != controller.option_poe_clients: + option_poe_clients = controller.option_poe_clients - if entity_registry.async_is_registered(entity.entity_id): - entity_registry.async_remove(entity.entity_id) + if option_poe_clients: + update.add("poe_clients_enabled") + else: + for poe_client_id, entity in switches.items(): + if isinstance(entity, UniFiPOEClientSwitch): + remove.add(poe_client_id) - hass.async_create_task(entity.async_remove()) + for client_id in remove: + entity = switches.pop(client_id) - if len(update) != len(option_block_clients): - update_controller() + if entity_registry.async_is_registered(entity.entity_id): + entity_registry.async_remove(entity.entity_id) + + hass.async_create_task(entity.async_remove()) + + if len(update) != len(option_block_clients): + update_controller() controller.listeners.append( async_dispatcher_connect( @@ -109,7 +121,6 @@ def add_entities(controller, async_add_entities, switches, switches_off): new_switches = [] devices = controller.api.devices - # block client for client_id in controller.option_block_clients: client = None @@ -130,49 +141,49 @@ def add_entities(controller, async_add_entities, switches, switches_off): switches[block_client_id] = UniFiBlockClientSwitch(client, controller) new_switches.append(switches[block_client_id]) - # control POE - for client_id in controller.api.clients: + if controller.option_poe_clients: + for client_id in controller.api.clients: - poe_client_id = f"poe-{client_id}" - - if poe_client_id in switches: - continue + poe_client_id = f"poe-{client_id}" - client = controller.api.clients[client_id] - - if poe_client_id in switches_off: - pass - # Network device with active POE - elif ( - client_id in controller.wireless_clients - or client.sw_mac not in devices - or not devices[client.sw_mac].ports[client.sw_port].port_poe - or not devices[client.sw_mac].ports[client.sw_port].poe_enable - or controller.mac == client.mac - ): - continue + if poe_client_id in switches: + continue - # Multiple POE-devices on same port means non UniFi POE driven switch - multi_clients_on_port = False - for client2 in controller.api.clients.values(): + client = controller.api.clients[client_id] if poe_client_id in switches_off: - break - - if ( - client2.is_wired - and client.mac != client2.mac - and client.sw_mac == client2.sw_mac - and client.sw_port == client2.sw_port + pass + # Network device with active POE + elif ( + client_id in controller.wireless_clients + or client.sw_mac not in devices + or not devices[client.sw_mac].ports[client.sw_port].port_poe + or not devices[client.sw_mac].ports[client.sw_port].poe_enable + or controller.mac == client.mac ): - multi_clients_on_port = True - break + continue - if multi_clients_on_port: - continue + # Multiple POE-devices on same port means non UniFi POE driven switch + multi_clients_on_port = False + for client2 in controller.api.clients.values(): + + if poe_client_id in switches_off: + break + + if ( + client2.is_wired + and client.mac != client2.mac + and client.sw_mac == client2.sw_mac + and client.sw_port == client2.sw_port + ): + multi_clients_on_port = True + break + + if multi_clients_on_port: + continue - switches[poe_client_id] = UniFiPOEClientSwitch(client, controller) - new_switches.append(switches[poe_client_id]) + switches[poe_client_id] = UniFiPOEClientSwitch(client, controller) + new_switches.append(switches[poe_client_id]) if new_switches: async_add_entities(new_switches) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 9632997ac1b135..88d6681a8049ac 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -82,8 +82,10 @@ def __init__(self, device): async def async_added_to_hass(self): """Subscribe to sensors events.""" - async_dispatcher_connect( - self.hass, SIGNAL_REMOVE_SENSOR, self._upnp_remove_sensor + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_REMOVE_SENSOR, self._upnp_remove_sensor + ) ) @callback diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index ef9d9b1ddcee90..24bfd77f762fa4 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -184,11 +184,11 @@ async def async_select_tariff(self, tariff): ) return self._current_tariff = tariff - await self.async_update_ha_state() + self.async_write_ha_state() async def async_next_tariff(self): """Offset current index.""" current_index = self._tariffs.index(self._current_tariff) new_index = (current_index + 1) % len(self._tariffs) self._current_tariff = self._tariffs[new_index] - await self.async_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index ad1b27f1fe09e9..c891c698cf6f61 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -217,7 +217,7 @@ async def async_reset_meter(self, entity_id): self._last_reset = dt_util.now() self._last_period = str(self._state) self._state = 0 - await self.async_update_ha_state() + self.async_write_ha_state() async def async_calibrate(self, value): """Calibrate the Utility Meter with a given value.""" @@ -253,7 +253,7 @@ async def async_added_to_hass(self): self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._last_period = state.attributes.get(ATTR_LAST_PERIOD) self._last_reset = state.attributes.get(ATTR_LAST_RESET) - await self.async_update_ha_state() + self.async_write_ha_state() if state.attributes.get(ATTR_STATUS) == PAUSED: # Fake cancellation function to init the meter paused self._collecting = lambda: None diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 5277a3309762f8..c79ee15db598f1 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -93,8 +93,10 @@ def device_state_attributes(self): async def async_added_to_hass(self): """Call to update.""" - async_dispatcher_connect( - self.hass, SIGNAL_VALLOX_STATE_UPDATE, self._update_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_VALLOX_STATE_UPDATE, self._update_callback + ) ) @callback diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 5bf9b8061ad1cf..b3a7e8758a0296 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -149,8 +149,10 @@ def state(self): async def async_added_to_hass(self): """Call to update.""" - async_dispatcher_connect( - self.hass, SIGNAL_VALLOX_STATE_UPDATE, self._update_callback + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_VALLOX_STATE_UPDATE, self._update_callback + ) ) @callback diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index c9b4aa53fe5233..fe5b1dcf3afca3 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -38,7 +38,7 @@ def async_register_callbacks(self): async def after_update_callback(device): """Call after device was updated.""" - await self.async_update_ha_state() + self.async_write_ha_state() self.node.register_device_updated_cb(after_update_callback) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 1c9d412d974ddd..c98833a7daa014 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,4 +1,5 @@ """Support for Vera devices.""" +import asyncio from collections import defaultdict import logging @@ -6,6 +7,8 @@ from requests.exceptions import RequestException import voluptuous as vol +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ARMED, ATTR_BATTERY_LEVEL, @@ -15,26 +18,23 @@ CONF_LIGHTS, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import convert, slugify from homeassistant.util.dt import utc_from_timestamp -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "vera" - -VERA_CONTROLLER = "vera_controller" - -CONF_CONTROLLER = "vera_controller_url" - -VERA_ID_FORMAT = "{}_{}" - -ATTR_CURRENT_POWER_W = "current_power_w" -ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" +from .common import ControllerData, get_configured_platforms +from .config_flow import new_options +from .const import ( + ATTR_CURRENT_ENERGY_KWH, + ATTR_CURRENT_POWER_W, + CONF_CONTROLLER, + DOMAIN, + VERA_ID_FORMAT, +) -VERA_DEVICES = "vera_devices" -VERA_SCENES = "vera_scenes" +_LOGGER = logging.getLogger(__name__) VERA_ID_LIST_SCHEMA = vol.Schema([int]) @@ -51,42 +51,53 @@ extra=vol.ALLOW_EXTRA, ) -VERA_COMPONENTS = [ - "binary_sensor", - "sensor", - "light", - "switch", - "lock", - "climate", - "cover", - "scene", -] +async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: + """Set up for Vera controllers.""" + config = base_config.get(DOMAIN) -def setup(hass, base_config): - """Set up for Vera devices.""" + if not config: + return True - def stop_subscription(event): - """Shutdown Vera subscriptions and subscription thread on exit.""" - _LOGGER.info("Shutting down subscriptions") - hass.data[VERA_CONTROLLER].stop() + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config, + ) + ) - config = base_config.get(DOMAIN) + return True - # Get Vera specific configuration. - base_url = config.get(CONF_CONTROLLER) - light_ids = config.get(CONF_LIGHTS) - exclude_ids = config.get(CONF_EXCLUDE) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Do setup of vera.""" + # Use options entered during initial config flow or provided from configuration.yml + if config_entry.data.get(CONF_LIGHTS) or config_entry.data.get(CONF_EXCLUDE): + hass.config_entries.async_update_entry( + entry=config_entry, + data=config_entry.data, + options=new_options( + config_entry.data.get(CONF_LIGHTS, []), + config_entry.data.get(CONF_EXCLUDE, []), + ), + ) + + base_url = config_entry.data[CONF_CONTROLLER] + light_ids = config_entry.options.get(CONF_LIGHTS, []) + exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) # Initialize the Vera controller. - controller, _ = veraApi.init_controller(base_url) - hass.data[VERA_CONTROLLER] = controller - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + controller = veraApi.VeraController(base_url) + controller.start() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, + lambda event: hass.async_add_executor_job(controller.stop), + ) try: - all_devices = controller.get_devices() + all_devices = await hass.async_add_executor_job(controller.get_devices) - all_scenes = controller.get_scenes() + all_scenes = await hass.async_add_executor_job(controller.get_scenes) except RequestException: # There was a network related error connecting to the Vera controller. _LOGGER.exception("Error communicating with Vera API") @@ -102,15 +113,35 @@ def stop_subscription(event): continue vera_devices[device_type].append(device) - hass.data[VERA_DEVICES] = vera_devices vera_scenes = [] for scene in all_scenes: vera_scenes.append(scene) - hass.data[VERA_SCENES] = vera_scenes - for component in VERA_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, base_config) + controller_data = ControllerData( + controller=controller, devices=vera_devices, scenes=vera_scenes + ) + + hass.data[DOMAIN] = controller_data + + # Forward the config data to the necessary platforms. + for platform in get_configured_platforms(controller_data): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload Withings config entry.""" + controller_data = hass.data[DOMAIN] + + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in get_configured_platforms(controller_data) + ] + await asyncio.gather(*tasks) return True diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 061d2c5c99aef4..621dc09930d99f 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -1,21 +1,34 @@ """Support for Vera binary sensors.""" import logging +from typing import Callable, List -from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + BinarySensorDevice, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Perform the setup for Vera controller devices.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraBinarySensor(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["binary_sensor"] - ], - True, + VeraBinarySensor(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 60e73d48978cdb..520c3b516df34f 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -1,7 +1,12 @@ """Support for Vera thermostats.""" import logging +from typing import Callable, List -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice +from homeassistant.components.climate import ( + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + ClimateDevice, +) from homeassistant.components.climate.const import ( FAN_AUTO, FAN_ON, @@ -12,10 +17,14 @@ SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from homeassistant.util import convert -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,14 +34,18 @@ SUPPORT_HVAC = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF] -def setup_platform(hass, config, add_entities_callback, discovery_info=None): - """Set up of Vera thermostats.""" - add_entities_callback( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraThermostat(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["climate"] - ], - True, + VeraThermostat(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py new file mode 100644 index 00000000000000..cdfdff404ec911 --- /dev/null +++ b/homeassistant/components/vera/common.py @@ -0,0 +1,29 @@ +"""Common vera code.""" +import logging +from typing import DefaultDict, List, NamedTuple, Set + +import pyvera as pv + +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ControllerData(NamedTuple): + """Controller data.""" + + controller: pv.VeraController + devices: DefaultDict[str, List[pv.VeraDevice]] + scenes: List[pv.VeraScene] + + +def get_configured_platforms(controller_data: ControllerData) -> Set[str]: + """Get configured platforms for a controller.""" + platforms = [] + for platform in controller_data.devices: + platforms.append(platform) + + if controller_data.scenes: + platforms.append(SCENE_DOMAIN) + + return set(platforms) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py new file mode 100644 index 00000000000000..3d2b30f1079fc7 --- /dev/null +++ b/homeassistant/components/vera/config_flow.py @@ -0,0 +1,130 @@ +"""Config flow for Vera.""" +import logging +import re +from typing import List, cast + +import pyvera as pv +from requests.exceptions import RequestException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE +from homeassistant.core import callback + +from .const import CONF_CONTROLLER, DOMAIN + +LIST_REGEX = re.compile("[^0-9]+") +_LOGGER = logging.getLogger(__name__) + + +def str_to_int_list(data: str) -> List[str]: + """Convert a string to an int list.""" + if isinstance(str, list): + return cast(List[str], data) + + return [s for s in LIST_REGEX.split(data) if len(s) > 0] + + +def int_list_to_str(data: List[str]) -> str: + """Convert an int list to a string.""" + return " ".join([str(i) for i in data]) + + +def new_options(lights: List[str], exclude: List[str]) -> dict: + """Create a standard options object.""" + return {CONF_LIGHTS: lights, CONF_EXCLUDE: exclude} + + +def options_schema(options: dict = None) -> dict: + """Return options schema.""" + options = options or {} + return { + vol.Optional( + CONF_LIGHTS, default=int_list_to_str(options.get(CONF_LIGHTS, [])), + ): str, + vol.Optional( + CONF_EXCLUDE, default=int_list_to_str(options.get(CONF_EXCLUDE, [])), + ): str, + } + + +def options_data(user_input: dict) -> dict: + """Return options dict.""" + return new_options( + str_to_int_list(user_input.get(CONF_LIGHTS, "")), + str_to_int_list(user_input.get(CONF_EXCLUDE, "")), + ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Options for the component.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Init object.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=options_data(user_input),) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(options_schema(self.config_entry.options)), + ) + + +class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Vera config flow.""" + + @staticmethod + @callback + def async_get_options_flow(config_entry) -> OptionsFlowHandler: + """Get the options flow.""" + return OptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input: dict = None): + """Handle user initiated flow.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_configured") + + if user_input is not None: + return await self.async_step_finish( + { + **user_input, + **options_data(user_input), + **{CONF_SOURCE: config_entries.SOURCE_USER}, + } + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {**{vol.Required(CONF_CONTROLLER): str}, **options_schema()} + ), + ) + + async def async_step_import(self, config: dict): + """Handle a flow initialized by import.""" + return await self.async_step_finish( + {**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}} + ) + + async def async_step_finish(self, config: dict): + """Validate and create config entry.""" + base_url = config[CONF_CONTROLLER] = config[CONF_CONTROLLER].rstrip("/") + controller = pv.VeraController(base_url) + + # Verify the controller is online and get the serial number. + try: + await self.hass.async_add_executor_job(controller.refresh_data) + except RequestException: + _LOGGER.error("Failed to connect to vera controller %s", base_url) + return self.async_abort( + reason="cannot_connect", description_placeholders={"base_url": base_url} + ) + + await self.async_set_unique_id(controller.serial_number) + self._abort_if_unique_id_configured(config) + + return self.async_create_entry(title=base_url, data=config) diff --git a/homeassistant/components/vera/const.py b/homeassistant/components/vera/const.py new file mode 100644 index 00000000000000..c4f1d0efa3a0b2 --- /dev/null +++ b/homeassistant/components/vera/const.py @@ -0,0 +1,11 @@ +"""Vera constants.""" +DOMAIN = "vera" + +CONF_CONTROLLER = "vera_controller_url" + +VERA_ID_FORMAT = "{}_{}" + +ATTR_CURRENT_POWER_W = "current_power_w" +ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh" + +CONTROLLER_DATAS = "controller_datas" diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index b90dd8a053112d..0d0edb841c1ec0 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -1,21 +1,35 @@ """Support for Vera cover - curtains, rollershutters etc.""" import logging +from typing import Callable, List -from homeassistant.components.cover import ATTR_POSITION, ENTITY_ID_FORMAT, CoverDevice +from homeassistant.components.cover import ( + ATTR_POSITION, + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + CoverDevice, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera covers.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraCover(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["cover"] - ], - True, + VeraCover(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index fee992356816e3..877fdf51f0a48d 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -1,29 +1,39 @@ """Support for Vera lights.""" import logging +from typing import Callable, List from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity import homeassistant.util.color as color_util -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera lights.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraLight(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["light"] - ], - True, + VeraLight(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index 23b62bb0331466..da3c432a6afa91 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -1,10 +1,19 @@ """Support for Vera locks.""" import logging - -from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice +from typing import Callable, List + +from homeassistant.components.lock import ( + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + LockDevice, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -12,14 +21,18 @@ ATTR_LOW_BATTERY = "low_battery" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Find and return Vera locks.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraLock(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["lock"] - ], - True, + VeraLock(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 63102c29687862..4f585d964a86a7 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -1,8 +1,11 @@ { "domain": "vera", "name": "Vera", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": ["pyvera==0.3.7"], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@vangorra" + ] } diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index af5266ed4b3d7b..7d09e248893881 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -1,22 +1,30 @@ """Support for Vera scenes.""" import logging +from typing import Callable, List from homeassistant.components.scene import Scene +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from . import VERA_CONTROLLER, VERA_ID_FORMAT, VERA_SCENES +from .const import DOMAIN, VERA_ID_FORMAT _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera scenes.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraScene(scene, hass.data[VERA_CONTROLLER]) - for scene in hass.data[VERA_SCENES] - ], - True, + VeraScene(device, controller_data.controller) + for device in controller_data.scenes + ] ) diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 9ac0a36ff9cd00..60ebeeb156655e 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -1,29 +1,37 @@ """Support for Vera sensors.""" from datetime import timedelta import logging +from typing import Callable, List import pyvera as veraApi -from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT +from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.util import convert -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=5) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera controller devices.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraSensor(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["sensor"] - ], - True, + VeraSensor(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json new file mode 100644 index 00000000000000..d8dec2c40cfa70 --- /dev/null +++ b/homeassistant/components/vera/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "title": "Vera", + "abort": { + "already_configured": "A controller is already configured.", + "cannot_connect": "Could not connect to controller with url {base_url}" + }, + "step": { + "user": { + "title": "Setup Vera controller", + "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480.", + "data": { + "vera_controller_url": "Controller URL", + "lights": "Vera switch device ids to treat as lights in Home Assistant.", + "exclude": "Vera device ids to exclude from Home Assistant." + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Vera controller options", + "description": "See the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/. Note: Any changes here will need a restart to the home assistant server. To clear values, provide a space.", + "data": { + "lights": "Vera switch device ids to treat as lights in Home Assistant.", + "exclude": "Vera device ids to exclude from Home Assistant." + } + } + } + } +} diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index ab3c3e6adb9555..a7ae6d45573e41 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -1,22 +1,35 @@ """Support for Vera switches.""" import logging +from typing import Callable, List -from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice +from homeassistant.components.switch import ( + DOMAIN as PLATFORM_DOMAIN, + ENTITY_ID_FORMAT, + SwitchDevice, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from homeassistant.util import convert -from . import VERA_CONTROLLER, VERA_DEVICES, VeraDevice +from . import VeraDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vera switches.""" - add_entities( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the sensor config entry.""" + controller_data = hass.data[DOMAIN] + async_add_entities( [ - VeraSwitch(device, hass.data[VERA_CONTROLLER]) - for device in hass.data[VERA_DEVICES]["switch"] - ], - True, + VeraSwitch(device, controller_data.controller) + for device in controller_data.devices.get(PLATFORM_DOMAIN) + ] ) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 795f12266fb440..43cb993cec3fe1 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -63,6 +63,9 @@ ), } +VIZIO_SOUND_MODE = "eq" +VIZIO_AUDIO_SETTINGS = "audio" + # Since Vizio component relies on device class, this dict will ensure that changes to # the values of DEVICE_CLASS_SPEAKER or DEVICE_CLASS_TV don't require changes to pyvizio. VIZIO_DEVICE_CLASSES = { diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 885cfacca41529..2436ce6298b3f6 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "VIZIO SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.44"], + "requirements": ["pyvizio==0.1.45"], "dependencies": [], "codeowners": ["@raman325"], "config_flow": true, diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index d463ebca36a422..bb7ae3f75b0510 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -9,6 +9,7 @@ from homeassistant.components.media_player import ( DEVICE_CLASS_SPEAKER, + SUPPORT_SELECT_SOUND_MODE, MediaPlayerDevice, ) from homeassistant.config_entries import ConfigEntry @@ -41,7 +42,9 @@ DOMAIN, ICON, SUPPORTED_COMMANDS, + VIZIO_AUDIO_SETTINGS, VIZIO_DEVICE_CLASSES, + VIZIO_SOUND_MODE, ) _LOGGER = logging.getLogger(__name__) @@ -133,6 +136,8 @@ def __init__( self._current_input = None self._current_app = None self._current_app_config = None + self._current_sound_mode = None + self._available_sound_modes = None self._available_inputs = [] self._available_apps = [] self._conf_apps = config_entry.options.get(CONF_APPS, {}) @@ -191,17 +196,29 @@ async def async_update(self) -> None: self._current_app = None self._current_app_config = None self._available_apps = None + self._current_sound_mode = None + self._available_sound_modes = None return self._state = STATE_ON - audio_settings = await self._device.get_all_audio_settings( - log_api_exception=False + audio_settings = await self._device.get_all_settings( + VIZIO_AUDIO_SETTINGS, log_api_exception=False ) if audio_settings is not None: self._volume_level = float(audio_settings["volume"]) / self._max_volume self._is_muted = audio_settings["mute"].lower() == "on" + if VIZIO_SOUND_MODE in audio_settings: + self._supported_commands |= SUPPORT_SELECT_SOUND_MODE + self._current_sound_mode = audio_settings[VIZIO_SOUND_MODE] + if self._available_sound_modes is None: + self._available_sound_modes = await self._device.get_setting_options( + VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE + ) + else: + self._supported_commands ^= SUPPORT_SELECT_SOUND_MODE + input_ = await self._device.get_current_input(log_api_exception=False) if input_ is not None: self._current_input = input_ @@ -367,7 +384,7 @@ def unique_id(self) -> str: return self._config_entry.unique_id @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device registry information.""" return { "identifiers": {(DOMAIN, self._config_entry.unique_id)}, @@ -378,10 +395,27 @@ def device_info(self): } @property - def device_class(self): + def device_class(self) -> str: """Return device class for entity.""" return self._device_class + @property + def sound_mode(self) -> Optional[str]: + """Name of the current sound mode.""" + return self._current_sound_mode + + @property + def sound_mode_list(self) -> Optional[List[str]]: + """List of available sound modes.""" + return self._available_sound_modes + + async def async_select_sound_mode(self, sound_mode): + """Select sound mode.""" + if sound_mode in self._available_sound_modes: + await self._device.set_setting( + VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE, sound_mode + ) + async def async_turn_on(self) -> None: """Turn the device on.""" await self._device.pow_on() diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index c621a12943b8cb..c408080524ebed 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -230,8 +230,10 @@ def __init__(self, data, vin, component, attribute): async def async_added_to_hass(self): """Register update dispatcher.""" - async_dispatcher_connect( - self.hass, SIGNAL_STATE_UPDATED, self.async_schedule_update_ha_state + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_STATE_UPDATED, self.async_write_ha_state + ) ) @property diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 14f3549b2a37a2..b2b8aaa6f3580d 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -105,8 +105,10 @@ def should_poll(self): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_update_callback + ) ) @callback diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index a74381b8a856a6..6be07dfb1f42a1 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -28,11 +28,15 @@ def __init__(self): async def async_added_to_hass(self): """Added to hass.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_CONNECTED, self._update_count + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_WEBSOCKET_CONNECTED, self._update_count + ) ) - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_DISCONNECTED, self._update_count + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_WEBSOCKET_DISCONNECTED, self._update_count + ) ) self._update_count() diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index 07acf6057a1059..eae8c17edcd32e 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -102,10 +102,12 @@ async def async_added_to_hass(self): tag_id = self.tag_id event_type = self.device_class mac = self.tag_manager_mac - async_dispatcher_connect( - self.hass, - SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac), - self._on_binary_event_callback, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac), + self._on_binary_event_callback, + ) ) @property diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index 7a41d237781674..14f63084709847 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -64,10 +64,12 @@ def __init__(self, api, tag, sensor_type, config): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, - SIGNAL_TAG_UPDATE.format(self.tag_id, self.tag_manager_mac), - self._update_tag_info_callback, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_TAG_UPDATE.format(self.tag_id, self.tag_manager_mac), + self._update_tag_info_callback, + ) ) @property diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 3d179c63adbdaa..4d88cdef0f2a84 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -2,7 +2,7 @@ "domain": "xiaomi_miio", "name": "Xiaomi miio", "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.9.45", "python-miio==0.4.8"], + "requirements": ["construct==2.9.45", "python-miio==0.5.0.1"], "dependencies": [], "codeowners": ["@rytilahti", "@syssi"] } diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index a32a28993cac3e..416918e6f43603 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -60,8 +60,6 @@ extra=vol.ALLOW_EXTRA, ) -FAN_SPEEDS = {"Silent": 38, "Standard": 60, "Medium": 77, "Turbo": 90, "Gentle": 105} - ATTR_CLEAN_START = "clean_start" ATTR_CLEAN_STOP = "clean_stop" ATTR_CLEANING_TIME = "cleaning_time" @@ -246,6 +244,8 @@ def __init__(self, name, vacuum): self.clean_history = None self.dnd_state = None self.last_clean = None + self._fan_speeds = None + self._fan_speeds_reverse = None @property def name(self): @@ -281,14 +281,17 @@ def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" if self.vacuum_state is not None: speed = self.vacuum_state.fanspeed - if speed in FAN_SPEEDS.values(): - return [key for key, value in FAN_SPEEDS.items() if value == speed][0] + if speed in self._fan_speeds_reverse: + return self._fan_speeds_reverse[speed] + + _LOGGER.debug("Unable to find reverse for %s", speed) + return speed @property def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" - return list(sorted(FAN_SPEEDS.keys(), key=lambda s: FAN_SPEEDS[s])) + return list(self._fan_speeds) @property def device_state_attributes(self): @@ -372,8 +375,8 @@ async def async_stop(self, **kwargs): async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" - if fan_speed.capitalize() in FAN_SPEEDS: - fan_speed = FAN_SPEEDS[fan_speed.capitalize()] + if fan_speed in self._fan_speeds: + fan_speed = self._fan_speeds[fan_speed] else: try: fan_speed = int(fan_speed) @@ -453,6 +456,9 @@ def update(self): state = self._vacuum.status() self.vacuum_state = state + self._fan_speeds = self._vacuum.fan_speed_presets() + self._fan_speeds_reverse = {v: k for k, v in self._fan_speeds.items()} + self.consumable_state = self._vacuum.consumable_status() self.clean_history = self._vacuum.clean_history() self.last_clean = self._vacuum.last_clean_details() diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 3c06a75fb71a98..f5f3e03b7650ee 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -27,21 +27,17 @@ class YeelightNightlightModeSensor(BinarySensorDevice): def __init__(self, device): """Initialize nightlight mode sensor.""" self._device = device - self._unsub_disp = None async def async_added_to_hass(self): """Handle entity which will be added.""" - self._unsub_disp = async_dispatcher_connect( - self.hass, - DATA_UPDATED.format(self._device.ipaddr), - self.async_write_ha_state, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + DATA_UPDATED.format(self._device.ipaddr), + self.async_write_ha_state, + ) ) - async def async_will_remove_from_hass(self): - """When entity will be removed from hass.""" - self._unsub_disp() - self._unsub_disp = None - @property def should_poll(self): """No polling needed.""" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 59863464d21eb5..2f69b98bcbcf2e 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -456,10 +456,12 @@ def _schedule_immediate_update(self): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( - self.hass, - DATA_UPDATED.format(self._device.ipaddr), - self._schedule_immediate_update, + self.async_on_remove( + async_dispatcher_connect( + self.hass, + DATA_UPDATED.format(self._device.ipaddr), + self._schedule_immediate_update, + ) ) @property diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index c6aaeea7ac9a46..9955a650cd3684 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -96,11 +96,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: dev.append(YrSensor(name, sensor_type)) - async_add_entities(dev) weather = YrData(hass, coordinates, forecast, dev) - async_track_utc_time_change(hass, weather.updating_devices, minute=31, second=0) + async_track_utc_time_change( + hass, weather.updating_devices, minute=randrange(60), second=0 + ) await weather.fetching_data() + async_add_entities(dev) class YrSensor(Entity): @@ -234,7 +236,6 @@ async def updating_devices(self, *_): ordered_entries.sort(key=lambda item: item[0]) # Update all devices - tasks = [] if ordered_entries: for dev in self.devices: new_state = None @@ -274,7 +275,5 @@ async def updating_devices(self, *_): # pylint: disable=protected-access if new_state != dev._state: dev._state = new_state - tasks.append(dev.async_update_ha_state()) - - if tasks: - await asyncio.wait(tasks) + if dev.hass: + dev.async_write_ha_state() diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index b0808d83d68fc2..3171b8e953bc58 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.24.5"], + "requirements": ["zeroconf==0.25.0"], "dependencies": ["api"], "codeowners": ["@robbiet480", "@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 3fb5491ea62632..f1b76075ae8bc7 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -87,7 +87,7 @@ def _schedule_update(self): @callback def do_update(): """Really update.""" - self.hass.async_add_job(self.async_update_ha_state) + self.async_write_ha_state() self._update_scheduled = False self._update_scheduled = True diff --git a/homeassistant/core.py b/homeassistant/core.py index 9265c57bbf349e..d9155ece2d3e66 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -152,7 +152,7 @@ class CoreState(enum.Enum): starting = "STARTING" running = "RUNNING" stopping = "STOPPING" - writing_data = "WRITING_DATA" + final_write = "FINAL_WRITE" def __str__(self) -> str: """Return the event.""" @@ -414,7 +414,7 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: # regardless of the state of the loop. if self.state == CoreState.not_running: # just ignore return - if self.state == CoreState.stopping or self.state == CoreState.writing_data: + if self.state == CoreState.stopping or self.state == CoreState.final_write: _LOGGER.info("async_stop called twice: ignored") return if self.state == CoreState.starting: @@ -428,7 +428,7 @@ async def async_stop(self, exit_code: int = 0, *, force: bool = False) -> None: await self.async_block_till_done() # stage 2 - self.state = CoreState.writing_data + self.state = CoreState.final_write self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) await self.async_block_till_done() diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index dd0342a06a31fa..e00cd1b5936b34 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -31,6 +31,7 @@ "elkm1", "emulated_roku", "esphome", + "flunearyou", "freebox", "garmin_connect", "gdacs", @@ -121,6 +122,7 @@ "unifi", "upnp", "velbus", + "vera", "vesync", "vilfo", "vizio", diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 4cbb7a23496c5c..bf6db55a2da4ae 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -444,7 +444,7 @@ async def _async_add_entity( await entity.async_internal_added_to_hass() await entity.async_added_to_hass() - await entity.async_update_ha_state() + entity.async_write_ha_state() async def async_reset(self) -> None: """Remove all entities and reset data. diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 0757770d2f7885..d57d3ad99207a3 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -4,10 +4,7 @@ import logging from typing import Any, Awaitable, Dict, List, Optional, Set, cast -from homeassistant.const import ( - EVENT_HOMEASSISTANT_FINAL_WRITE, - EVENT_HOMEASSISTANT_START, -) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( CoreState, HomeAssistant, @@ -187,9 +184,7 @@ def _async_dump_states(*_: Any) -> None: async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL) # Dump states when stopping hass - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_FINAL_WRITE, _async_dump_states - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states) @callback def async_restore_entity_added(self, entity_id: str) -> None: diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 145bb42af5bfe6..c724b9e890d01a 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -36,7 +36,6 @@ Context, HomeAssistant, callback, - is_callback, ) from homeassistant.helpers import ( condition, @@ -679,10 +678,7 @@ def __init__( def _changed(self): if self.change_listener: - if is_callback(self.change_listener): - self.change_listener() - else: - self._hass.async_add_job(self.change_listener) + self._hass.async_run_job(self.change_listener) @property def is_running(self) -> bool: diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 5885aa01e6fbf9..00df728fb36d08 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,7 +6,7 @@ from typing import Any, Callable, Dict, List, Optional, Type, Union from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass from homeassistant.util import json as json_util @@ -72,7 +72,7 @@ def __init__( self._private = private self._data: Optional[Dict[str, Any]] = None self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None - self._unsub_stop_listener: Optional[CALLBACK_TYPE] = None + self._unsub_final_write_listener: Optional[CALLBACK_TYPE] = None self._write_lock = asyncio.Lock() self._load_task: Optional[asyncio.Future] = None self._encoder = encoder @@ -132,7 +132,12 @@ async def async_save(self, data: Union[Dict, List]) -> None: self._data = {"version": self.version, "key": self.key, "data": data} self._async_cleanup_delay_listener() - self._async_cleanup_stop_listener() + self._async_cleanup_final_write_listener() + + if self.hass.state == CoreState.stopping: + self._async_ensure_final_write_listener() + return + await self._async_handle_write_data() @callback @@ -141,27 +146,31 @@ def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> N self._data = {"version": self.version, "key": self.key, "data_func": data_func} self._async_cleanup_delay_listener() + self._async_cleanup_final_write_listener() + + if self.hass.state == CoreState.stopping: + self._async_ensure_final_write_listener() + return self._unsub_delay_listener = async_call_later( self.hass, delay, self._async_callback_delayed_write ) - - self._async_ensure_stop_listener() + self._async_ensure_final_write_listener() @callback - def _async_ensure_stop_listener(self): + def _async_ensure_final_write_listener(self): """Ensure that we write if we quit before delay has passed.""" - if self._unsub_stop_listener is None: - self._unsub_stop_listener = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_stop_write + if self._unsub_final_write_listener is None: + self._unsub_final_write_listener = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_final_write ) @callback - def _async_cleanup_stop_listener(self): + def _async_cleanup_final_write_listener(self): """Clean up a stop listener.""" - if self._unsub_stop_listener is not None: - self._unsub_stop_listener() - self._unsub_stop_listener = None + if self._unsub_final_write_listener is not None: + self._unsub_final_write_listener() + self._unsub_final_write_listener = None @callback def _async_cleanup_delay_listener(self): @@ -172,13 +181,17 @@ def _async_cleanup_delay_listener(self): async def _async_callback_delayed_write(self, _now): """Handle a delayed write callback.""" + # catch the case where a call is scheduled and then we stop Home Assistant + if self.hass.state == CoreState.stopping: + self._async_ensure_final_write_listener() + return self._unsub_delay_listener = None - self._async_cleanup_stop_listener() + self._async_cleanup_final_write_listener() await self._async_handle_write_data() - async def _async_callback_stop_write(self, _event): - """Handle a write because Home Assistant is stopping.""" - self._unsub_stop_listener = None + async def _async_callback_final_write(self, _event): + """Handle a write because Home Assistant is in final write state.""" + self._unsub_final_write_listener = None self._async_cleanup_delay_listener() await self._async_handle_write_data() diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f450fb6283c936..3258f0fb2f2991 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.15 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.24.5 +zeroconf==0.25.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index d3d1f669df9502..ed8a0dd98e1986 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,10 +35,10 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==2.7.0 +HAP-python==2.8.0 # homeassistant.components.mastodon -Mastodon.py==1.5.0 +Mastodon.py==1.5.1 # homeassistant.components.orangepi_gpio OPi.GPIO==0.4.0 @@ -447,7 +447,7 @@ deluge-client==1.7.1 denonavr==0.8.1 # homeassistant.components.directv -directv==0.2.0 +directv==0.3.0 # homeassistant.components.discogs discogs_client==2.2.2 @@ -474,7 +474,7 @@ dsmr_parser==0.18 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.32 +dynalite_devices==0.1.39 # homeassistant.components.rainforest_eagle eagle200_reader==0.2.4 @@ -611,7 +611,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.1 # homeassistant.components.gios -gios==0.0.5 +gios==0.1.1 # homeassistant.components.gitter gitterpy==0.1.7 @@ -840,7 +840,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.3 +luftdaten==0.6.4 # homeassistant.components.lupusec lupupy==0.0.18 @@ -1281,7 +1281,7 @@ pyflic-homeassistant==0.4.dev0 pyflume==0.3.0 # homeassistant.components.flunearyou -pyflunearyou==1.0.3 +pyflunearyou==1.0.7 # homeassistant.components.futurenow pyfnip==0.2 @@ -1336,7 +1336,7 @@ pyintesishome==1.7.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.8.1 +pyipp==0.8.3 # homeassistant.components.iqvia pyiqvia==0.2.1 @@ -1465,7 +1465,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.5b1 +pyotgw==0.6b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -1638,7 +1638,7 @@ python-juicenet==0.1.6 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.4.8 +python-miio==0.5.0.1 # homeassistant.components.mpd python-mpd2==1.0.0 @@ -1738,7 +1738,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.44 +pyvizio==0.1.45 # homeassistant.components.velux pyvlx==0.2.12 @@ -2173,7 +2173,7 @@ youtube_dl==2020.03.24 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.24.5 +zeroconf==0.25.0 # homeassistant.components.zha zha-quirks==0.0.38 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24986a574a17e7..bc8276cf458754 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==2.7.0 +HAP-python==2.8.0 # homeassistant.components.mobile_app # homeassistant.components.owntracks @@ -178,7 +178,7 @@ defusedxml==0.6.0 denonavr==0.8.1 # homeassistant.components.directv -directv==0.2.0 +directv==0.3.0 # homeassistant.components.updater distro==1.4.0 @@ -190,7 +190,7 @@ doorbirdpy==2.0.8 dsmr_parser==0.18 # homeassistant.components.dynalite -dynalite_devices==0.1.32 +dynalite_devices==0.1.39 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 @@ -243,7 +243,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.1 # homeassistant.components.gios -gios==0.0.5 +gios==0.1.1 # homeassistant.components.glances glances_api==0.2.0 @@ -329,7 +329,7 @@ libsoundtouch==0.7.2 logi_circle==0.2.2 # homeassistant.components.luftdaten -luftdaten==0.6.3 +luftdaten==0.6.4 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 @@ -493,6 +493,9 @@ pyeverlights==0.1.0 # homeassistant.components.fido pyfido==2.1.1 +# homeassistant.components.flunearyou +pyflunearyou==1.0.7 + # homeassistant.components.fritzbox pyfritzhome==0.4.0 @@ -519,7 +522,7 @@ pyicloud==0.9.6.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.8.1 +pyipp==0.8.3 # homeassistant.components.iqvia pyiqvia==0.2.1 @@ -570,7 +573,7 @@ pyopenuv==1.0.9 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==0.5b1 +pyotgw==0.6b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -617,7 +620,7 @@ python-forecastio==1.4.0 python-izone==1.1.2 # homeassistant.components.xiaomi_miio -python-miio==0.4.8 +python-miio==0.5.0.1 # homeassistant.components.nest python-nest==4.1.0 @@ -647,7 +650,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.44 +pyvizio==0.1.45 # homeassistant.components.html5 pywebpush==1.9.2 @@ -795,7 +798,7 @@ ya_ma==0.3.8 yahooweather==0.10 # homeassistant.components.zeroconf -zeroconf==0.24.5 +zeroconf==0.25.0 # homeassistant.components.zha zha-quirks==0.0.38 diff --git a/tests/common.py b/tests/common.py index 8fdcc9b8f860ce..f39d458bbe0dfd 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,6 +14,8 @@ from unittest.mock import MagicMock, Mock, patch import uuid +from aiohttp.test_utils import unused_port as get_test_instance_port # noqa + from homeassistant import auth, config_entries, core as ha, loader from homeassistant.auth import ( auth_store, @@ -37,7 +39,6 @@ EVENT_PLATFORM_DISCOVERED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, - SERVER_PORT, STATE_OFF, STATE_ON, ) @@ -59,7 +60,6 @@ from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.yaml.loader as yaml_loader -_TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) INSTANCES = [] CLIENT_ID = "https://example.com/app" @@ -217,18 +217,6 @@ def clear_instance(event): return hass -def get_test_instance_port(): - """Return unused port for running test instance. - - The socket that holds the default port does not get released when we stop - HA in a different test case. Until I have figured out what is going on, - let's run each test on a different port. - """ - global _TEST_INSTANCE_PORT - _TEST_INSTANCE_PORT += 1 - return _TEST_INSTANCE_PORT - - def async_mock_service(hass, domain, service, schema=None): """Set up a fake service & return a calls log list to this service.""" calls = [] @@ -1017,7 +1005,7 @@ async def flush_store(store): if store._data is None: return - store._async_cleanup_stop_listener() + store._async_cleanup_final_write_listener() store._async_cleanup_delay_listener() await store._async_handle_write_data() @@ -1030,7 +1018,7 @@ async def get_system_health_info(hass, domain): def mock_integration(hass, module): """Mock an integration.""" integration = loader.Integration( - hass, f"homeassistant.components.{module.DOMAIN}", None, module.mock_manifest(), + hass, f"homeassistant.components.{module.DOMAIN}", None, module.mock_manifest() ) _LOGGER.info("Adding mock integration: %s", module.DOMAIN) diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index b91b815d58e855..62e5ed891ffaeb 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -24,6 +24,18 @@ async def test_password_or_pub_key_required(hass): assert not result +async def test_network_unreachable(hass): + """Test creating an AsusWRT scanner without a pass or pubkey.""" + with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: + AsusWrt().connection.async_connect = mock_coro_func(exception=OSError) + AsusWrt().is_connected = False + result = await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} + ) + assert result + assert hass.data.get(DATA_ASUSWRT, None) is None + + async def test_get_scanner_with_password_no_pubkey(hass): """Test creating an AsusWRT scanner with a password and no pubkey.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py new file mode 100644 index 00000000000000..1e598b358928b4 --- /dev/null +++ b/tests/components/directv/test_remote.py @@ -0,0 +1,130 @@ +"""The tests for the DirecTV remote platform.""" +from typing import Any, List + +from asynctest import patch + +from homeassistant.components.remote import ( + ATTR_COMMAND, + ATTR_DELAY_SECS, + ATTR_DEVICE, + ATTR_NUM_REPEATS, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.components.directv import setup_integration +from tests.test_util.aiohttp import AiohttpClientMocker + +ATTR_UNIQUE_ID = "unique_id" +CLIENT_ENTITY_ID = f"{REMOTE_DOMAIN}.client" +MAIN_ENTITY_ID = f"{REMOTE_DOMAIN}.host" +UNAVAILABLE_ENTITY_ID = f"{REMOTE_DOMAIN}.unavailable_client" + +# pylint: disable=redefined-outer-name + + +async def async_send_command( + hass: HomeAssistantType, + command: List[str], + entity_id: Any = ENTITY_MATCH_ALL, + device: str = None, + num_repeats: str = None, + delay_secs: str = None, +) -> None: + """Send a command to a device.""" + data = {ATTR_COMMAND: command} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + if device: + data[ATTR_DEVICE] = device + + if num_repeats: + data[ATTR_NUM_REPEATS] = num_repeats + + if delay_secs: + data[ATTR_DELAY_SECS] = delay_secs + + await hass.services.async_call(REMOTE_DOMAIN, SERVICE_SEND_COMMAND, data) + + +async def async_turn_on( + hass: HomeAssistantType, entity_id: Any = ENTITY_MATCH_ALL +) -> None: + """Turn on device.""" + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_ON, data) + + +async def async_turn_off( + hass: HomeAssistantType, entity_id: Any = ENTITY_MATCH_ALL +) -> None: + """Turn off remote.""" + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_OFF, data) + + +async def test_setup( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with basic config.""" + await setup_integration(hass, aioclient_mock) + assert hass.states.get(MAIN_ENTITY_ID) + assert hass.states.get(CLIENT_ENTITY_ID) + assert hass.states.get(UNAVAILABLE_ENTITY_ID) + + +async def test_unique_id( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test unique id.""" + await setup_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + main = entity_registry.async_get(MAIN_ENTITY_ID) + assert main.unique_id == "028877455858" + + client = entity_registry.async_get(CLIENT_ENTITY_ID) + assert client.unique_id == "2CA17D1CD30X" + + unavailable_client = entity_registry.async_get(UNAVAILABLE_ENTITY_ID) + assert unavailable_client.unique_id == "9XXXXXXXXXX9" + + +async def test_main_services( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the different services.""" + await setup_integration(hass, aioclient_mock) + + with patch("directv.DIRECTV.remote") as remote_mock: + await async_turn_off(hass, MAIN_ENTITY_ID) + await hass.async_block_till_done() + remote_mock.assert_called_once_with("poweroff", "0") + + with patch("directv.DIRECTV.remote") as remote_mock: + await async_turn_on(hass, MAIN_ENTITY_ID) + await hass.async_block_till_done() + remote_mock.assert_called_once_with("poweron", "0") + + with patch("directv.DIRECTV.remote") as remote_mock: + await async_send_command(hass, ["dash"], MAIN_ENTITY_ID) + await hass.async_block_till_done() + remote_mock.assert_called_once_with("dash", "0") diff --git a/tests/components/dynalite/common.py b/tests/components/dynalite/common.py index 56554efaa071fa..b90e61204441ea 100755 --- a/tests/components/dynalite/common.py +++ b/tests/components/dynalite/common.py @@ -44,6 +44,7 @@ async def create_entity_from_device(hass, device): new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] new_device_func([device]) await hass.async_block_till_done() + return mock_dyn_dev.mock_calls[1][2]["update_device_func"] async def run_service_tests(hass, device, platform, services): diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index 97759e96b699cf..938bc09f59a528 100755 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -61,8 +61,15 @@ async def test_add_devices_then_register(hass): device2.name = "NAME2" device2.unique_id = "unique2" new_device_func([device1, device2]) + device3 = Mock() + device3.category = "switch" + device3.name = "NAME3" + device3.unique_id = "unique3" + new_device_func([device3]) await hass.async_block_till_done() assert hass.states.get("light.name") + assert hass.states.get("switch.name2") + assert hass.states.get("switch.name3") async def test_register_then_add_devices(hass): @@ -89,3 +96,4 @@ async def test_register_then_add_devices(hass): new_device_func([device1, device2]) await hass.async_block_till_done() assert hass.states.get("light.name") + assert hass.states.get("switch.name2") diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index 96e361e260f8ca..1a1cdc16f49a18 100755 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -1,6 +1,7 @@ """Test Dynalite config flow.""" from asynctest import CoroutineMock, patch +import pytest from homeassistant import config_entries from homeassistant.components import dynalite @@ -8,12 +9,20 @@ from tests.common import MockConfigEntry -async def run_flow(hass, connection): +@pytest.mark.parametrize( + "first_con, second_con,exp_type, exp_result, exp_reason", + [ + (True, True, "create_entry", "loaded", ""), + (False, False, "abort", "", "no_connection"), + (True, False, "create_entry", "setup_retry", ""), + ], +) +async def test_flow(hass, first_con, second_con, exp_type, exp_result, exp_reason): """Run a flow with or without errors and return result.""" host = "1.2.3.4" with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", - side_effect=connection, + side_effect=[first_con, second_con], ): result = await hass.config_entries.flow.async_init( dynalite.DOMAIN, @@ -21,35 +30,18 @@ async def run_flow(hass, connection): data={dynalite.CONF_HOST: host}, ) await hass.async_block_till_done() - return result - - -async def test_flow_works(hass): - """Test a successful config flow.""" - result = await run_flow(hass, [True, True]) - assert result["type"] == "create_entry" - assert result["result"].state == "loaded" - - -async def test_flow_setup_fails(hass): - """Test a flow where async_setup fails.""" - result = await run_flow(hass, [False]) - assert result["type"] == "abort" - assert result["reason"] == "no_connection" - - -async def test_flow_setup_fails_in_setup_entry(hass): - """Test a flow where the initial check works but inside setup_entry, the bridge setup fails.""" - result = await run_flow(hass, [True, False]) - assert result["type"] == "create_entry" - assert result["result"].state == "setup_retry" + assert result["type"] == exp_type + if exp_result: + assert result["result"].state == exp_result + if exp_reason: + assert result["reason"] == exp_reason async def test_existing(hass): """Test when the entry exists with the same config.""" host = "1.2.3.4" MockConfigEntry( - domain=dynalite.DOMAIN, unique_id=host, data={dynalite.CONF_HOST: host} + domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host} ).add_to_hass(hass) with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", @@ -81,7 +73,7 @@ async def test_existing_update(hass): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() mock_dyn_dev().configure.assert_called_once() - assert mock_dyn_dev().configure.mock_calls[0][1][0][dynalite.CONF_PORT] == port1 + assert mock_dyn_dev().configure.mock_calls[0][1][0]["port"] == port1 result = await hass.config_entries.flow.async_init( dynalite.DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -89,6 +81,26 @@ async def test_existing_update(hass): ) await hass.async_block_till_done() assert mock_dyn_dev().configure.call_count == 2 - assert mock_dyn_dev().configure.mock_calls[1][1][0][dynalite.CONF_PORT] == port2 + assert mock_dyn_dev().configure.mock_calls[1][1][0]["port"] == port2 assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_two_entries(hass): + """Test when two different entries exist with different hosts.""" + host1 = "1.2.3.4" + host2 = "5.6.7.8" + MockConfigEntry( + domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host1} + ).add_to_hass(hass) + with patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + dynalite.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={dynalite.CONF_HOST: host2}, + ) + assert result["type"] == "create_entry" + assert result["result"].state == "loaded" diff --git a/tests/components/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py new file mode 100644 index 00000000000000..cef4081c60714b --- /dev/null +++ b/tests/components/dynalite/test_cover.py @@ -0,0 +1,100 @@ +"""Test Dynalite cover.""" +from dynalite_devices_lib.cover import DynaliteTimeCoverWithTiltDevice +import pytest + +from .common import ( + ATTR_ARGS, + ATTR_METHOD, + ATTR_SERVICE, + create_entity_from_device, + create_mock_device, + run_service_tests, +) + + +@pytest.fixture +def mock_device(): + """Mock a Dynalite device.""" + mock_dev = create_mock_device("cover", DynaliteTimeCoverWithTiltDevice) + mock_dev.device_class = "blind" + return mock_dev + + +async def test_cover_setup(hass, mock_device): + """Test a successful setup.""" + await create_entity_from_device(hass, mock_device) + entity_state = hass.states.get("cover.name") + assert entity_state.attributes["friendly_name"] == mock_device.name + assert ( + entity_state.attributes["current_position"] + == mock_device.current_cover_position + ) + assert ( + entity_state.attributes["current_tilt_position"] + == mock_device.current_cover_tilt_position + ) + assert entity_state.attributes["device_class"] == mock_device.device_class + await run_service_tests( + hass, + mock_device, + "cover", + [ + {ATTR_SERVICE: "open_cover", ATTR_METHOD: "async_open_cover"}, + {ATTR_SERVICE: "close_cover", ATTR_METHOD: "async_close_cover"}, + {ATTR_SERVICE: "stop_cover", ATTR_METHOD: "async_stop_cover"}, + { + ATTR_SERVICE: "set_cover_position", + ATTR_METHOD: "async_set_cover_position", + ATTR_ARGS: {"position": 50}, + }, + {ATTR_SERVICE: "open_cover_tilt", ATTR_METHOD: "async_open_cover_tilt"}, + {ATTR_SERVICE: "close_cover_tilt", ATTR_METHOD: "async_close_cover_tilt"}, + {ATTR_SERVICE: "stop_cover_tilt", ATTR_METHOD: "async_stop_cover_tilt"}, + { + ATTR_SERVICE: "set_cover_tilt_position", + ATTR_METHOD: "async_set_cover_tilt_position", + ATTR_ARGS: {"tilt_position": 50}, + }, + ], + ) + + +async def test_cover_without_tilt(hass, mock_device): + """Test a cover with no tilt.""" + mock_device.has_tilt = False + await create_entity_from_device(hass, mock_device) + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": "cover.name"}, blocking=True + ) + await hass.async_block_till_done() + mock_device.async_open_cover_tilt.assert_not_called() + + +async def check_cover_position( + hass, update_func, device, closing, opening, closed, expected +): + """Check that a given position behaves correctly.""" + device.is_closing = closing + device.is_opening = opening + device.is_closed = closed + update_func(device) + await hass.async_block_till_done() + entity_state = hass.states.get("cover.name") + assert entity_state.state == expected + + +async def test_cover_positions(hass, mock_device): + """Test that the state updates in the various positions.""" + update_func = await create_entity_from_device(hass, mock_device) + await check_cover_position( + hass, update_func, mock_device, True, False, False, "closing" + ) + await check_cover_position( + hass, update_func, mock_device, False, True, False, "opening" + ) + await check_cover_position( + hass, update_func, mock_device, False, False, True, "closed" + ) + await check_cover_position( + hass, update_func, mock_device, False, False, False, "open" + ) diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index b74fcd64da029a..8e2290a9c407af 100755 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -3,7 +3,8 @@ from asynctest import call, patch -from homeassistant.components import dynalite +import homeassistant.components.dynalite.const as dynalite +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -17,8 +18,7 @@ async def test_empty_config(hass): async def test_async_setup(hass): - """Test a successful setup.""" - host = "1.2.3.4" + """Test a successful setup with all of the different options.""" with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", return_value=True, @@ -30,8 +30,46 @@ async def test_async_setup(hass): dynalite.DOMAIN: { dynalite.CONF_BRIDGES: [ { - dynalite.CONF_HOST: host, - dynalite.CONF_AREA: {"1": {dynalite.CONF_NAME: "Name"}}, + CONF_HOST: "1.2.3.4", + CONF_PORT: 1234, + dynalite.CONF_AUTO_DISCOVER: True, + dynalite.CONF_POLL_TIMER: 5.5, + dynalite.CONF_AREA: { + "1": { + CONF_NAME: "Name1", + dynalite.CONF_CHANNEL: {"4": {}}, + dynalite.CONF_NO_DEFAULT: True, + }, + "2": {CONF_NAME: "Name2"}, + "3": { + CONF_NAME: "Name3", + dynalite.CONF_TEMPLATE: CONF_ROOM, + }, + "4": { + CONF_NAME: "Name4", + dynalite.CONF_TEMPLATE: dynalite.CONF_TIME_COVER, + }, + }, + dynalite.CONF_DEFAULT: {dynalite.CONF_FADE: 2.3}, + dynalite.CONF_ACTIVE: dynalite.ACTIVE_INIT, + dynalite.CONF_PRESET: { + "5": {CONF_NAME: "pres5", dynalite.CONF_FADE: 4.5} + }, + dynalite.CONF_TEMPLATE: { + CONF_ROOM: { + dynalite.CONF_ROOM_ON: 6, + dynalite.CONF_ROOM_OFF: 7, + }, + dynalite.CONF_TIME_COVER: { + dynalite.CONF_OPEN_PRESET: 8, + dynalite.CONF_CLOSE_PRESET: 9, + dynalite.CONF_STOP_PRESET: 10, + dynalite.CONF_CHANNEL_COVER: 3, + dynalite.CONF_DURATION: 2.2, + dynalite.CONF_TILT_TIME: 3.3, + dynalite.CONF_DEVICE_CLASS: "awning", + }, + }, } ] } @@ -41,6 +79,35 @@ async def test_async_setup(hass): assert len(hass.config_entries.async_entries(dynalite.DOMAIN)) == 1 +async def test_async_setup_bad_config1(hass): + """Test a successful with bad config on templates.""" + with patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=True, + ): + assert not await async_setup_component( + hass, + dynalite.DOMAIN, + { + dynalite.DOMAIN: { + dynalite.CONF_BRIDGES: [ + { + CONF_HOST: "1.2.3.4", + dynalite.CONF_AREA: { + "1": { + dynalite.CONF_TEMPLATE: dynalite.CONF_TIME_COVER, + CONF_NAME: "Name", + dynalite.CONF_ROOM_ON: 7, + } + }, + } + ] + } + }, + ) + await hass.async_block_till_done() + + async def test_async_setup_bad_config2(hass): """Test a successful with bad config on numbers.""" host = "1.2.3.4" @@ -55,8 +122,8 @@ async def test_async_setup_bad_config2(hass): dynalite.DOMAIN: { dynalite.CONF_BRIDGES: [ { - dynalite.CONF_HOST: host, - dynalite.CONF_AREA: {"WRONG": {dynalite.CONF_NAME: "Name"}}, + CONF_HOST: host, + dynalite.CONF_AREA: {"WRONG": {CONF_NAME: "Name"}}, } ] } @@ -69,7 +136,7 @@ async def test_async_setup_bad_config2(hass): async def test_unload_entry(hass): """Test being able to unload an entry.""" host = "1.2.3.4" - entry = MockConfigEntry(domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host}) + entry = MockConfigEntry(domain=dynalite.DOMAIN, data={CONF_HOST: host}) entry.add_to_hass(hass) with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", diff --git a/tests/components/flunearyou/__init__.py b/tests/components/flunearyou/__init__.py new file mode 100644 index 00000000000000..21252facd75910 --- /dev/null +++ b/tests/components/flunearyou/__init__.py @@ -0,0 +1 @@ +"""Define tests for the flunearyou component.""" diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py new file mode 100644 index 00000000000000..21fcb4798db387 --- /dev/null +++ b/tests/components/flunearyou/test_config_flow.py @@ -0,0 +1,87 @@ +"""Define tests for the flunearyou config flow.""" +from asynctest import patch +from pyflunearyou.errors import FluNearYouError + +from homeassistant import data_entry_flow +from homeassistant.components.flunearyou import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import MockConfigEntry + + +async def test_duplicate_error(hass): + """Test that an error is shown when duplicates are added.""" + conf = {CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"} + + MockConfigEntry( + domain=DOMAIN, unique_id="51.528308, -0.3817765", data=conf + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_general_error(hass): + """Test that an error is shown on a library error.""" + conf = {CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"} + + with patch( + "pyflunearyou.cdc.CdcReport.status_by_coordinates", side_effect=FluNearYouError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["errors"] == {"base": "general_error"} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = {CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"} + + with patch( + "homeassistant.components.flunearyou.async_setup_entry", return_value=True + ), patch("pyflunearyou.cdc.CdcReport.status_by_coordinates"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "51.528308, -0.3817765" + assert result["data"] == { + CONF_LATITUDE: "51.528308", + CONF_LONGITUDE: "-0.3817765", + } + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = {CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"} + + with patch( + "homeassistant.components.flunearyou.async_setup_entry", return_value=True + ), patch("pyflunearyou.cdc.CdcReport.status_by_coordinates"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "51.528308, -0.3817765" + assert result["data"] == { + CONF_LATITUDE: "51.528308", + CONF_LONGITUDE: "-0.3817765", + } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index ec1848bf1ed5d8..d0ed9a9d33c241 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1532,7 +1532,8 @@ async def test_openclose_cover_no_position(hass): @pytest.mark.parametrize( - "device_class", (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE) + "device_class", + (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE), ) async def test_openclose_cover_secure(hass, device_class): """Test OpenClose trait support for cover domain.""" diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 18c0825b6a1dcb..30421756d22514 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -25,8 +25,7 @@ async def test_user_form(hass): harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ), patch( "homeassistant.components.harmony.async_setup", return_value=True ) as mock_setup, patch( @@ -53,8 +52,7 @@ async def test_form_import(hass): harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ), patch( "homeassistant.components.harmony.async_setup", return_value=True ) as mock_setup, patch( @@ -68,9 +66,11 @@ async def test_form_import(hass): "name": "friend", "activity": "Watch TV", "delay_secs": 0.9, + "unique_id": "555234534543", }, ) + assert result["result"].unique_id == "555234534543" assert result["type"] == "create_entry" assert result["title"] == "friend" assert result["data"] == { @@ -94,8 +94,7 @@ async def test_form_ssdp(hass): harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -114,8 +113,7 @@ async def test_form_ssdp(hass): } with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ), patch( "homeassistant.components.harmony.async_setup", return_value=True ) as mock_setup, patch( @@ -141,8 +139,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - side_effect=CannotConnect, + "homeassistant.components.harmony.util.HarmonyAPI", side_effect=CannotConnect, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 8834f730bcea91..888ad87a848afa 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,6 +1,9 @@ """Test different accessory types: Lights.""" from collections import namedtuple +from asynctest import patch +from pyhap.accessory_driver import AccessoryDriver +from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest from homeassistant.components.homekit.const import ATTR_VALUE @@ -30,6 +33,15 @@ from tests.components.homekit.common import patch_debounce +@pytest.fixture +def driver(): + """Patch AccessoryDriver without zeroconf or HAPServer.""" + with patch("pyhap.accessory_driver.HAPServer"), patch( + "pyhap.accessory_driver.Zeroconf" + ), patch("pyhap.accessory_driver.AccessoryDriver.persist"): + yield AccessoryDriver() + + @pytest.fixture(scope="module") def cls(): """Patch debounce decorator during import of type_lights.""" @@ -43,15 +55,16 @@ def cls(): patcher.stop() -async def test_light_basic(hass, hk_driver, cls, events): +async def test_light_basic(hass, hk_driver, cls, events, driver): """Test light with char state.""" entity_id = "light.demo" hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) - assert acc.aid == 2 + assert acc.aid == 1 assert acc.category == 5 # Lightbulb assert acc.char_on.value == 0 @@ -75,25 +88,43 @@ async def test_light_basic(hass, hk_driver, cls, events): call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_off = async_mock_service(hass, DOMAIN, "turn_off") + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1} + ] + }, + "mock_addr", + ) + await hass.async_add_job(acc.char_on.client_update_value, 1) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] is None + assert events[-1].data[ATTR_VALUE] == "Set state to 1" hass.states.async_set(entity_id, STATE_ON) await hass.async_block_till_done() - await hass.async_add_job(acc.char_on.client_update_value, 0) + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 0} + ] + }, + "mock_addr", + ) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] is None + assert events[-1].data[ATTR_VALUE] == "Set state to 0" -async def test_light_brightness(hass, hk_driver, cls, events): +async def test_light_brightness(hass, hk_driver, cls, events, driver): """Test light with brightness.""" entity_id = "light.demo" @@ -103,11 +134,14 @@ async def test_light_brightness(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] await hass.async_add_job(acc.run) await hass.async_block_till_done() @@ -121,34 +155,88 @@ async def test_light_brightness(hass, hk_driver, cls, events): call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_off = async_mock_service(hass, DOMAIN, "turn_off") - await hass.async_add_job(acc.char_brightness.client_update_value, 20) - await hass.async_add_job(acc.char_on.client_update_value, 1) + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 20, + }, + ] + }, + "mock_addr", + ) await hass.async_block_till_done() assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == f"brightness at 20{UNIT_PERCENTAGE}" + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}" + ) - await hass.async_add_job(acc.char_on.client_update_value, 1) - await hass.async_add_job(acc.char_brightness.client_update_value, 40) + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 40, + }, + ] + }, + "mock_addr", + ) await hass.async_block_till_done() assert call_turn_on[1] assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == f"brightness at 40{UNIT_PERCENTAGE}" + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 40{UNIT_PERCENTAGE}" + ) - await hass.async_add_job(acc.char_on.client_update_value, 1) - await hass.async_add_job(acc.char_brightness.client_update_value, 0) + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 0, + }, + ] + }, + "mock_addr", + ) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 3 - assert events[-1].data[ATTR_VALUE] is None + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 0, brightness at 0{UNIT_PERCENTAGE}" + ) + + # 0 is a special case for homekit, see "Handle Brightness" + # in update_state + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 1 + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 255}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 100 + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 1 -async def test_light_color_temperature(hass, hk_driver, cls, events): +async def test_light_color_temperature(hass, hk_driver, cls, events, driver): """Test light with color temperature.""" entity_id = "light.demo" @@ -158,7 +246,8 @@ async def test_light_color_temperature(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) assert acc.char_color_temperature.value == 153 @@ -169,6 +258,20 @@ async def test_light_color_temperature(hass, hk_driver, cls, events): # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_VALUE: 250, + } + ] + }, + "mock_addr", + ) await hass.async_add_job(acc.char_color_temperature.client_update_value, 250) await hass.async_block_till_done() assert call_turn_on @@ -197,7 +300,7 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event assert not hasattr(acc, "char_color_temperature") -async def test_light_rgb_color(hass, hk_driver, cls, events): +async def test_light_rgb_color(hass, hk_driver, cls, events, driver): """Test light with rgb_color.""" entity_id = "light.demo" @@ -207,7 +310,8 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) assert acc.char_hue.value == 0 assert acc.char_saturation.value == 75 @@ -220,8 +324,26 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") - await hass.async_add_job(acc.char_hue.client_update_value, 145) - await hass.async_add_job(acc.char_saturation.client_update_value, 75) + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id @@ -230,7 +352,7 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" -async def test_light_restore(hass, hk_driver, cls, events): +async def test_light_restore(hass, hk_driver, cls, events, driver): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -250,7 +372,9 @@ async def test_light_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", "light.simple", 2, None) + acc = cls.light(hass, hk_driver, "Light", "light.simple", 1, None) + driver.add_accessory(acc) + assert acc.category == 5 # Lightbulb assert acc.chars == [] assert acc.char_on.value == 0 @@ -259,3 +383,141 @@ async def test_light_restore(hass, hk_driver, cls, events): assert acc.category == 5 # Lightbulb assert acc.chars == ["Brightness"] assert acc.char_on.value == 0 + + +async def test_light_set_brightness_and_color(hass, hk_driver, cls, events, driver): + """Test light with all chars in one go.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + ATTR_BRIGHTNESS: 255, + }, + ) + await hass.async_block_till_done() + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) + + # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the + # brightness to 100 when turning on a light on a freshly booted up server. + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_brightness.value == 100 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 40 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 20, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 + assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) + + assert len(events) == 1 + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, set color at (145, 75)" + ) + + +async def test_light_set_brightness_and_color_temp( + hass, hk_driver, cls, events, driver +): + """Test light with all chars in one go.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP, + ATTR_BRIGHTNESS: 255, + }, + ) + await hass.async_block_till_done() + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) + + # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the + # brightness to 100 when turning on a light on a freshly booted up server. + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_brightness.value == 100 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 40 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 20, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_VALUE: 250, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 + assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 + + assert len(events) == 1 + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, color temperature at 250" + ) diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 250a0fe26a8251..1c40f352ff0180 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -166,4 +166,4 @@ async def test_deprecation_warning(hass, caplog): [State("light.entity_off", "on", {"brightness_pct": 80})], blocking=True ) assert len(turn_on_calls) == 1 - assert DEPRECATION_WARNING in caplog.text + assert DEPRECATION_WARNING % ["brightness_pct"] in caplog.text diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7aa185c2c391e9..60ca91b00520ba 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -33,6 +33,7 @@ mock_device_registry, mock_mqtt_component, mock_registry, + mock_storage, threadsafe_coroutine_factory, ) @@ -54,6 +55,7 @@ def mock_MQTT(): """Make sure connection is established.""" with mock.patch("homeassistant.components.mqtt.MQTT") as mock_MQTT: mock_MQTT.return_value.async_connect.return_value = mock_coro(True) + mock_MQTT.return_value.async_disconnect.return_value = mock_coro(True) yield mock_MQTT @@ -83,12 +85,15 @@ class TestMQTTComponent(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.mock_storage = mock_storage() + self.mock_storage.__enter__() mock_mqtt_component(self.hass) self.calls = [] def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() + self.mock_storage.__exit__(None, None, None) @callback def record_calls(self, *args): @@ -298,12 +303,15 @@ class TestMQTTCallbacks(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.mock_storage = mock_storage() + self.mock_storage.__enter__() mock_mqtt_client(self.hass) self.calls = [] def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() + self.mock_storage.__exit__(None, None, None) @callback def record_calls(self, *args): diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 95f31b6782637a..a9b5656a0b3a9e 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -7,7 +7,7 @@ from homeassistant.const import CONF_PASSWORD from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, mock_coro +from tests.common import get_test_home_assistant, mock_coro, mock_storage class TestMQTT: @@ -16,10 +16,13 @@ class TestMQTT: def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.mock_storage = mock_storage() + self.mock_storage.__enter__() def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() + self.mock_storage.__exit__(None, None, None) @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index f4062458d91e99..36698db87e187d 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -15,6 +15,7 @@ get_test_home_assistant, mock_mqtt_component, mock_state_change_event, + mock_storage, ) @@ -24,11 +25,14 @@ class TestMqttEventStream: def setup_method(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.mock_storage = mock_storage() + self.mock_storage.__enter__() self.mock_mqtt = mock_mqtt_component(self.hass) def teardown_method(self): """Stop everything that was started.""" self.hass.stop() + self.mock_storage.__exit__(None, None, None) def add_eventstream(self, sub_topic=None, pub_topic=None, ignore_event=None): """Add a mqtt_eventstream component.""" diff --git a/tests/components/mqtt_statestream/test_init.py b/tests/components/mqtt_statestream/test_init.py index ffab0e0846f083..af9a721f1a4fbf 100644 --- a/tests/components/mqtt_statestream/test_init.py +++ b/tests/components/mqtt_statestream/test_init.py @@ -9,6 +9,7 @@ get_test_home_assistant, mock_mqtt_component, mock_state_change_event, + mock_storage, ) @@ -18,11 +19,14 @@ class TestMqttStateStream: def setup_method(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.mock_storage = mock_storage() + self.mock_storage.__enter__() self.mock_mqtt = mock_mqtt_component(self.hass) def teardown_method(self): """Stop everything that was started.""" self.hass.stop() + self.mock_storage.__exit__(None, None, None) def add_statestream( self, diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 0adcdb188d08b9..f57ad20f5d5d18 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import patch -from pyotgw import OTGW_ABOUT +from pyotgw.vars import OTGW_ABOUT from serial import SerialException from homeassistant import config_entries, data_entry_flow, setup diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index dccca97a1cc9d1..d9adf6015c90bf 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -543,7 +543,13 @@ def test_level_action_no_template(self): @pytest.mark.parametrize( "expected_level,template", - [(255, "{{255}}"), (None, "{{256}}"), (None, "{{x - 12}}")], + [ + (255, "{{255}}"), + (None, "{{256}}"), + (None, "{{x - 12}}"), + (None, "{{ none }}"), + (None, ""), + ], ) def test_level_template(self, expected_level, template): """Test the template for the level.""" @@ -588,7 +594,14 @@ def test_level_template(self, expected_level, template): @pytest.mark.parametrize( "expected_temp,template", - [(500, "{{500}}"), (None, "{{501}}"), (None, "{{x - 12}}")], + [ + (500, "{{500}}"), + (None, "{{501}}"), + (None, "{{x - 12}}"), + (None, "None"), + (None, "{{ none }}"), + (None, ""), + ], ) def test_temperature_template(self, expected_temp, template): """Test the template for the temperature.""" @@ -894,6 +907,8 @@ def test_color_action_no_template(self): (None, "{{(361, 100)}}"), (None, "{{(360, 101)}}"), (None, "{{x - 12}}"), + (None, ""), + (None, "{{ none }}"), ], ) def test_color_template(self, expected_hs, template): diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index dcf9c36474fcad..dea116b3905a67 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -33,6 +33,7 @@ ATTR_ID, ATTR_NAME, CONF_ENTITY_ID, + EVENT_STATE_CHANGED, SERVICE_RELOAD, ) from homeassistant.core import Context, CoreState @@ -406,6 +407,47 @@ def fake_event_listener(event): assert len(results) == 4 +async def test_state_changed_when_timer_restarted(hass): + """Ensure timer's state changes when it restarted.""" + hass.state = CoreState.starting + + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) + + state = hass.states.get("timer.test1") + assert state + assert state.state == STATUS_IDLE + + results = [] + + def fake_event_listener(event): + """Fake event listener for trigger.""" + results.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, fake_event_listener) + + await hass.services.async_call( + DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"} + ) + await hass.async_block_till_done() + state = hass.states.get("timer.test1") + assert state + assert state.state == STATUS_ACTIVE + + assert results[-1].event_type == EVENT_STATE_CHANGED + assert len(results) == 1 + + await hass.services.async_call( + DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"} + ) + await hass.async_block_till_done() + state = hass.states.get("timer.test1") + assert state + assert state.state == STATUS_ACTIVE + + assert results[-1].event_type == EVENT_STATE_CHANGED + assert len(results) == 2 + + async def test_load_from_storage(hass, storage_setup): """Test set up from storage.""" assert await storage_setup() diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 365f84cf7a32f2..e4bdd140faa92c 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -155,11 +155,15 @@ def mock_light(test_features={}, test_state={}, n=0): """Mock a tradfri light.""" mock_light_data = Mock(**test_state) + dev_info_mock = MagicMock() + dev_info_mock.manufacturer = "manufacturer" + dev_info_mock.model_number = "model" + dev_info_mock.firmware_version = "1.2.3" mock_light = Mock( id=f"mock-light-id-{n}", reachable=True, observe=Mock(), - device_info=MagicMock(), + device_info=dev_info_mock, ) mock_light.name = f"tradfri_light_{n}" diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 62c4bc3a065511..ab5d562ffc802c 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1,14 +1,12 @@ """The tests for the TTS component.""" import ctypes import os -import shutil from unittest.mock import PropertyMock, patch import pytest -import requests +import yarl from homeassistant.components.demo.tts import DemoProvider -import homeassistant.components.http as http from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, @@ -17,260 +15,267 @@ SERVICE_PLAY_MEDIA, ) import homeassistant.components.tts as tts -from homeassistant.setup import async_setup_component, setup_component - -from tests.common import ( - assert_setup_component, - get_test_home_assistant, - get_test_instance_port, - mock_service, - mock_storage, -) +from homeassistant.components.tts import _get_cache_files +from homeassistant.setup import async_setup_component +from tests.common import assert_setup_component, async_mock_service -@pytest.fixture(autouse=True) -def mutagen_mock(): - """Mock writing tags.""" - with patch( - "homeassistant.components.tts.SpeechManager.write_tags", - side_effect=lambda *args: args[1], - ): - yield +def relative_url(url): + """Convert an absolute url to a relative one.""" + return str(yarl.URL(url).relative()) -class TestTTS: - """Test the Google speech component.""" - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.demo_provider = DemoProvider("en") - self.default_tts_cache = self.hass.config.path(tts.DEFAULT_CACHE_DIR) - self.mock_storage = mock_storage() - self.mock_storage.__enter__() +@pytest.fixture +def demo_provider(): + """Demo TTS provider.""" + return DemoProvider("en") - setup_component( - self.hass, - http.DOMAIN, - {http.DOMAIN: {http.CONF_SERVER_PORT: get_test_instance_port()}}, - ) - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() - self.mock_storage.__exit__(None, None, None) +@pytest.fixture(autouse=True) +def mock_get_cache_files(): + """Mock the list TTS cache function.""" + with patch( + "homeassistant.components.tts._get_cache_files", return_value={} + ) as mock_cache_files: + yield mock_cache_files - if os.path.isdir(self.default_tts_cache): - shutil.rmtree(self.default_tts_cache) - def test_setup_component_demo(self): - """Set up the demo platform with defaults.""" - config = {tts.DOMAIN: {"platform": "demo"}} +@pytest.fixture(autouse=True) +def mock_init_cache_dir(): + """Mock the TTS cache dir in memory.""" + with patch( + "homeassistant.components.tts._init_tts_cache_dir", + side_effect=lambda hass, cache_dir: hass.config.path(cache_dir), + ) as mock_cache_dir: + yield mock_cache_dir - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - assert self.hass.services.has_service(tts.DOMAIN, "demo_say") - assert self.hass.services.has_service(tts.DOMAIN, "clear_cache") +@pytest.fixture +def empty_cache_dir(tmp_path, mock_init_cache_dir, mock_get_cache_files): + """Mock the TTS cache dir with empty dir.""" + mock_init_cache_dir.side_effect = None + mock_init_cache_dir.return_value = str(tmp_path) - @patch("os.mkdir", side_effect=OSError(2, "No access")) - def test_setup_component_demo_no_access_cache_folder(self, mock_mkdir): - """Set up the demo platform with defaults.""" - config = {tts.DOMAIN: {"platform": "demo"}} + # Restore original get cache files behavior, we're working with a real dir. + mock_get_cache_files.side_effect = _get_cache_files - assert not setup_component(self.hass, tts.DOMAIN, config) + return tmp_path - assert not self.hass.services.has_service(tts.DOMAIN, "demo_say") - assert not self.hass.services.has_service(tts.DOMAIN, "clear_cache") - def test_setup_component_and_test_service(self): - """Set up the demo platform and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +@pytest.fixture(autouse=True) +def mutagen_mock(): + """Mock writing tags.""" + with patch( + "homeassistant.components.tts.SpeechManager.write_tags", + side_effect=lambda *args: args[1], + ): + yield - config = {tts.DOMAIN: {"platform": "demo"}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) +async def test_setup_component_demo(hass): + """Set up the demo platform with defaults.""" + config = {tts.DOMAIN: {"platform": "demo"}} - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( - self.hass.config.api.base_url - ) - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) + assert hass.services.has_service(tts.DOMAIN, "demo_say") + assert hass.services.has_service(tts.DOMAIN, "clear_cache") - def test_setup_component_and_test_service_with_config_language(self): - """Set up the demo platform and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo", "language": "de"}} +async def test_setup_component_demo_no_access_cache_folder(hass, mock_init_cache_dir): + """Set up the demo platform with defaults.""" + config = {tts.DOMAIN: {"platform": "demo"}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + mock_init_cache_dir.side_effect = OSError(2, "No access") + assert not await async_setup_component(hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + assert not hass.services.has_service(tts.DOMAIN, "demo_say") + assert not hass.services.has_service(tts.DOMAIN, "clear_cache") - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( - self.hass.config.api.base_url - ) - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", - ) - ) - def test_setup_component_and_test_service_with_wrong_conf_language(self): - """Set up the demo platform and call service with wrong config.""" - config = {tts.DOMAIN: {"platform": "demo", "language": "ru"}} +async def test_setup_component_and_test_service(hass, empty_cache_dir): + """Set up the demo platform and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - with assert_setup_component(0, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + config = {tts.DOMAIN: {"platform": "demo"}} - def test_setup_component_and_test_service_with_service_language(self): - """Set up the demo platform and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) - config = {tts.DOMAIN: {"platform": "demo"}} + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( + hass.config.api.base_url + ) + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() + + +async def test_setup_component_and_test_service_with_config_language( + hass, empty_cache_dir +): + """Set up the demo platform and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = {tts.DOMAIN: {"platform": "demo", "language": "de"}} + + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( + hass.config.api.base_url + ) + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" + ).is_file() - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_LANGUAGE: "de", - }, - ) - self.hass.block_till_done() +async def test_setup_component_and_test_service_with_wrong_conf_language(hass): + """Set up the demo platform and call service with wrong config.""" + config = {tts.DOMAIN: {"platform": "demo", "language": "ru"}} - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( - self.hass.config.api.base_url - ) - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", - ) - ) + with assert_setup_component(0, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - def test_setup_component_test_service_with_wrong_service_language(self): - """Set up the demo platform and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo"}} +async def test_setup_component_and_test_service_with_service_language( + hass, empty_cache_dir +): + """Set up the demo platform and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + config = {tts.DOMAIN: {"platform": "demo"}} - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_LANGUAGE: "lang", - }, - ) - self.hass.block_till_done() + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_LANGUAGE: "de", + }, + blocking=True, + ) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( + hass.config.api.base_url + ) + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" + ).is_file() - assert len(calls) == 0 - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3", - ) - ) - def test_setup_component_and_test_service_with_service_options(self): - """Set up the demo platform and call service with options.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +async def test_setup_component_test_service_with_wrong_service_language( + hass, empty_cache_dir +): + """Set up the demo platform and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo"}} + config = {tts.DOMAIN: {"platform": "demo"}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_LANGUAGE: "lang", + }, + blocking=True, + ) + assert len(calls) == 0 + assert not ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3" + ).is_file() - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_LANGUAGE: "de", - tts.ATTR_OPTIONS: {"voice": "alex"}, - }, - ) - self.hass.block_till_done() - opt_hash = ctypes.c_size_t(hash(frozenset({"voice": "alex"}))).value +async def test_setup_component_and_test_service_with_service_options( + hass, empty_cache_dir +): + """Set up the demo platform and call service with options.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( - self.hass.config.api.base_url, opt_hash - ) - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( - opt_hash - ), - ) - ) + config = {tts.DOMAIN: {"platform": "demo"}} - @patch( - "homeassistant.components.demo.tts.DemoProvider.default_options", - new_callable=PropertyMock(return_value={"voice": "alex"}), + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_LANGUAGE: "de", + tts.ATTR_OPTIONS: {"voice": "alex"}, + }, + blocking=True, ) - def test_setup_component_and_test_with_service_options_def(self, def_mock): - """Set up the demo platform and call service with default options.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + opt_hash = ctypes.c_size_t(hash(frozenset({"voice": "alex"}))).value + + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( + hass.config.api.base_url, opt_hash + ) + assert ( + empty_cache_dir + / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" + ).is_file() + - config = {tts.DOMAIN: {"platform": "demo"}} +async def test_setup_component_and_test_with_service_options_def(hass, empty_cache_dir): + """Set up the demo platform and call service with default options.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + config = {tts.DOMAIN: {"platform": "demo"}} + + with assert_setup_component(1, tts.DOMAIN), patch( + "homeassistant.components.demo.tts.DemoProvider.default_options", + new_callable=PropertyMock(return_value={"voice": "alex"}), + ): + assert await async_setup_component(hass, tts.DOMAIN, config) - self.hass.services.call( + await hass.services.async_call( tts.DOMAIN, "demo_say", { @@ -278,9 +283,8 @@ def test_setup_component_and_test_with_service_options_def(self, def_mock): tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", }, + blocking=True, ) - self.hass.block_till_done() - opt_hash = ctypes.c_size_t(hash(frozenset({"voice": "alex"}))).value assert len(calls) == 1 @@ -288,362 +292,341 @@ def test_setup_component_and_test_with_service_options_def(self, def_mock): assert calls[0].data[ ATTR_MEDIA_CONTENT_ID ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( - self.hass.config.api.base_url, opt_hash + hass.config.api.base_url, opt_hash ) assert os.path.isfile( os.path.join( - self.default_tts_cache, + empty_cache_dir, "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( opt_hash ), ) ) - def test_setup_component_and_test_service_with_service_options_wrong(self): - """Set up the demo platform and call service with wrong options.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo"}} +async def test_setup_component_and_test_service_with_service_options_wrong( + hass, empty_cache_dir +): + """Set up the demo platform and call service with wrong options.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + config = {tts.DOMAIN: {"platform": "demo"}} - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_LANGUAGE: "de", - tts.ATTR_OPTIONS: {"speed": 1}, - }, - ) - self.hass.block_till_done() + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_LANGUAGE: "de", + tts.ATTR_OPTIONS: {"speed": 1}, + }, + blocking=True, + ) + opt_hash = ctypes.c_size_t(hash(frozenset({"speed": 1}))).value - opt_hash = ctypes.c_size_t(hash(frozenset({"speed": 1}))).value + assert len(calls) == 0 + assert not ( + empty_cache_dir + / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" + ).is_file() - assert len(calls) == 0 - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( - opt_hash - ), - ) - ) - def test_setup_component_and_test_service_with_base_url_set(self): - """Set up the demo platform with ``base_url`` set and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +async def test_setup_component_and_test_service_with_base_url_set(hass): + """Set up the demo platform with ``base_url`` set and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo", "base_url": "http://fnord"}} + config = {tts.DOMAIN: {"platform": "demo", "base_url": "http://fnord"}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] == "http://fnord" + "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" + "_en_-_demo.mp3" + ) - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert ( - calls[0].data[ATTR_MEDIA_CONTENT_ID] == "http://fnord" - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - "_en_-_demo.mp3" - ) - def test_setup_component_and_test_service_clear_cache(self): - """Set up the demo platform and call service clear cache.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +async def test_setup_component_and_test_service_clear_cache(hass, empty_cache_dir): + """Set up the demo platform and call service clear cache.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo"}} + config = {tts.DOMAIN: {"platform": "demo"}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + # To make sure the file is persisted + await hass.async_block_till_done() + assert len(calls) == 1 + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() + + await hass.services.async_call( + tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {}, blocking=True + ) - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + assert not ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() - assert len(calls) == 1 - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) - self.hass.services.call(tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {}) - self.hass.block_till_done() +async def test_setup_component_and_test_service_with_receive_voice( + hass, demo_provider, hass_client +): + """Set up the demo platform and call service and receive voice.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) + config = {tts.DOMAIN: {"platform": "demo"}} - def test_setup_component_and_test_service_with_receive_voice(self): - """Set up the demo platform and call service and receive voice.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - config = {tts.DOMAIN: {"platform": "demo"}} + client = await hass_client() - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + + req = await client.get(relative_url(calls[0].data[ATTR_MEDIA_CONTENT_ID])) + _, demo_data = demo_provider.get_tts_audio("bla", "en") + demo_data = tts.SpeechManager.write_tags( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", + demo_data, + demo_provider, + "AI person is in front of your door.", + "en", + None, + ) + assert req.status == 200 + assert await req.read() == demo_data - self.hass.start() - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() +async def test_setup_component_and_test_service_with_receive_voice_german( + hass, demo_provider, hass_client +): + """Set up the demo platform and call service and receive voice.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - assert len(calls) == 1 - req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) - _, demo_data = self.demo_provider.get_tts_audio("bla", "en") - demo_data = tts.SpeechManager.write_tags( - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - demo_data, - self.demo_provider, - "AI person is in front of your door.", - "en", - None, - ) - assert req.status_code == 200 - assert req.content == demo_data + config = {tts.DOMAIN: {"platform": "demo", "language": "de"}} - def test_setup_component_and_test_service_with_receive_voice_german(self): - """Set up the demo platform and call service and receive voice.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - config = {tts.DOMAIN: {"platform": "demo", "language": "de"}} + client = await hass_client() - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + req = await client.get(relative_url(calls[0].data[ATTR_MEDIA_CONTENT_ID])) + _, demo_data = demo_provider.get_tts_audio("bla", "de") + demo_data = tts.SpeechManager.write_tags( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", + demo_data, + demo_provider, + "There is someone at the door.", + "de", + None, + ) + assert req.status == 200 + assert await req.read() == demo_data - self.hass.start() - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() +async def test_setup_component_and_web_view_wrong_file(hass, hass_client): + """Set up the demo platform and receive wrong file from web.""" + config = {tts.DOMAIN: {"platform": "demo"}} - assert len(calls) == 1 - req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) - _, demo_data = self.demo_provider.get_tts_audio("bla", "de") - demo_data = tts.SpeechManager.write_tags( - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", - demo_data, - self.demo_provider, - "There is someone at the door.", - "de", - None, - ) - assert req.status_code == 200 - assert req.content == demo_data + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - def test_setup_component_and_web_view_wrong_file(self): - """Set up the demo platform and receive wrong file from web.""" - config = {tts.DOMAIN: {"platform": "demo"}} + client = await hass_client() - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" - self.hass.start() + req = await client.get(url) + assert req.status == 404 - url = ( - "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" - ).format(self.hass.config.api.base_url) - req = requests.get(url) - assert req.status_code == 404 +async def test_setup_component_and_web_view_wrong_filename(hass, hass_client): + """Set up the demo platform and receive wrong filename from web.""" + config = {tts.DOMAIN: {"platform": "demo"}} - def test_setup_component_and_web_view_wrong_filename(self): - """Set up the demo platform and receive wrong filename from web.""" - config = {tts.DOMAIN: {"platform": "demo"}} + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + client = await hass_client() - self.hass.start() + url = "/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd_en_-_demo.mp3" - url = ( - "{}/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd_en_-_demo.mp3" - ).format(self.hass.config.api.base_url) + req = await client.get(url) + assert req.status == 404 - req = requests.get(url) - assert req.status_code == 404 - def test_setup_component_test_without_cache(self): - """Set up demo platform without cache.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +async def test_setup_component_test_without_cache(hass, empty_cache_dir): + """Set up demo platform without cache.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo", "cache": False}} + config = {tts.DOMAIN: {"platform": "demo", "cache": False}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + assert not ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() + + +async def test_setup_component_test_with_cache_call_service_without_cache( + hass, empty_cache_dir +): + """Set up demo platform with cache and call service without cache.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = {tts.DOMAIN: {"platform": "demo", "cache": True}} + + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_CACHE: False, + }, + blocking=True, + ) + assert len(calls) == 1 + assert not ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() - assert len(calls) == 1 - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) - def test_setup_component_test_with_cache_call_service_without_cache(self): - """Set up demo platform with cache and call service without cache.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) +async def test_setup_component_test_with_cache_dir( + hass, empty_cache_dir, demo_provider +): + """Set up demo platform with cache and call service without cache.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + _, demo_data = demo_provider.get_tts_audio("bla", "en") + cache_file = ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ) + + with open(cache_file, "wb") as voice_file: + voice_file.write(demo_data) - config = {tts.DOMAIN: {"platform": "demo", "cache": True}} + config = {tts.DOMAIN: {"platform": "demo", "cache": True}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - self.hass.services.call( + with patch( + "homeassistant.components.demo.tts.DemoProvider.get_tts_audio", + return_value=(None, None), + ): + await hass.services.async_call( tts.DOMAIN, "demo_say", { "entity_id": "media_player.something", tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_CACHE: False, }, + blocking=True, ) - self.hass.block_till_done() - - assert len(calls) == 1 - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) - - def test_setup_component_test_with_cache_dir(self): - """Set up demo platform with cache and call service without cache.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - _, demo_data = self.demo_provider.get_tts_audio("bla", "en") - cache_file = os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) + assert len(calls) == 1 + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( + hass.config.api.base_url + ) - os.mkdir(self.default_tts_cache) - with open(cache_file, "wb") as voice_file: - voice_file.write(demo_data) - - config = {tts.DOMAIN: {"platform": "demo", "cache": True}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - with patch( - "homeassistant.components.demo.tts.DemoProvider.get_tts_audio", - return_value=(None, None), - ): - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() - assert len(calls) == 1 - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( - self.hass.config.api.base_url - ) +async def test_setup_component_test_with_error_on_get_tts(hass): + """Set up demo platform with wrong get_tts_audio.""" + config = {tts.DOMAIN: {"platform": "demo"}} - @patch( + with assert_setup_component(1, tts.DOMAIN), patch( "homeassistant.components.demo.tts.DemoProvider.get_tts_audio", return_value=(None, None), - ) - def test_setup_component_test_with_error_on_get_tts(self, tts_mock): - """Set up demo platform with wrong get_tts_audio.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - config = {tts.DOMAIN: {"platform": "demo"}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + ): + assert await async_setup_component(hass, tts.DOMAIN, config) - assert len(calls) == 0 - def test_setup_component_load_cache_retrieve_without_mem_cache(self): - """Set up component and load cache and get without mem cache.""" - _, demo_data = self.demo_provider.get_tts_audio("bla", "en") - cache_file = os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) +async def test_setup_component_load_cache_retrieve_without_mem_cache( + hass, demo_provider, empty_cache_dir, hass_client +): + """Set up component and load cache and get without mem cache.""" + _, demo_data = demo_provider.get_tts_audio("bla", "en") + cache_file = ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ) - os.mkdir(self.default_tts_cache) - with open(cache_file, "wb") as voice_file: - voice_file.write(demo_data) + with open(cache_file, "wb") as voice_file: + voice_file.write(demo_data) - config = {tts.DOMAIN: {"platform": "demo", "cache": True}} + config = {tts.DOMAIN: {"platform": "demo", "cache": True}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - self.hass.start() + client = await hass_client() - url = ( - "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" - ).format(self.hass.config.api.base_url) + url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" - req = requests.get(url) - assert req.status_code == 200 - assert req.content == demo_data + req = await client.get(url) + assert req.status == 200 + assert await req.read() == demo_data async def test_setup_component_and_web_get_url(hass, hass_client): @@ -666,10 +649,6 @@ async def test_setup_component_and_web_get_url(hass, hass_client): ) ) - tts_cache = hass.config.path(tts.DEFAULT_CACHE_DIR) - if os.path.isdir(tts_cache): - shutil.rmtree(tts_cache) - async def test_setup_component_and_web_get_url_bad_config(hass, hass_client): """Set up the demo platform and receive wrong file from web.""" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 9a280ffe9e69b8..b89dbfeb700ebd 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -11,6 +11,7 @@ CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, @@ -287,6 +288,7 @@ async def test_option_flow(hass): user_input={ CONF_BLOCK_CLIENT: clients_to_block, CONF_NEW_CLIENT: "00:00:00:00:00:01", + CONF_POE_CLIENTS: False, }, ) @@ -327,5 +329,6 @@ async def test_option_flow(hass): CONF_DETECTION_TIME: 100, CONF_SSID_FILTER: ["SSID 1"], CONF_BLOCK_CLIENT: ["00:00:00:00:00:01"], + CONF_POE_CLIENTS: False, CONF_ALLOW_BANDWIDTH_SENSORS: True, } diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 649cf9af6a54e8..5574c93c515d09 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -1,47 +1,91 @@ """Common code for tests.""" -from typing import Callable, NamedTuple, Tuple +from typing import Callable, Dict, NamedTuple, Tuple from mock import MagicMock -from pyvera import VeraController, VeraDevice, VeraScene +import pyvera as pv -from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry -class ComponentData(NamedTuple): - """Component data.""" +SetupCallback = Callable[[pv.VeraController, dict], None] + + +class ControllerData(NamedTuple): + """Test data about a specific vera controller.""" + + controller: pv.VeraController + update_callback: Callable - controller: VeraController + +class ComponentData(NamedTuple): + """Test data about the vera component.""" + + controller_data: ControllerData + + +class ControllerConfig(NamedTuple): + """Test config for mocking a vera controller.""" + + config: Dict + options: Dict + config_from_file: bool + serial_number: str + devices: Tuple[pv.VeraDevice, ...] + scenes: Tuple[pv.VeraScene, ...] + setup_callback: SetupCallback + + +def new_simple_controller_config( + config: dict = None, + options: dict = None, + config_from_file=False, + serial_number="1111", + devices: Tuple[pv.VeraDevice, ...] = (), + scenes: Tuple[pv.VeraScene, ...] = (), + setup_callback: SetupCallback = None, +) -> ControllerConfig: + """Create simple contorller config.""" + return ControllerConfig( + config=config or {CONF_CONTROLLER: "http://127.0.0.1:123"}, + options=options, + config_from_file=config_from_file, + serial_number=serial_number, + devices=devices, + scenes=scenes, + setup_callback=setup_callback, + ) class ComponentFactory: """Factory class.""" - def __init__(self, init_controller_mock): - """Initialize component factory.""" - self.init_controller_mock = init_controller_mock + def __init__(self, vera_controller_class_mock): + """Initialize the factory.""" + self.vera_controller_class_mock = vera_controller_class_mock async def configure_component( - self, - hass: HomeAssistant, - devices: Tuple[VeraDevice] = (), - scenes: Tuple[VeraScene] = (), - setup_callback: Callable[[VeraController], None] = None, + self, hass: HomeAssistant, controller_config: ControllerConfig ) -> ComponentData: """Configure the component with specific mock data.""" - controller_url = "http://127.0.0.1:123" - - hass_config = { - DOMAIN: {CONF_CONTROLLER: controller_url}, + component_config = { + **(controller_config.config or {}), + **(controller_config.options or {}), } - controller = MagicMock(spec=VeraController) # type: VeraController - controller.base_url = controller_url + controller = MagicMock(spec=pv.VeraController) # type: pv.VeraController + controller.base_url = component_config.get(CONF_CONTROLLER) controller.register = MagicMock() - controller.get_devices = MagicMock(return_value=devices or ()) - controller.get_scenes = MagicMock(return_value=scenes or ()) + controller.start = MagicMock() + controller.stop = MagicMock() + controller.refresh_data = MagicMock() + controller.temperature_units = "C" + controller.serial_number = controller_config.serial_number + controller.get_devices = MagicMock(return_value=controller_config.devices) + controller.get_scenes = MagicMock(return_value=controller_config.scenes) for vera_obj in controller.get_devices() + controller.get_scenes(): vera_obj.vera_controller = controller @@ -49,17 +93,39 @@ async def configure_component( controller.get_devices.reset_mock() controller.get_scenes.reset_mock() - if setup_callback: - setup_callback(controller, hass_config) + if controller_config.setup_callback: + controller_config.setup_callback(controller) + + self.vera_controller_class_mock.return_value = controller - def init_controller(base_url: str) -> list: - nonlocal controller - return [controller, True] + hass_config = {} - self.init_controller_mock.side_effect = init_controller + # Setup component through config file import. + if controller_config.config_from_file: + hass_config[DOMAIN] = component_config # Setup Home Assistant. assert await async_setup_component(hass, DOMAIN, hass_config) await hass.async_block_till_done() - return ComponentData(controller=controller) + # Setup component through config flow. + if not controller_config.config_from_file: + entry = MockConfigEntry( + domain=DOMAIN, data=component_config, options={}, unique_id="12345" + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + update_callback = ( + controller.register.call_args_list[0][0][1] + if controller.register.call_args_list + else None + ) + + return ComponentData( + controller_data=ControllerData( + controller=controller, update_callback=update_callback + ) + ) diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py index b94a40135d8b09..2c15d3e4182fbe 100644 --- a/tests/components/vera/conftest.py +++ b/tests/components/vera/conftest.py @@ -9,5 +9,5 @@ @pytest.fixture() def vera_component_factory(): """Return a factory for initializing the vera component.""" - with patch("pyvera.init_controller") as init_controller_mock: - yield ComponentFactory(init_controller_mock) + with patch("pyvera.VeraController") as vera_controller_class_mock: + yield ComponentFactory(vera_controller_class_mock) diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index 2c2e2b8638818a..72651d6eda4f74 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -1,38 +1,36 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import VeraBinarySensor +import pyvera as pv from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_binary_sensor( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraBinarySensor) # type: VeraBinarySensor + vera_device = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor vera_device.device_id = 1 + vera_device.vera_device_id = 1 vera_device.name = "dev1" vera_device.is_tripped = False entity_id = "binary_sensor.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,) + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback vera_device.is_tripped = False update_callback(vera_device) await hass.async_block_till_done() assert hass.states.get(entity_id).state == "off" - controller.register.reset_mock() vera_device.is_tripped = True update_callback(vera_device) await hass.async_block_till_done() assert hass.states.get(entity_id).state == "on" - controller.register.reset_mock() diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index c27a72865fd995..9e5fa983ed053f 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -1,7 +1,7 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_THERMOSTAT, VeraController, VeraThermostat +import pyvera as pv from homeassistant.components.climate.const import ( FAN_AUTO, @@ -13,17 +13,17 @@ ) from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_climate( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraThermostat) # type: VeraThermostat + vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_THERMOSTAT + vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 vera_device.get_current_temperature.return_value = 71 vera_device.get_hvac_mode.return_value = "Off" @@ -31,10 +31,10 @@ async def test_climate( entity_id = "climate.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback assert hass.states.get(entity_id).state == HVAC_MODE_OFF @@ -123,24 +123,26 @@ async def test_climate_f( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraThermostat) # type: VeraThermostat + vera_device = MagicMock(spec=pv.VeraThermostat) # type: pv.VeraThermostat vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_THERMOSTAT + vera_device.category = pv.CATEGORY_THERMOSTAT vera_device.power = 10 vera_device.get_current_temperature.return_value = 71 vera_device.get_hvac_mode.return_value = "Off" vera_device.get_current_goal_temperature.return_value = 72 entity_id = "climate.dev1_1" - def setup_callback(controller: VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController) -> None: controller.temperature_units = "F" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), setup_callback=setup_callback + hass=hass, + controller_config=new_simple_controller_config( + devices=(vera_device,), setup_callback=setup_callback + ), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback await hass.services.async_call( "climate", "set_temperature", {"entity_id": entity_id, "temperature": 30}, diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py new file mode 100644 index 00000000000000..52ba55b509c8ac --- /dev/null +++ b/tests/components/vera/test_config_flow.py @@ -0,0 +1,159 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from mock import patch +from requests.exceptions import RequestException + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_user_success(hass: HomeAssistant) -> None: + """Test user step success.""" + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock() + controller.serial_number = "serial_number_0" + vera_controller_class_mock.return_value = controller + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_CONTROLLER: "http://127.0.0.1:123/", + CONF_LIGHTS: "12 13", + CONF_EXCLUDE: "14 15", + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "http://127.0.0.1:123" + assert result["data"] == { + CONF_CONTROLLER: "http://127.0.0.1:123", + CONF_SOURCE: config_entries.SOURCE_USER, + CONF_LIGHTS: ["12", "13"], + CONF_EXCLUDE: ["14", "15"], + } + assert result["result"].unique_id == controller.serial_number + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + + +async def test_async_step_user_already_configured(hass: HomeAssistant) -> None: + """Test user step with entry already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_import_success(hass: HomeAssistant) -> None: + """Test import step success.""" + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock() + controller.serial_number = "serial_number_1" + vera_controller_class_mock.return_value = controller + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "http://127.0.0.1:123" + assert result["data"] == { + CONF_CONTROLLER: "http://127.0.0.1:123", + CONF_SOURCE: config_entries.SOURCE_IMPORT, + } + assert result["result"].unique_id == controller.serial_number + + +async def test_async_step_import_alredy_setup(hass: HomeAssistant) -> None: + """Test import step with entry already setup.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options={}, unique_id="12345") + entry.add_to_hass(hass) + + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock() + controller.serial_number = "12345" + vera_controller_class_mock.return_value = controller + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_CONTROLLER: "http://localhost:445"}, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_finish_error(hass: HomeAssistant) -> None: + """Test finish step with error.""" + with patch("pyvera.VeraController") as vera_controller_class_mock: + controller = MagicMock() + controller.refresh_data = MagicMock(side_effect=RequestException()) + vera_controller_class_mock.return_value = controller + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + assert result["description_placeholders"] == { + "base_url": "http://127.0.0.1:123" + } + + +async def test_options(hass): + """Test updating options.""" + base_url = "http://127.0.0.1/" + entry = MockConfigEntry( + domain=DOMAIN, + title=base_url, + data={CONF_CONTROLLER: "http://127.0.0.1/"}, + options={CONF_LIGHTS: [1, 2, 3]}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_LIGHTS: "1,2;3 4 5_6bb7", + CONF_EXCLUDE: "8,9;10 11 12_13bb14", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_LIGHTS: ["1", "2", "3", "4", "5", "6", "7"], + CONF_EXCLUDE: ["8", "9", "10", "11", "12", "13", "14"], + } diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index 79cb4adedfbc92..62cd47f831cdc3 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -1,30 +1,30 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_CURTAIN, VeraCurtain +import pyvera as pv from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_cover( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraCurtain) # type: VeraCurtain + vera_device = MagicMock(spec=pv.VeraCurtain) # type: pv.VeraCurtain vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_CURTAIN + vera_device.category = pv.CATEGORY_CURTAIN vera_device.is_closed = False vera_device.get_level.return_value = 0 entity_id = "cover.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback assert hass.states.get(entity_id).state == "closed" assert hass.states.get(entity_id).attributes["current_position"] == 0 diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index 9ff6cb4058b3c0..a6208726451ba0 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,78 +1,112 @@ """Vera tests.""" -from unittest.mock import MagicMock - -from pyvera import ( - VeraArmableDevice, - VeraBinarySensor, - VeraController, - VeraCurtain, - VeraDevice, - VeraDimmer, - VeraLock, - VeraScene, - VeraSceneController, - VeraSensor, - VeraSwitch, - VeraThermostat, -) - -from homeassistant.components.vera import ( - CONF_EXCLUDE, - CONF_LIGHTS, - DOMAIN, - VERA_DEVICES, -) +from asynctest import MagicMock +import pyvera as pv +from requests.exceptions import RequestException + +from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config + +from tests.common import MockConfigEntry -def new_vera_device(cls, device_id: int) -> VeraDevice: - """Create new mocked vera device..""" - vera_device = MagicMock(spec=cls) # type: VeraDevice - vera_device.device_id = device_id - vera_device.name = f"dev${device_id}" - return vera_device +async def test_init( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = 1 + vera_device1.name = "first_dev" + vera_device1.is_tripped = False + entity1_id = "binary_sensor.first_dev_1" + + await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config( + config={CONF_CONTROLLER: "http://127.0.0.1:111"}, + config_from_file=False, + serial_number="first_serial", + devices=(vera_device1,), + ), + ) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry1 = entity_registry.async_get(entity1_id) -def assert_hass_vera_devices(hass: HomeAssistant, platform: str, arr_len: int) -> None: - """Assert vera devices are present..""" - assert hass.data[VERA_DEVICES][platform] - assert len(hass.data[VERA_DEVICES][platform]) == arr_len + assert entry1 -async def test_init( +async def test_init_from_file( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - - def setup_callback(controller: VeraController, hass_config: dict) -> None: - hass_config[DOMAIN][CONF_EXCLUDE] = [11] - hass_config[DOMAIN][CONF_LIGHTS] = [10] + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = 1 + vera_device1.name = "first_dev" + vera_device1.is_tripped = False + entity1_id = "binary_sensor.first_dev_1" await vera_component_factory.configure_component( hass=hass, - devices=( - new_vera_device(VeraDimmer, 1), - new_vera_device(VeraBinarySensor, 2), - new_vera_device(VeraSensor, 3), - new_vera_device(VeraArmableDevice, 4), - new_vera_device(VeraLock, 5), - new_vera_device(VeraThermostat, 6), - new_vera_device(VeraCurtain, 7), - new_vera_device(VeraSceneController, 8), - new_vera_device(VeraSwitch, 9), - new_vera_device(VeraSwitch, 10), - new_vera_device(VeraSwitch, 11), + controller_config=new_simple_controller_config( + config={CONF_CONTROLLER: "http://127.0.0.1:111"}, + config_from_file=True, + serial_number="first_serial", + devices=(vera_device1,), ), - scenes=(MagicMock(spec=VeraScene),), - setup_callback=setup_callback, ) - assert_hass_vera_devices(hass, "light", 2) - assert_hass_vera_devices(hass, "binary_sensor", 1) - assert_hass_vera_devices(hass, "sensor", 2) - assert_hass_vera_devices(hass, "switch", 2) - assert_hass_vera_devices(hass, "lock", 1) - assert_hass_vera_devices(hass, "climate", 1) - assert_hass_vera_devices(hass, "cover", 1) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry1 = entity_registry.async_get(entity1_id) + assert entry1 + + +async def test_unload( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = 1 + vera_device1.name = "first_dev" + vera_device1.is_tripped = False + + await vera_component_factory.configure_component( + hass=hass, controller_config=new_simple_controller_config() + ) + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + + for config_entry in entries: + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state == ENTRY_STATE_NOT_LOADED + + +async def test_async_setup_entry_error( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: pv.VeraController) -> None: + controller.get_devices.side_effect = RequestException() + controller.get_scenes.side_effect = RequestException() + + await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config(setup_callback=setup_callback), + ) + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_CONTROLLER: "http://127.0.0.1"}, + options={}, + unique_id="12345", + ) + entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index fa63ce63454444..fefa07ffa6ee40 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -1,22 +1,22 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_DIMMER, VeraDimmer +import pyvera as pv from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_light( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraDimmer) # type: VeraDimmer + vera_device = MagicMock(spec=pv.VeraDimmer) # type: pv.VeraDimmer vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_DIMMER + vera_device.category = pv.CATEGORY_DIMMER vera_device.is_switched_on = MagicMock(return_value=False) vera_device.get_brightness = MagicMock(return_value=0) vera_device.get_color = MagicMock(return_value=[0, 0, 0]) @@ -24,10 +24,10 @@ async def test_light( entity_id = "light.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback assert hass.states.get(entity_id).state == "off" diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index 362bdbeddc0fde..d1b2209294a375 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -1,30 +1,30 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_LOCK, VeraLock +import pyvera as pv from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_lock( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraLock) # type: VeraLock + vera_device = MagicMock(spec=pv.VeraLock) # type: pv.VeraLock vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_LOCK + vera_device.category = pv.CATEGORY_LOCK vera_device.is_locked = MagicMock(return_value=False) entity_id = "lock.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index 136227ffa7126e..732a331681bdb2 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -1,24 +1,24 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import VeraScene +import pyvera as pv from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_scene( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_scene = MagicMock(spec=VeraScene) # type: VeraScene + vera_scene = MagicMock(spec=pv.VeraScene) # type: pv.VeraScene vera_scene.scene_id = 1 vera_scene.name = "dev1" entity_id = "scene.dev1_1" await vera_component_factory.configure_component( - hass=hass, scenes=(vera_scene,), + hass=hass, controller_config=new_simple_controller_config(scenes=(vera_scene,)), ) await hass.services.async_call( diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 9e84815d636230..c915c5ead0fd17 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -2,21 +2,12 @@ from typing import Any, Callable, Tuple from unittest.mock import MagicMock -from pyvera import ( - CATEGORY_HUMIDITY_SENSOR, - CATEGORY_LIGHT_SENSOR, - CATEGORY_POWER_METER, - CATEGORY_SCENE_CONTROLLER, - CATEGORY_TEMPERATURE_SENSOR, - CATEGORY_UV_SENSOR, - VeraController, - VeraSensor, -) +import pyvera as pv from homeassistant.const import UNIT_PERCENTAGE from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def run_sensor_test( @@ -26,10 +17,10 @@ async def run_sensor_test( class_property: str, assert_states: Tuple[Tuple[Any, Any]], assert_unit_of_measurement: str = None, - setup_callback: Callable[[VeraController], None] = None, + setup_callback: Callable[[pv.VeraController], None] = None, ) -> None: """Test generic sensor.""" - vera_device = MagicMock(spec=VeraSensor) # type: VeraSensor + vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 vera_device.name = "dev1" vera_device.category = category @@ -37,10 +28,12 @@ async def run_sensor_test( entity_id = "sensor.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), setup_callback=setup_callback + hass=hass, + controller_config=new_simple_controller_config( + devices=(vera_device,), setup_callback=setup_callback + ), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback for (initial_value, state_value) in assert_states: setattr(vera_device, class_property, initial_value) @@ -57,13 +50,13 @@ async def test_temperature_sensor_f( ) -> None: """Test function.""" - def setup_callback(controller: VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController) -> None: controller.temperature_units = "F" await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_TEMPERATURE_SENSOR, + category=pv.CATEGORY_TEMPERATURE_SENSOR, class_property="temperature", assert_states=(("33", "1"), ("44", "7")), setup_callback=setup_callback, @@ -77,7 +70,7 @@ async def test_temperature_sensor_c( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_TEMPERATURE_SENSOR, + category=pv.CATEGORY_TEMPERATURE_SENSOR, class_property="temperature", assert_states=(("33", "33"), ("44", "44")), ) @@ -90,7 +83,7 @@ async def test_light_sensor( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_LIGHT_SENSOR, + category=pv.CATEGORY_LIGHT_SENSOR, class_property="light", assert_states=(("12", "12"), ("13", "13")), assert_unit_of_measurement="lx", @@ -104,7 +97,7 @@ async def test_uv_sensor( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_UV_SENSOR, + category=pv.CATEGORY_UV_SENSOR, class_property="light", assert_states=(("12", "12"), ("13", "13")), assert_unit_of_measurement="level", @@ -118,7 +111,7 @@ async def test_humidity_sensor( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_HUMIDITY_SENSOR, + category=pv.CATEGORY_HUMIDITY_SENSOR, class_property="humidity", assert_states=(("12", "12"), ("13", "13")), assert_unit_of_measurement=UNIT_PERCENTAGE, @@ -132,7 +125,7 @@ async def test_power_meter_sensor( await run_sensor_test( hass=hass, vera_component_factory=vera_component_factory, - category=CATEGORY_POWER_METER, + category=pv.CATEGORY_POWER_METER, class_property="power", assert_states=(("12", "12"), ("13", "13")), assert_unit_of_measurement="watts", @@ -144,7 +137,7 @@ async def test_trippable_sensor( ) -> None: """Test function.""" - def setup_callback(controller: VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController) -> None: controller.get_devices()[0].is_trippable = True await run_sensor_test( @@ -162,7 +155,7 @@ async def test_unknown_sensor( ) -> None: """Test function.""" - def setup_callback(controller: VeraController, hass_config: dict) -> None: + def setup_callback(controller: pv.VeraController) -> None: controller.get_devices()[0].is_trippable = False await run_sensor_test( @@ -179,21 +172,21 @@ async def test_scene_controller_sensor( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraSensor) # type: VeraSensor + vera_device = MagicMock(spec=pv.VeraSensor) # type: pv.VeraSensor vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_SCENE_CONTROLLER + vera_device.category = pv.CATEGORY_SCENE_CONTROLLER vera_device.get_last_scene_id = MagicMock(return_value="id0") vera_device.get_last_scene_time = MagicMock(return_value="0000") entity_id = "sensor.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,) + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback - vera_device.get_last_scene_time = "1111" + vera_device.get_last_scene_time.return_value = "1111" update_callback(vera_device) await hass.async_block_till_done() assert hass.states.get(entity_id).state == "id0" diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index ba09068e7e602f..c41afad4759f8f 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -1,29 +1,29 @@ """Vera tests.""" from unittest.mock import MagicMock -from pyvera import CATEGORY_SWITCH, VeraSwitch +import pyvera as pv from homeassistant.core import HomeAssistant -from .common import ComponentFactory +from .common import ComponentFactory, new_simple_controller_config async def test_switch( hass: HomeAssistant, vera_component_factory: ComponentFactory ) -> None: """Test function.""" - vera_device = MagicMock(spec=VeraSwitch) # type: VeraSwitch + vera_device = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch vera_device.device_id = 1 vera_device.name = "dev1" - vera_device.category = CATEGORY_SWITCH + vera_device.category = pv.CATEGORY_SWITCH vera_device.is_switched_on = MagicMock(return_value=False) entity_id = "switch.dev1_1" component_data = await vera_component_factory.configure_component( - hass=hass, devices=(vera_device,), + hass=hass, + controller_config=new_simple_controller_config(devices=(vera_device,)), ) - controller = component_data.controller - update_callback = controller.register.call_args_list[0][0][1] + update_callback = component_data.controller_data.update_callback assert hass.states.get(entity_id).state == "off" diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 868bf44a11ba7f..e630f201e121d8 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -8,7 +8,9 @@ APP_LIST, CH_TYPE, CURRENT_APP_CONFIG, + CURRENT_EQ, CURRENT_INPUT, + EQ_LIST, INPUT_LIST, INPUT_LIST_WITH_APPS, MODEL, @@ -135,11 +137,15 @@ def vizio_update_fixture(): "homeassistant.components.vizio.media_player.VizioAsync.can_connect_with_auth_check", return_value=True, ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_all_audio_settings", + "homeassistant.components.vizio.media_player.VizioAsync.get_all_settings", return_value={ "volume": int(MAX_VOLUME[DEVICE_CLASS_SPEAKER] / 2), + "eq": CURRENT_EQ, "mute": "Off", }, + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_setting_options", + return_value=EQ_LIST, ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", return_value=CURRENT_INPUT, diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index f1ddc4abba69d4..034fa23a3af2f1 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -64,6 +64,9 @@ def __init__(self, auth_token: str) -> None: self.auth_token = auth_token +CURRENT_EQ = "Music" +EQ_LIST = ["Music", "Movie"] + CURRENT_INPUT = "HDMI" INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"] diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index f860c1cec4fdb6..7678712db5156b 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -21,11 +21,13 @@ ATTR_INPUT_SOURCE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, + ATTR_SOUND_MODE, DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV, DOMAIN as MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -58,9 +60,11 @@ APP_LIST, CURRENT_APP, CURRENT_APP_CONFIG, + CURRENT_EQ, CURRENT_INPUT, CUSTOM_CONFIG, ENTITY_ID, + EQ_LIST, INPUT_LIST, INPUT_LIST_WITH_APPS, MOCK_SPEAKER_APPS_FAILURE, @@ -99,6 +103,11 @@ async def _test_setup( data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG), unique_id=UNIQUE_ID, ) + dict_to_return = { + "volume": int(MAX_VOLUME[vizio_device_class] / 2), + "mute": "Off", + "eq": CURRENT_EQ, + } else: vizio_device_class = VIZIO_DEVICE_CLASS_TV config_entry = MockConfigEntry( @@ -106,10 +115,17 @@ async def _test_setup( data=vol.Schema(VIZIO_SCHEMA)(MOCK_USER_VALID_TV_CONFIG), unique_id=UNIQUE_ID, ) + dict_to_return = { + "volume": int(MAX_VOLUME[vizio_device_class] / 2), + "mute": "Off", + } with patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_all_audio_settings", - return_value={"volume": int(MAX_VOLUME[vizio_device_class] / 2), "mute": "Off"}, + "homeassistant.components.vizio.media_player.VizioAsync.get_all_settings", + return_value=dict_to_return, + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_setting_options", + return_value=EQ_LIST, ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", return_value=vizio_power_state, @@ -130,6 +146,9 @@ async def _test_setup( assert attr["source"] == CURRENT_INPUT if ha_device_class == DEVICE_CLASS_SPEAKER: assert not service_call.called + assert "sound_mode" in attr + else: + assert "sound_mode" not in attr assert ( attr["volume_level"] == float(int(MAX_VOLUME[vizio_device_class] / 2)) @@ -149,7 +168,7 @@ async def _test_setup_with_apps( ) with patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_all_audio_settings", + "homeassistant.components.vizio.media_player.VizioAsync.get_all_settings", return_value={ "volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2), "mute": "Off", @@ -351,6 +370,9 @@ async def test_services( ) await _test_service(hass, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None) await _test_service(hass, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None) + await _test_service( + hass, "set_setting", SERVICE_SELECT_SOUND_MODE, {ATTR_SOUND_MODE: "Music"} + ) async def test_options_update( diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 47c7a98023ce59..d497aec0dca35e 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -100,6 +100,36 @@ def mirobo_is_got_error_fixture(): yield mock_vacuum +old_fanspeeds = { + "Silent": 38, + "Standard": 60, + "Medium": 77, + "Turbo": 90, +} +new_fanspeeds = { + "Silent": 101, + "Standard": 102, + "Medium": 103, + "Turbo": 104, + "Gentle": 105, +} + + +@pytest.fixture(name="mock_mirobo_fanspeeds", params=[old_fanspeeds, new_fanspeeds]) +def mirobo_old_speeds_fixture(request): + """Fixture for testing both types of fanspeeds.""" + mock_vacuum = mock.MagicMock() + mock_vacuum.status().battery = 32 + mock_vacuum.fan_speed_presets.return_value = request.param + mock_vacuum.status().fanspeed = list(request.param.values())[0] + + with mock.patch( + "homeassistant.components.xiaomi_miio.vacuum.Vacuum" + ) as mock_vaccum_cls: + mock_vaccum_cls.return_value = mock_vacuum + yield mock_vacuum + + @pytest.fixture(name="mock_mirobo_is_on") def mirobo_is_on_fixture(): """Mock mock_mirobo.""" @@ -204,14 +234,6 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-80" assert state.attributes.get(ATTR_CLEANING_TIME) == 155 assert state.attributes.get(ATTR_CLEANED_AREA) == 123 - assert state.attributes.get(ATTR_FAN_SPEED) == "Silent" - assert state.attributes.get(ATTR_FAN_SPEED_LIST) == [ - "Silent", - "Standard", - "Medium", - "Turbo", - "Gentle", - ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 12 assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 12 assert state.attributes.get(ATTR_FILTER_LEFT) == 12 @@ -257,40 +279,6 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - # Set speed service: - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_SPEED, - {"entity_id": entity_id, "fan_speed": 60}, - blocking=True, - ) - mock_mirobo_is_got_error.assert_has_calls( - [mock.call.set_fan_speed(60)], any_order=True - ) - mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) - mock_mirobo_is_got_error.reset_mock() - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_SPEED, - {"entity_id": entity_id, "fan_speed": "Medium"}, - blocking=True, - ) - mock_mirobo_is_got_error.assert_has_calls( - [mock.call.set_fan_speed(77)], any_order=True - ) - mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) - mock_mirobo_is_got_error.reset_mock() - - assert "ERROR" not in caplog.text - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_SPEED, - {"entity_id": entity_id, "fan_speed": "invent"}, - blocking=True, - ) - assert "ERROR" in caplog.text - await hass.services.async_call( DOMAIN, SERVICE_SEND_COMMAND, @@ -346,14 +334,6 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-30" assert state.attributes.get(ATTR_CLEANING_TIME) == 175 assert state.attributes.get(ATTR_CLEANED_AREA) == 133 - assert state.attributes.get(ATTR_FAN_SPEED) == 99 - assert state.attributes.get(ATTR_FAN_SPEED_LIST) == [ - "Silent", - "Standard", - "Medium", - "Turbo", - "Gentle", - ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 11 assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 11 assert state.attributes.get(ATTR_FILTER_LEFT) == 11 @@ -409,3 +389,67 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): ) mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() + + +async def test_xiaomi_vacuum_fanspeeds(hass, caplog, mock_mirobo_fanspeeds): + """Test Xiaomi vacuum fanspeeds.""" + entity_name = "test_vacuum_cleaner_2" + entity_id = f"{DOMAIN}.{entity_name}" + + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_PLATFORM: PLATFORM, + CONF_HOST: "192.168.1.100", + CONF_NAME: entity_name, + CONF_TOKEN: "12345678901234567890123456789012", + } + }, + ) + await hass.async_block_till_done() + + assert "Initializing with host 192.168.1.100 (token 12345" in caplog.text + + state = hass.states.get(entity_id) + assert state.attributes.get(ATTR_FAN_SPEED) == "Silent" + fanspeeds = state.attributes.get(ATTR_FAN_SPEED_LIST) + for speed in ["Silent", "Standard", "Medium", "Turbo"]: + assert speed in fanspeeds + + # Set speed service: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_SPEED, + {"entity_id": entity_id, "fan_speed": 60}, + blocking=True, + ) + mock_mirobo_fanspeeds.assert_has_calls( + [mock.call.set_fan_speed(60)], any_order=True + ) + mock_mirobo_fanspeeds.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_fanspeeds.reset_mock() + + fan_speed_dict = mock_mirobo_fanspeeds.fan_speed_presets() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_SPEED, + {"entity_id": entity_id, "fan_speed": "Medium"}, + blocking=True, + ) + mock_mirobo_fanspeeds.assert_has_calls( + [mock.call.set_fan_speed(fan_speed_dict["Medium"])], any_order=True + ) + mock_mirobo_fanspeeds.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_fanspeeds.reset_mock() + + assert "ERROR" not in caplog.text + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_SPEED, + {"entity_id": entity_id, "fan_speed": "invent"}, + blocking=True, + ) + assert "ERROR" in caplog.text diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 4d358bde770c22..6d19022f9592bd 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -28,6 +28,7 @@ get_test_home_assistant, mock_coro, mock_registry, + mock_storage, ) from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue @@ -827,6 +828,8 @@ def set_mock_openzwave(self, mock_openzwave): def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() + self.mock_storage = mock_storage() + self.mock_storage.__enter__() self.hass.start() self.registry = mock_registry(self.hass) @@ -862,6 +865,7 @@ def setUp(self): def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() + self.mock_storage.__exit__(None, None, None) @patch.object(zwave, "import_module") @patch.object(zwave, "discovery") @@ -1194,6 +1198,8 @@ def set_mock_openzwave(self, mock_openzwave): def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() + self.mock_storage = mock_storage() + self.mock_storage.__enter__() self.hass.start() # Initialize zwave @@ -1209,6 +1215,7 @@ def tearDown(self): # pylint: disable=invalid-name self.hass.services.call("zwave", "stop_network", {}) self.hass.block_till_done() self.hass.stop() + self.mock_storage.__exit__(None, None, None) def test_add_node(self): """Test zwave add_node service.""" diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 9136b53ff0b3a4..28161cfa18b67c 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -13,6 +13,7 @@ async def test_maybe_schedule_update(hass, mock_openzwave): """Test maybe schedule update.""" base_entity = node_entity.ZWaveBaseEntity() + base_entity.entity_id = "zwave.bla" base_entity.hass = hass with patch.object(hass.loop, "call_later") as mock_call_later: @@ -21,12 +22,12 @@ async def test_maybe_schedule_update(hass, mock_openzwave): base_entity._schedule_update() assert len(mock_call_later.mock_calls) == 1 + assert base_entity._update_scheduled is True do_update = mock_call_later.mock_calls[0][1][1] - with patch.object(hass, "async_add_job") as mock_add_job: - do_update() - assert mock_add_job.called + do_update() + assert base_entity._update_scheduled is False base_entity._schedule_update() assert len(mock_call_later.mock_calls) == 2 diff --git a/tests/conftest.py b/tests/conftest.py index 0963d1514904b0..f93d5190350637 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,10 @@ from homeassistant.setup import async_setup_component from homeassistant.util import location -from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS +from tests.ignore_uncaught_exceptions import ( + IGNORE_UNCAUGHT_EXCEPTIONS, + IGNORE_UNCAUGHT_JSON_EXCEPTIONS, +) pytest.register_assert_rewrite("tests.common") @@ -104,6 +107,13 @@ def exc_handle(loop, context): continue if isinstance(ex, ServiceNotFound): continue + if ( + isinstance(ex, TypeError) + and "is not JSON serializable" in str(ex) + and (request.module.__name__, request.function.__name__) + in IGNORE_UNCAUGHT_JSON_EXCEPTIONS + ): + continue raise ex @@ -211,7 +221,7 @@ def hass_client(hass, aiohttp_client, hass_access_token): async def auth_client(): """Return an authenticated client.""" return await aiohttp_client( - hass.http.app, headers={"Authorization": f"Bearer {hass_access_token}"}, + hass.http.app, headers={"Authorization": f"Bearer {hass_access_token}"} ) return auth_client diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 306402cd2b9812..1c5224d89c3e3b 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -379,15 +379,16 @@ async def test_update_entity(hass): """Test that we can update an entity with the helper.""" component = EntityComponent(_LOGGER, DOMAIN, hass) entity = MockEntity() + entity.async_write_ha_state = Mock() entity.async_update_ha_state = Mock(return_value=mock_coro()) await component.async_add_entities([entity]) # Called as part of async_add_entities - assert len(entity.async_update_ha_state.mock_calls) == 1 + assert len(entity.async_write_ha_state.mock_calls) == 1 await hass.helpers.entity_component.async_update_entity(entity.entity_id) - assert len(entity.async_update_ha_state.mock_calls) == 2 + assert len(entity.async_update_ha_state.mock_calls) == 1 assert entity.async_update_ha_state.mock_calls[-1][1][0] is True diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index dcadd4d4369f34..61648c85ada9e4 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -6,7 +6,11 @@ import pytest -from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE +from homeassistant.const import ( + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import CoreState from homeassistant.helpers import storage from homeassistant.util import dt @@ -79,10 +83,18 @@ async def test_saving_with_delay(hass, store, hass_storage): } -async def test_saving_on_stop(hass, hass_storage): +async def test_saving_on_final_write(hass, hass_storage): """Test delayed saves trigger when we quit Home Assistant.""" store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) - store.async_delay_save(lambda: MOCK_DATA, 1) + store.async_delay_save(lambda: MOCK_DATA, 5) + assert store.key not in hass_storage + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.state = CoreState.stopping + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() assert store.key not in hass_storage hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) @@ -94,6 +106,43 @@ async def test_saving_on_stop(hass, hass_storage): } +async def test_not_delayed_saving_while_stopping(hass, hass_storage): + """Test delayed saves don't write after the stop event has fired.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + hass.state = CoreState.stopping + + store.async_delay_save(lambda: MOCK_DATA, 1) + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + assert store.key not in hass_storage + + +async def test_not_delayed_saving_after_stopping(hass, hass_storage): + """Test delayed saves don't write after stop if issued before stopping Home Assistant.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + store.async_delay_save(lambda: MOCK_DATA, 10) + assert store.key not in hass_storage + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.state = CoreState.stopping + await hass.async_block_till_done() + assert store.key not in hass_storage + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=15)) + await hass.async_block_till_done() + assert store.key not in hass_storage + + +async def test_not_saving_while_stopping(hass, hass_storage): + """Test saves don't write when stopping Home Assistant.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + hass.state = CoreState.stopping + await store.async_save(MOCK_DATA) + assert store.key not in hass_storage + + async def test_loading_while_delay(hass, store, hass_storage): """Test we load new data even if not written yet.""" await store.async_save({"delay": "no"}) diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 428de1a683c13b..6f7910f9120438 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -48,30 +48,6 @@ ("tests.components.ios.test_init", "test_creating_entry_sets_up_sensor"), ("tests.components.ios.test_init", "test_not_configuring_ios_not_creates_entry"), ("tests.components.local_file.test_camera", "test_file_not_readable"), - ( - "tests.components.mqtt.test_init", - "test_setup_uses_certificate_on_certificate_set_to_auto", - ), - ( - "tests.components.mqtt.test_init", - "test_setup_does_not_use_certificate_on_mqtts_port", - ), - ( - "tests.components.mqtt.test_init", - "test_setup_without_tls_config_uses_tlsv1_under_python36", - ), - ( - "tests.components.mqtt.test_init", - "test_setup_with_tls_config_uses_tls_version1_2", - ), - ( - "tests.components.mqtt.test_init", - "test_setup_with_tls_config_of_v1_under_python36_only_uses_v1", - ), - ("tests.components.mqtt.test_light", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_light_json", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_light_template", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_switch", "test_entity_device_info_remove"), ("tests.components.qwikswitch.test_init", "test_binary_sensor_device"), ("tests.components.qwikswitch.test_init", "test_sensor_device"), ("tests.components.rflink.test_init", "test_send_command_invalid_arguments"), @@ -80,11 +56,6 @@ "tests.components.tplink.test_init", "test_configuring_devices_from_multiple_sources", ), - ("tests.components.tradfri.test_light", "test_light"), - ("tests.components.tradfri.test_light", "test_light_observed"), - ("tests.components.tradfri.test_light", "test_light_available"), - ("tests.components.tradfri.test_light", "test_turn_on"), - ("tests.components.tradfri.test_light", "test_turn_off"), ("tests.components.unifi_direct.test_device_tracker", "test_get_scanner"), ("tests.components.upnp.test_init", "test_async_setup_entry_default"), ("tests.components.upnp.test_init", "test_async_setup_entry_port_mapping"), @@ -93,3 +64,42 @@ ("tests.components.yr.test_sensor", "test_forecast_setup"), ("tests.components.zwave.test_init", "test_power_schemes"), ] + +IGNORE_UNCAUGHT_JSON_EXCEPTIONS = [ + ("tests.components.spotify.test_config_flow", "test_full_flow"), + ("tests.components.smartthings.test_init", "test_config_entry_loads_platforms"), + ( + "tests.components.smartthings.test_init", + "test_scenes_unauthorized_loads_platforms", + ), + ( + "tests.components.smartthings.test_init", + "test_config_entry_loads_unconnected_cloud", + ), + ("tests.components.samsungtv.test_config_flow", "test_ssdp"), + ("tests.components.samsungtv.test_config_flow", "test_user_websocket"), + ("tests.components.samsungtv.test_config_flow", "test_user_already_configured"), + ("tests.components.samsungtv.test_config_flow", "test_autodetect_websocket"), + ("tests.components.samsungtv.test_config_flow", "test_autodetect_websocket_ssl"), + ("tests.components.samsungtv.test_config_flow", "test_ssdp_already_configured"), + ("tests.components.samsungtv.test_config_flow", "test_ssdp_noprefix"), + ("tests.components.samsungtv.test_config_flow", "test_user_legacy"), + ("tests.components.samsungtv.test_config_flow", "test_autodetect_legacy"), + ( + "tests.components.samsungtv.test_media_player", + "test_select_source_invalid_source", + ), + ( + "tests.components.samsungtv.test_media_player", + "test_play_media_channel_as_string", + ), + ( + "tests.components.samsungtv.test_media_player", + "test_play_media_channel_as_non_positive", + ), + ("tests.components.samsungtv.test_media_player", "test_turn_off_websocket"), + ("tests.components.samsungtv.test_media_player", "test_play_media_invalid_type"), + ("tests.components.harmony.test_config_flow", "test_form_import"), + ("tests.components.harmony.test_config_flow", "test_form_ssdp"), + ("tests.components.harmony.test_config_flow", "test_user_form"), +]