diff --git a/CODEOWNERS b/CODEOWNERS index 4598c6f049d03..4d4c7d3d90073 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -261,6 +261,7 @@ homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuheat/* @bdraco homeassistant/components/nuki/* @pvizeli +homeassistant/components/nut/* @bdraco homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index b4ad0a556b288..9bc22ae668982 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -1,7 +1,6 @@ # https://dev.azure.com/home-assistant trigger: - batch: true branches: include: - dev @@ -16,7 +15,6 @@ schedules: branches: include: - dev - always: true variables: - name: versionWheels value: '1.10.1-3.7-alpine3.11' @@ -73,4 +71,4 @@ jobs: sed -i "s|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} done - displayName: 'Prepare requirements files for Hass.io' + displayName: 'Prepare requirements files for Home Assistant wheels' diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 000c23e1d963d..5d939d4b34e9c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -325,15 +325,30 @@ async def _async_set_up_integrations( hass: core.HomeAssistant, config: Dict[str, Any] ) -> None: """Set up all the integrations.""" + + async def async_setup_multi_components(domains: Set[str]) -> None: + """Set up multiple domains. Log on failure.""" + futures = { + domain: hass.async_create_task(async_setup_component(hass, domain, config)) + for domain in domains + } + await asyncio.wait(futures.values()) + errors = [domain for domain in domains if futures[domain].exception()] + for domain in errors: + exception = futures[domain].exception() + _LOGGER.error( + "Error setting up integration %s - received exception", + domain, + exc_info=(type(exception), exception, exception.__traceback__), + ) + domains = _get_domains(hass, config) # Start up debuggers. Start these first in case they want to wait. debuggers = domains & DEBUGGER_INTEGRATIONS if debuggers: _LOGGER.debug("Starting up debuggers %s", debuggers) - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in debuggers) - ) + await async_setup_multi_components(debuggers) domains -= DEBUGGER_INTEGRATIONS # Resolve all dependencies of all components so we can find the logging @@ -358,9 +373,7 @@ async def _async_set_up_integrations( if logging_domains: _LOGGER.info("Setting up %s", logging_domains) - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in logging_domains) - ) + await async_setup_multi_components(logging_domains) # Kick off loading the registries. They don't need to be awaited. asyncio.gather( @@ -370,9 +383,7 @@ async def _async_set_up_integrations( ) if stage_1_domains: - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in stage_1_domains) - ) + await async_setup_multi_components(stage_1_domains) # Load all integrations after_dependencies: Dict[str, Set[str]] = {} @@ -401,9 +412,7 @@ async def _async_set_up_integrations( _LOGGER.debug("Setting up %s", domains_to_load) - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in domains_to_load) - ) + await async_setup_multi_components(domains_to_load) last_load = domains_to_load stage_2_domains -= domains_to_load @@ -413,9 +422,7 @@ async def _async_set_up_integrations( if stage_2_domains: _LOGGER.debug("Final set up: %s", stage_2_domains) - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in stage_2_domains) - ) + await async_setup_multi_components(stage_2_domains) # Wrap up startup await hass.async_block_till_done() diff --git a/homeassistant/components/airvisual/.translations/lb.json b/homeassistant/components/airvisual/.translations/lb.json index 0ae807dde5245..eb267e793bb82 100644 --- a/homeassistant/components/airvisual/.translations/lb.json +++ b/homeassistant/components/airvisual/.translations/lb.json @@ -17,5 +17,13 @@ } }, "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "description": "Verschidden Optioune fir d'AirVisual Integratioun d\u00e9fin\u00e9ieren.", + "title": "Airvisual ariichten" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 9aa3c62e76c9f..1f8176968d80d 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -249,7 +249,7 @@ async def begin_alerting(self): else: await self._schedule_notify() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def end_alerting(self): """End the alert procedures.""" @@ -259,7 +259,7 @@ async def end_alerting(self): self._firing = False if self._send_done_message: await self._notify_done_message() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def _schedule_notify(self): """Schedule a notification.""" diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index 2d5f2412d8528..a494e78bdc765 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -67,7 +67,7 @@ async def async_turn_on(self, **kwargs): else: await self._ipcam.change_setting(self._setting, True) self._state = True - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn device off.""" @@ -80,7 +80,7 @@ async def async_turn_off(self, **kwargs): else: await self._ipcam.change_setting(self._setting, False) self._state = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def icon(self): diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index c34a46a8b827f..8b9f33559309f 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -138,7 +138,7 @@ def state(self): def playstatus_update(self, updater, playing): """Print what is currently playing when it changes.""" self._playing = playing - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def playstatus_error(self, updater, exception): @@ -151,7 +151,7 @@ def playstatus_error(self, updater, exception): # implemented here later. updater.start(initial_delay=10) self._playing = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def media_content_type(self): diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index dde092dd1fa31..002b032fa92b9 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -109,4 +109,4 @@ def async_update_callback(self): panel = self._processor.panel if panel is not None: self._state = getattr(panel, self._type) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index 6950929ee8076..e54fcff139d50 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -6,7 +6,6 @@ from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import DOMAIN, UPDATE_TOPIC @@ -51,7 +50,6 @@ class AquaLogicSwitch(SwitchDevice): def __init__(self, processor, switch_type): """Initialize switch.""" - self._processor = processor self._type = switch_type self._state_name = { @@ -66,6 +64,7 @@ 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): @@ -102,11 +101,11 @@ def turn_off(self, **kwargs): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback + self._unsub_disp = self.hass.helpers.dispatcher.async_dispatcher_connect( + UPDATE_TOPIC, self.async_write_ha_state ) - @callback - def async_update_callback(self): - """Update callback.""" - self.async_schedule_update_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 8a54c745695ec..a49802ea96fae 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -130,7 +130,7 @@ async def async_added_to_hass(self): @callback def _data(host): if host == self._state.client.host: - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def _started(host): @@ -160,7 +160,7 @@ async def async_update(self): async def async_mute_volume(self, mute): """Send mute command.""" await self._state.set_mute(mute) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_select_source(self, source): """Select a specific source.""" @@ -171,7 +171,7 @@ async def async_select_source(self, source): return await self._state.set_source(value) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_select_sound_mode(self, sound_mode): """Select a specific source.""" @@ -184,22 +184,22 @@ async def async_select_sound_mode(self, sound_mode): _LOGGER.error("Unsupported sound_mode %s", sound_mode) return - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" await self._state.set_volume(round(volume * 99.0)) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_volume_up(self): """Turn volume up for media player.""" await self._state.inc_volume() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_volume_down(self): """Turn volume up for media player.""" await self._state.dec_volume() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_on(self): """Turn the media player on.""" diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 8152a76feecc2..e2bb85c9f8404 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -7,7 +7,6 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_BATTERY_LEVEL -from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -62,6 +61,7 @@ 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,12 +69,14 @@ def camera_image(self): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback) + self._unsub_disp = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ARLO, self.async_write_ha_state + ) - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_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.""" diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 014c46fd73c83..6684a5b882b83 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -119,7 +119,7 @@ def set_event(self, event): """Update the sensor with the most recent event.""" self.event = {} self.event.update(event) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def state(self): diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index a0afbed69f155..a0eee38c3f8af 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -51,7 +51,7 @@ cv.ensure_list, [vol.In(SENSOR_TYPES)] ), vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string, - vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.isdir, + vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.string, } ) }, diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index f22a169a1023d..e61c4cea6b03d 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -41,7 +41,7 @@ def device_info(self): @callback def update_callback(self, no_delay=None): """Update the entities state.""" - self.async_schedule_update_ha_state() + self.async_write_ha_state() class AxisEventBase(AxisEntityBase): diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index d7551abebc13b..d992c28746c3d 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -53,13 +53,13 @@ def update_callback(self, no_delay=False): self.remove_timer = None if self.is_on or delay == 0 or no_delay: - self.async_schedule_update_ha_state() + self.async_write_ha_state() return @callback def _delay_update(now): """Timer callback for sensor update.""" - self.async_schedule_update_ha_state() + self.async_write_ha_state() self.remove_timer = None self.remove_timer = async_track_point_in_utc_time( diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 29658c19c5b7e..37141d6017ae9 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure Axis devices.""" +from ipaddress import ip_address + import voluptuous as vol from homeassistant import config_entries @@ -11,6 +13,7 @@ CONF_PORT, CONF_USERNAME, ) +from homeassistant.util.network import is_link_local from .const import CONF_MODEL, DOMAIN from .device import get_device @@ -129,7 +132,7 @@ async def async_step_zeroconf(self, discovery_info): if serial_number[:6] not in AXIS_OUI: return self.async_abort(reason="not_axis_device") - if discovery_info[CONF_HOST].startswith("169.254"): + if is_link_local(ip_address(discovery_info[CONF_HOST])): return self.async_abort(reason="link_local_address") await self.async_set_unique_id(serial_number) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index e2cc0dd31e273..300927abefabe 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -426,7 +426,7 @@ async def async_update_status(self): # communication is moved to a separate library await self.force_update_sync_status() - self.async_schedule_update_ha_state() + self.async_write_ha_state() elif response.status == 595: _LOGGER.info("Status 595 returned, treating as timeout") raise BluesoundPlayer._TimeoutException() @@ -439,7 +439,7 @@ async def async_update_status(self): self._is_online = False self._last_status_update = None self._status = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() _LOGGER.info("Client connection error, marking %s as offline", self._name) raise diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 24150c513dfd0..7f48c7ee22c1d 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "dependencies": [], "codeowners": ["@bieniu"], - "requirements": ["brother==0.1.9"], + "requirements": ["brother==0.1.11"], "zeroconf": ["_printer._tcp.local."], "config_flow": true, "quality_scale": "platinum" diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index e0c48062dfbbe..edac0e6e3ec83 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -356,7 +356,7 @@ async def async_set_cast_info(self, cast_info): self.cast_status = chromecast.status self.media_status = chromecast.media_controller.status self._chromecast.start() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_del_cast_info(self, cast_info): """Remove the service.""" @@ -411,7 +411,7 @@ async def async_set_dynamic_group(self, cast_info): self._dynamic_group_available = False self.dynamic_group_media_status = chromecast.media_controller.status self._dynamic_group_cast.start() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_del_dynamic_group(self): """Remove the dynamic group.""" @@ -432,7 +432,7 @@ async def async_del_dynamic_group(self): self._dynamic_group_invalidate() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def _async_disconnect(self): """Disconnect Chromecast object if it is set.""" @@ -447,7 +447,7 @@ async def _async_disconnect(self): self._cast_info.port, ) self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() await self.hass.async_add_executor_job(self._chromecast.disconnect) if self._dynamic_group_cast is not None: @@ -455,7 +455,7 @@ async def _async_disconnect(self): self._invalidate() - self.async_schedule_update_ha_state() + self.async_write_ha_state() def _invalidate(self): """Invalidate some attributes.""" diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 0a0b9f0fe8895..a40529b6fe4aa 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -10,7 +10,6 @@ CONF_MODE, CONF_NAME, CONF_REGION, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback @@ -191,11 +190,7 @@ async def async_setup(hass, config): client = CloudClient(hass, prefs, websession, alexa_conf, google_conf) cloud = hass.data[DOMAIN] = Cloud(client, **kwargs) - async def _startup(event): - """Startup event.""" - await cloud.start() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _startup) + await cloud.start() async def _shutdown(event): """Shutdown event.""" @@ -230,17 +225,11 @@ async def _on_connect(): return loaded = True - hass.async_create_task( - hass.helpers.discovery.async_load_platform( - "binary_sensor", DOMAIN, {}, config - ) - ) - hass.async_create_task( - hass.helpers.discovery.async_load_platform("stt", DOMAIN, {}, config) - ) - hass.async_create_task( - hass.helpers.discovery.async_load_platform("tts", DOMAIN, {}, config) + await hass.helpers.discovery.async_load_platform( + "binary_sensor", DOMAIN, {}, config ) + await hass.helpers.discovery.async_load_platform("stt", DOMAIN, {}, config) + await hass.helpers.discovery.async_load_platform("tts", DOMAIN, {}, config) cloud.iot.register_on_connect(_on_connect) diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 056105f807101..c2974678faae7 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -62,7 +62,7 @@ async def async_added_to_hass(self): async def async_state_update(data): """Update callback.""" await asyncio.sleep(WAIT_UNTIL_CHANGE) - self.async_schedule_update_ha_state() + self.async_write_ha_state() self._unsub_dispatcher = async_dispatcher_connect( self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index cfbb221c164ef..b8c2bc277f099 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.32.2"], + "requirements": ["hass-nabucasa==0.33.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 6a528a66ba642..d16722525f92e 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -64,7 +64,7 @@ def async_update_callback(self, force_update=False, ignore_update=False): keys = {"on", "reachable", "state"} if force_update or self._device.changed_keys.intersection(keys): - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def is_on(self): diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 0724f9f9b4515..80557caeca68f 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -106,7 +106,7 @@ def async_update_callback(self, force_update=False, ignore_update=False): if ignore_update: return - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def available(self): diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index fd8ffeeaaf0c2..ae0e55ae51f48 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -109,7 +109,7 @@ def async_update_callback(self, force_update=False, ignore_update=False): keys = {"on", "reachable", "state"} if force_update or self._device.changed_keys.intersection(keys): - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def state(self): @@ -174,7 +174,7 @@ def async_update_callback(self, force_update=False, ignore_update=False): keys = {"battery", "reachable"} if force_update or self._device.changed_keys.intersection(keys): - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def unique_id(self): diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 202c588588714..65bbd9affee42 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -182,7 +182,7 @@ def calc_derivative(entity, old_state, new_state): _LOGGER.error("Could not calculate derivative: %s", err) else: self._state = derivative - self.async_schedule_update_ha_state() + self.async_write_ha_state() async_track_state_change(self.hass, self._sensor_source_id, calc_derivative) diff --git a/homeassistant/components/directv/.translations/pl.json b/homeassistant/components/directv/.translations/pl.json index 81305324f5e1c..c02e69601c886 100644 --- a/homeassistant/components/directv/.translations/pl.json +++ b/homeassistant/components/directv/.translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Odbiornik DirecTV jest ju\u017c skonfigurowany." + "already_configured": "Odbiornik DirecTV jest ju\u017c skonfigurowany.", + "unknown": "Niespodziewany b\u0142\u0105d." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", diff --git a/homeassistant/components/doorbird/.translations/de.json b/homeassistant/components/doorbird/.translations/de.json index 8676359e5cafe..3709adaa69ac4 100644 --- a/homeassistant/components/doorbird/.translations/de.json +++ b/homeassistant/components/doorbird/.translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Dieser DoorBird ist bereits konfiguriert" + "already_configured": "Dieser DoorBird ist bereits konfiguriert", + "link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt", + "not_doorbird_device": "Dieses Ger\u00e4t ist kein DoorBird" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", diff --git a/homeassistant/components/doorbird/.translations/en.json b/homeassistant/components/doorbird/.translations/en.json index 9b2c95dd7c989..27c16fac3a17d 100644 --- a/homeassistant/components/doorbird/.translations/en.json +++ b/homeassistant/components/doorbird/.translations/en.json @@ -1,36 +1,36 @@ { - "options" : { - "step" : { - "init" : { - "data" : { - "events" : "Comma separated list of events." - }, - "description" : "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" - } - } - }, - "config" : { - "step" : { - "user" : { - "title" : "Connect to the DoorBird", - "data" : { - "password" : "Password", - "host" : "Host (IP Address)", - "name" : "Device Name", - "username" : "Username" + "config": { + "abort": { + "already_configured": "This DoorBird is already configured", + "link_local_address": "Link local addresses are not supported", + "not_doorbird_device": "This device is not a DoorBird" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host (IP Address)", + "name": "Device Name", + "password": "Password", + "username": "Username" + }, + "title": "Connect to the DoorBird" } - } - }, - "abort" : { - "already_configured" : "This DoorBird is already configured", - "link_local_address": "Link local addresses are not supported", - "not_doorbird_device": "This device is not a DoorBird" - }, - "title" : "DoorBird", - "error" : { - "invalid_auth" : "Invalid authentication", - "unknown" : "Unexpected error", - "cannot_connect" : "Failed to connect, please try again" - } - } -} + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Comma separated list of events." + }, + "description": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/es.json b/homeassistant/components/doorbird/.translations/es.json index e7cf75f38fb23..93ab919cc0301 100644 --- a/homeassistant/components/doorbird/.translations/es.json +++ b/homeassistant/components/doorbird/.translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "DoorBird ya est\u00e1 configurado" + "already_configured": "DoorBird ya est\u00e1 configurado", + "not_doorbird_device": "Este dispositivo no es un DoorBird" }, "error": { "cannot_connect": "No se pudo conectar, por favor int\u00e9ntalo de nuevo", diff --git a/homeassistant/components/doorbird/.translations/lb.json b/homeassistant/components/doorbird/.translations/lb.json index 936dfddf261c7..d0b94ed6c594e 100644 --- a/homeassistant/components/doorbird/.translations/lb.json +++ b/homeassistant/components/doorbird/.translations/lb.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "D\u00ebse DoorBird ass scho konfigur\u00e9iert" + "already_configured": "D\u00ebse DoorBird ass scho konfigur\u00e9iert", + "link_local_address": "Lokal Link Adressen ginn net \u00ebnnerst\u00ebtzt", + "not_doorbird_device": "D\u00ebsen Apparat ass kee DoorBird" }, "error": { "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", diff --git a/homeassistant/components/doorbird/.translations/no.json b/homeassistant/components/doorbird/.translations/no.json index 91784f0b42ad4..29fb34672c86f 100644 --- a/homeassistant/components/doorbird/.translations/no.json +++ b/homeassistant/components/doorbird/.translations/no.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Denne DoorBird er allerede konfigurert" + "already_configured": "Denne DoorBird er allerede konfigurert", + "link_local_address": "Linking av lokale adresser st\u00f8ttes ikke", + "not_doorbird_device": "Denne enheten er ikke en DoorBird" }, "error": { "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", diff --git a/homeassistant/components/doorbird/.translations/zh-Hant.json b/homeassistant/components/doorbird/.translations/zh-Hant.json index afeded494c6ed..d8b6330b87995 100644 --- a/homeassistant/components/doorbird/.translations/zh-Hant.json +++ b/homeassistant/components/doorbird/.translations/zh-Hant.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u6b64 DoorBird \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u6b64 DoorBird \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", + "not_doorbird_device": "\u6b64\u8a2d\u5099\u4e26\u975e DoorBird" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 410fb13a2121f..aa712a63ed0b8 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DoorBird integration.""" +from ipaddress import ip_address import logging import urllib @@ -8,6 +9,7 @@ from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.util.network import is_link_local from .const import CONF_EVENTS, DOORBIRD_OUI from .const import DOMAIN # pylint:disable=unused-import @@ -90,7 +92,7 @@ async def async_step_zeroconf(self, discovery_info): if macaddress[:6] != DOORBIRD_OUI: return self.async_abort(reason="not_doorbird_device") - if discovery_info[CONF_HOST].startswith("169.254"): + if is_link_local(ip_address(discovery_info[CONF_HOST])): return self.async_abort(reason="link_local_address") await self.async_set_unique_id(macaddress) diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 01c010c4971ad..341451522d4ec 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -48,7 +48,7 @@ def message_received(message): else: self._state = message.payload - self.async_schedule_update_ha_state() + self.async_write_ha_state() await mqtt.async_subscribe(self.hass, self._topic, message_received, 1) diff --git a/homeassistant/components/elkm1/.translations/es.json b/homeassistant/components/elkm1/.translations/es.json index 6602ff3da2e45..8fdce004c41a1 100644 --- a/homeassistant/components/elkm1/.translations/es.json +++ b/homeassistant/components/elkm1/.translations/es.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "address_already_configured": "Ya est\u00e1 configurado un Elk-M1 con esta direcci\u00f3n", + "already_configured": "Ya est\u00e1 configurado un Elk-M1 con este prefijo" + }, "error": { "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", @@ -8,9 +12,17 @@ "step": { "user": { "data": { - "protocol": "Protocolo" - } + "address": "La direcci\u00f3n IP o dominio o puerto serie si se conecta a trav\u00e9s de serie.", + "password": "Contrase\u00f1a (s\u00f3lo seguro)", + "prefix": "Un prefijo \u00fanico (d\u00e9jalo en blanco si s\u00f3lo tienes un Elk-M1).", + "protocol": "Protocolo", + "temperature_unit": "La temperatura que usa la unidad Elk-M1", + "username": "Usuario (s\u00f3lo seguro)" + }, + "description": "La cadena de direcci\u00f3n debe estar en el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no-seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no-seguro' y 2601 para 'seguro'. Para el protocolo serie, la direcci\u00f3n debe tener la forma 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. Los baudios son opcionales y el valor predeterminado es 115200.", + "title": "Conectar con Control Elk-M1" } - } + }, + "title": "Control Elk-M1" } } \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/lb.json b/homeassistant/components/elkm1/.translations/lb.json new file mode 100644 index 0000000000000..bb56b4c8154f1 --- /dev/null +++ b/homeassistant/components/elkm1/.translations/lb.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "address_already_configured": "Een ElkM1 mat d\u00ebser Adress ass scho konfigur\u00e9iert", + "already_configured": "Een ElkM1 mat d\u00ebsem Prefix ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "address": "IP Adress oder Domain oder Serielle Port falls d'Verbindung seriell ass.", + "password": "Passwuert (n\u00ebmmen ges\u00e9chert)", + "prefix": "Een eenzegaartege Pr\u00e4fix (eidel lossen wann et n\u00ebmmen 1 ElkM1 g\u00ebtt)", + "protocol": "Protokoll", + "temperature_unit": "Temperatur Eenheet d\u00e9i den ElkM1 benotzt.", + "username": "Benotzernumm (n\u00ebmmen ges\u00e9chert)" + }, + "description": "D'Adress muss an der Form 'adress[:port]' fir 'ges\u00e9chert' an 'onges\u00e9chert' sinn. Beispill: '192.168.1.1'. De Port os optionell an ass standardm\u00e9isseg op 2101 fir 'onges\u00e9chert' an op 2601 fir 'ges\u00e9chert' d\u00e9fin\u00e9iert. Fir de serielle Protokoll, muss d'Adress an der Form 'tty[:baud]' sinn. Beispill: '/dev/ttyS1'. Baud Rate ass optionell an ass standardmlisseg op 115200 d\u00e9fin\u00e9iert.", + "title": "Mat Elk-M1 Control verbannen" + } + }, + "title": "Elk-M1 Control" + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/ru.json b/homeassistant/components/elkm1/.translations/ru.json index 1575b47ed68e7..11e04ad816c6e 100644 --- a/homeassistant/components/elkm1/.translations/ru.json +++ b/homeassistant/components/elkm1/.translations/ru.json @@ -1,7 +1,25 @@ { "config": { + "abort": { + "address_already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, "step": { "user": { + "data": { + "address": "IP-\u0430\u0434\u0440\u0435\u0441, \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0438 'secure')", + "prefix": "\u0423\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d ElkM1).", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "temperature_unit": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", + "username": "\u041b\u043e\u0433\u0438\u043d (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0438 'secure')" + }, + "description": "\u0421\u0442\u0440\u043e\u043a\u0430 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0435 'addres[:port]' \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0439 'secure' \u0438 'non-secure'. \u041f\u0440\u0438\u043c\u0435\u0440: '192.168.1.1'. \u041f\u043e\u0440\u0442 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0438 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e 2101 \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0438 'non-secure'\u00bb \u0438 2601 \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0438 'secure'\u00bb. \u0414\u043b\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 \u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0435 'tty[:baud]'. \u041f\u0440\u0438\u043c\u0435\u0440: '/dev/ttyS1'. Baud \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0438 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0440\u0430\u0432\u0435\u043d 115200.", "title": "Elk-M1 Control" } }, diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index e063fc49f2fd3..19fad984b27b8 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -166,7 +166,7 @@ def async_update_callback(self, msg): self.media_status_last_position = None self.media_status_received = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def available(self): diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 7630169dcadaf..beb1c1cda822b 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -121,7 +121,7 @@ async def async_added_to_hass(self): def _update_callback(self, partition): """Update Home Assistant state, if needed.""" if partition is None or int(partition) == self._partition_number: - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def code_format(self): diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py index fbe9824d06733..f698a9d27d92d 100644 --- a/homeassistant/components/envisalink/binary_sensor.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -94,4 +94,4 @@ def device_class(self): def _update_callback(self, zone): """Update the zone's state, if needed.""" if zone is None or int(zone) == self._zone_number: - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index 05ad0783facda..b4f15a2999e23 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -74,4 +74,4 @@ def device_state_attributes(self): def _update_callback(self, partition): """Update the partition state in HA, if needed.""" if partition is None or int(partition) == self._partition_number: - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 9fbe3eff8227a..3895e172024ef 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -510,7 +510,7 @@ async def async_added_to_hass(self) -> None: async def _on_state_update(self) -> None: """Update the entity state when state or static info changed.""" - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def _on_device_update(self) -> None: """Update the entity state when device info has changed.""" @@ -519,7 +519,7 @@ async def _on_device_update(self) -> None: # Only update the HA state when the full state arrives # through the next entity state packet. return - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_will_remove_from_hass(self) -> None: """Unregister callbacks.""" diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index bc402b46fb21a..ad0c590b87db0 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -202,6 +202,6 @@ async def async_shutdown_handle(event): async def async_start_handle(event): """Start FFmpeg process.""" await self._async_start_ffmpeg(None) - self.async_schedule_update_ha_state() + self.async_write_ha_state() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start_handle) diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 294fcc2518fa2..e3a9c09b5d97b 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -70,7 +70,7 @@ def __init__(self, config): def _async_callback(self, state): """HA-FFmpeg callback for noise detection.""" self._state = state - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def is_on(self): diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 4d508ce2d8179..7c2a35938b261 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -212,7 +212,7 @@ def filter_sensor_state_listener(entity, old_state, new_state, update_ha=True): ) if update_ha: - self.async_schedule_update_ha_state() + self.async_write_ha_state() if "recorder" in self.hass.config.components: history_list = [] diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 0205bb308be60..61bdb9a28621d 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -233,7 +233,7 @@ async def async_turn_on(self, **kwargs): # Make initial update await self.async_flux_update() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn off flux.""" @@ -241,7 +241,7 @@ async def async_turn_off(self, **kwargs): self.unsub_tracker() self.unsub_tracker = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_flux_update(self, utcnow=None): """Update all the lights using flux.""" diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index e1ae4bca255cf..8a540a96b0ea8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20200330.0" + "home-assistant-frontend==20200401.0" ], "dependencies": [ "api", diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index a7ddcc0831491..09af5ad5c44fb 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -368,7 +368,7 @@ def _async_switch_changed(self, entity_id, old_state, new_state): """Handle heater switch state changes.""" if new_state is None: return - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def _async_update_temp(self, state): diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 1d53525ab3739..ff5aff6fe500e 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -129,7 +129,7 @@ def _on_new_monitor(self, *args): async def async_will_remove_from_hass(self): """Remove listener from the sensor.""" if self._sensor: - self._sensor.remove_listener(self._schedule_update) + self._sensor.remove_listener(self.async_write_ha_state) else: monitors = self.hass.data[DATA_GREENEYE_MONITOR] monitors.remove_listener(self._on_new_monitor) @@ -140,16 +140,13 @@ def _try_connect_to_monitor(self, monitors): return False self._sensor = self._get_sensor(monitor) - self._sensor.add_listener(self._schedule_update) + self._sensor.add_listener(self.async_write_ha_state) return True def _get_sensor(self, monitor): raise NotImplementedError() - def _schedule_update(self): - self.async_schedule_update_ha_state(False) - class CurrentSensor(GEMSensor): """Entity showing power usage on one channel of the monitor.""" diff --git a/homeassistant/components/griddy/.translations/lb.json b/homeassistant/components/griddy/.translations/lb.json index 3ed4a9c550a69..c0ee3bc7d5ad2 100644 --- a/homeassistant/components/griddy/.translations/lb.json +++ b/homeassistant/components/griddy/.translations/lb.json @@ -1,9 +1,21 @@ { "config": { + "abort": { + "already_configured": "D\u00ebs Lued Zon ass scho konfigur\u00e9iert" + }, "error": { "cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.", "unknown": "Onerwaarte Feeler" }, + "step": { + "user": { + "data": { + "loadzone": "Lued Zone (Punkt vum R\u00e9glement)" + }, + "description": "Deng Lued Zon ass an dengem Griddy Kont enner \"Account > Meter > Load Zone.\"", + "title": "Griddy Lued Zon ariichten" + } + }, "title": "Griddy" } } \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/pl.json b/homeassistant/components/harmony/.translations/pl.json new file mode 100644 index 0000000000000..a9f611d0f353f --- /dev/null +++ b/homeassistant/components/harmony/.translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", + "title": "Konfiguracja Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa huba" + }, + "title": "Konfiguracja Logitech Harmony Hub" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Domy\u015blna aktywno\u015b\u0107 do wykonania, gdy \u017cadnej nie okre\u015blono.", + "delay_secs": "Op\u00f3\u017anienie mi\u0119dzy wysy\u0142aniem polece\u0144." + }, + "description": "Dostosuj opcje huba Harmony" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 7d23e15a4e70e..024af9b15804b 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -264,7 +264,7 @@ def new_activity(self, activity_info: tuple) -> None: self._current_activity = activity_name self._state = bool(activity_id != -1) self._available = True - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def new_config(self, _=None): """Call for updating the current activity.""" @@ -289,7 +289,7 @@ async def got_disconnected(self, _=None): if not self._available: # Still disconnected. Let the state engine know. - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_on(self, **kwargs): """Start an activity from the Harmony device.""" diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index edd3388e74f54..88103ec94c145 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -12,7 +12,6 @@ CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send @@ -183,14 +182,17 @@ 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.""" - async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) + self._unsub_disp = 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 - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_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/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index 1750e9b0ff452..52a82184dccf3 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -136,7 +136,7 @@ def handle_event_callback(self, event): """Propagate changes through ha.""" _LOGGER.debug("Relay %s new state callback: %r", self._device_port, event) self._is_on = event - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def should_poll(self): @@ -156,7 +156,7 @@ def available(self): @callback def _availability_callback(self, availability): """Update availability state.""" - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Register update callback.""" diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index f5316350091e1..fd3958344f50a 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -102,7 +102,7 @@ def _async_device_changed(self, *args, **kwargs) -> None: # Don't update disabled entities if self.enabled: _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) - self.async_schedule_update_ha_state() + self.async_write_ha_state() else: _LOGGER.debug( "Device Changed Event for %s (Alarm Control Panel) not fired. Entity is disabled.", diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 0407a1a0fe270..c2b67758152a6 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -108,7 +108,7 @@ def _async_device_changed(self, *args, **kwargs) -> None: # Don't update disabled entities if self.enabled: _LOGGER.debug("Event %s (%s)", self.name, self._device.modelType) - self.async_schedule_update_ha_state() + self.async_write_ha_state() else: _LOGGER.debug( "Device Changed Event for %s (%s) not fired. Entity is disabled.", diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 56d5bcacc476a..4cfb2b0a26d81 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -93,4 +93,4 @@ def _update_callback(self, msg_type, values): self._level = int((values[1] * 255.0) / 100.0) if self._level != 0: self._prev_level = self._level - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json index 4029b24df3f0d..17e4f7b8ef2e8 100644 --- a/homeassistant/components/huawei_lte/.translations/pl.json +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "To urz\u0105dzenie jest ju\u017c skonfigurowane.", - "already_in_progress": "To urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_in_progress": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE" }, "error": { diff --git a/homeassistant/components/hue/.translations/da.json b/homeassistant/components/hue/.translations/da.json index afcfd7071e7e7..c00c19be42a60 100644 --- a/homeassistant/components/hue/.translations/da.json +++ b/homeassistant/components/hue/.translations/da.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f8rste knap", + "button_2": "Anden knap", + "button_3": "Tredje knap", + "button_4": "Fjerde knap", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", + "turn_off": "Sluk", + "turn_on": "T\u00e6nd" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\"-knappen frigivet efter langt tryk", + "remote_button_short_press": "\"{subtype}\"-knappen trykket p\u00e5", + "remote_button_short_release": "\"{subtype}\"-knappen frigivet" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index 350360285af41..b16213bfbf821 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "dim_down": "Dim down", + "dim_up": "Dim up", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py new file mode 100644 index 0000000000000..8187765474676 --- /dev/null +++ b/homeassistant/components/hue/device_trigger.py @@ -0,0 +1,149 @@ +"""Provides device automations for Philips Hue events.""" +import logging + +import voluptuous as vol + +import homeassistant.components.automation.event as event +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_EVENT, + CONF_PLATFORM, + CONF_TYPE, +) + +from . import DOMAIN +from .hue_event import CONF_HUE_EVENT, CONF_UNIQUE_ID + +_LOGGER = logging.getLogger(__file__) + +CONF_SUBTYPE = "subtype" + +CONF_SHORT_PRESS = "remote_button_short_press" +CONF_SHORT_RELEASE = "remote_button_short_release" +CONF_LONG_RELEASE = "remote_button_long_release" + +CONF_TURN_ON = "turn_on" +CONF_TURN_OFF = "turn_off" +CONF_DIM_UP = "dim_up" +CONF_DIM_DOWN = "dim_down" +CONF_BUTTON_1 = "button_1" +CONF_BUTTON_2 = "button_2" +CONF_BUTTON_3 = "button_3" +CONF_BUTTON_4 = "button_4" + + +HUE_DIMMER_REMOTE_MODEL = "Hue dimmer switch" # RWL020/021 +HUE_DIMMER_REMOTE = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, +} + +HUE_TAP_REMOTE_MODEL = "Hue tap switch" # ZGPSWITCH +HUE_TAP_REMOTE = { + (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 34}, + (CONF_SHORT_PRESS, CONF_BUTTON_2): {CONF_EVENT: 16}, + (CONF_SHORT_PRESS, CONF_BUTTON_3): {CONF_EVENT: 17}, + (CONF_SHORT_PRESS, CONF_BUTTON_4): {CONF_EVENT: 18}, +} + +REMOTES = { + HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, + HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, +} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} +) + + +def _get_hue_event_from_device_id(hass, device_id): + """Resolve hue event from device id.""" + for bridge in hass.data.get(DOMAIN, {}).values(): + for hue_event in bridge.sensor_manager.current_events.values(): + if device_id == hue_event.device_registry_id: + return hue_event + + return None + + +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(config[CONF_DEVICE_ID]) + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + + if ( + not device + or device.model not in REMOTES + or trigger not in REMOTES[device.model] + ): + raise InvalidDeviceAutomationConfig + + return config + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(config[CONF_DEVICE_ID]) + + hue_event = _get_hue_event_from_device_id(hass, device.id) + if hue_event is None: + raise InvalidDeviceAutomationConfig + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + + trigger = REMOTES[device.model][trigger] + + event_config = { + event.CONF_PLATFORM: "event", + event.CONF_EVENT_TYPE: CONF_HUE_EVENT, + event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: hue_event.unique_id, **trigger}, + } + + event_config = event.TRIGGER_SCHEMA(event_config) + return await event.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers. + + Make sure device is a supported remote model. + Retrieve the hue event object matching device entry. + Generate device trigger list. + """ + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(device_id) + + if device.model not in REMOTES: + return + + triggers = [] + for trigger, subtype in REMOTES[device.model].keys(): + triggers.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/hue_event.py index 838d5ead6da7b..ed1bc1c8f7d9b 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/hue_event.py @@ -28,6 +28,7 @@ class HueEvent(GenericHueDevice): def __init__(self, sensor, name, bridge, primary_sensor=None): """Register callback that will be used for signals.""" super().__init__(sensor, name, bridge, primary_sensor) + self.device_registry_id = None self.event_id = slugify(self.sensor.name) # Use the 'lastupdated' string to detect new remote presses @@ -79,9 +80,10 @@ async def async_update_device_registry(self): entry = device_registry.async_get_or_create( config_entry_id=self.bridge.config_entry.entry_id, **self.device_info ) + self.device_registry_id = entry.id _LOGGER.debug( "Event registry with entry_id: %s and device_id: %s", - entry.id, + self.device_registry_id, self.device_id, ) diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 78b990d5f4276..0f70c49ff2e9d 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -27,5 +27,22 @@ "already_in_progress": "Config flow for bridge is already in progress.", "not_hue_bridge": "Not a Hue bridge" } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "dim_down": "Dim down", + "dim_up": "Dim up", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 16c8deac72e7b..214bdf302ea27 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -25,7 +25,6 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -203,14 +202,18 @@ 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.""" - async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) + self._unsub_disp = async_dispatcher_connect( + self.hass, DOMAIN, self.async_write_ha_state + ) - @callback - def _update_callback(self) -> None: - self.async_schedule_update_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: diff --git a/homeassistant/components/icloud/.translations/lb.json b/homeassistant/components/icloud/.translations/lb.json index 8ecc49a5ad97f..0aa3c90eff0dd 100644 --- a/homeassistant/components/icloud/.translations/lb.json +++ b/homeassistant/components/icloud/.translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kont ass scho konfigur\u00e9iert" + "already_configured": "Kont ass scho konfigur\u00e9iert", + "no_device": "Kee vun dengen Apparater huet \"Find my iPhone\" aktiv\u00e9iert" }, "error": { "login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert", diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index c489dd8e38257..e411cd82045d1 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -80,7 +80,7 @@ def async_entity_update(self, deviceid, group, val): group, val, ) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Register INSTEON update events.""" diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 6201348f21c09..3365e5b10c821 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -172,7 +172,7 @@ def calc_integration(entity, old_state, new_state): _LOGGER.error("Could not calculate integral: %s", err) else: self._state += integral - self.async_schedule_update_ha_state() + self.async_write_ha_state() async_track_state_change(self.hass, self._sensor_source_id, calc_integration) diff --git a/homeassistant/components/ipp/.translations/ca.json b/homeassistant/components/ipp/.translations/ca.json new file mode 100644 index 0000000000000..5708c8e638dcc --- /dev/null +++ b/homeassistant/components/ipp/.translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "flow_title": "Impressora: {name}", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/da.json b/homeassistant/components/ipp/.translations/da.json new file mode 100644 index 0000000000000..ede4601928e94 --- /dev/null +++ b/homeassistant/components/ipp/.translations/da.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Denne printer er allerede konfigureret.", + "connection_error": "Kunne ikke oprette forbindelse til printeren.", + "connection_upgrade": "Der kunne ikke oprettes forbindelse til printeren, fordi der kr\u00e6ves opgradering af forbindelsen." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til printeren.", + "connection_upgrade": "Kunne ikke oprette forbindelse til printeren. Pr\u00f8v igen med indstillingen SSL/TLS markeret." + }, + "flow_title": "Printer: {name}", + "step": { + "user": { + "data": { + "base_path": "Relativ sti til printeren", + "host": "V\u00e6rt eller IP-adresse", + "port": "Port", + "ssl": "Printeren underst\u00f8tter kommunikation via SSL/TLS", + "verify_ssl": "Printeren bruger et korrekt SSL-certifikat" + }, + "description": "Konfigurer din printer via Internet Printing Protocol (IPP) til at integrere med Home Assistant.", + "title": "Forbind din printer" + }, + "zeroconf_confirm": { + "description": "Vil du tilf\u00f8je printeren med navnet '{name}' til Home Assistant?", + "title": "Fandt printer" + } + }, + "title": "Internet Printing Protocol (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/de.json b/homeassistant/components/ipp/.translations/de.json new file mode 100644 index 0000000000000..7e72fb2403f62 --- /dev/null +++ b/homeassistant/components/ipp/.translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Dieser Drucker ist bereits konfiguriert", + "connection_error": "Verbindung zum Drucker fehlgeschlagen.", + "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen, da ein Verbindungsupgrade erforderlich ist." + }, + "error": { + "connection_error": "Verbindung zum Drucker fehlgeschlagen.", + "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen. Bitte versuchen Sie es erneut mit aktivierter SSL / TLS-Option." + }, + "flow_title": "Drucker: {name}", + "step": { + "user": { + "data": { + "base_path": "Relativer Pfad zum Drucker", + "host": "Host oder IP-Adresse", + "port": "Port", + "ssl": "Der Drucker unterst\u00fctzt die Kommunikation \u00fcber SSL / TLS", + "verify_ssl": "Der Drucker verwendet ein ordnungsgem\u00e4\u00dfes SSL-Zertifikat" + }, + "description": "Richten Sie Ihren Drucker \u00fcber das Internet Printing Protocol (IPP) f\u00fcr die Integration in Home Assistant ein.", + "title": "Verbinden Sie Ihren Drucker" + }, + "zeroconf_confirm": { + "description": "M\u00f6chten Sie den Drucker mit dem Namen \"{name}\" zu Home Assistant hinzuf\u00fcgen?", + "title": "Entdeckter Drucker" + } + }, + "title": "Internet-Druckprotokoll (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/es.json b/homeassistant/components/ipp/.translations/es.json new file mode 100644 index 0000000000000..6e86f702902ab --- /dev/null +++ b/homeassistant/components/ipp/.translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Esta impresora ya est\u00e1 configurada.", + "connection_error": "No se pudo conectar con la impresora.", + "connection_upgrade": "No se pudo conectar con la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n." + }, + "error": { + "connection_error": "No se pudo conectar con la impresora.", + "connection_upgrade": "No se pudo conectar con la impresora. Int\u00e9ntalo de nuevo con la opci\u00f3n SSL/TLS marcada." + }, + "flow_title": "Impresora: {name}", + "step": { + "user": { + "data": { + "base_path": "Ruta relativa a la impresora", + "host": "Host o direcci\u00f3n IP", + "port": "Puerto", + "ssl": "La impresora admite la comunicaci\u00f3n a trav\u00e9s de SSL/TLS", + "verify_ssl": "La impresora usa un certificado SSL adecuado" + }, + "description": "Configura tu impresora a trav\u00e9s del Protocolo de Impresi\u00f3n de Internet (IPP) para integrarla con Home Assistant.", + "title": "Vincula tu impresora" + }, + "zeroconf_confirm": { + "description": "\u00bfQuieres a\u00f1adir la impresora llamada `{name}` a Home Assistant?", + "title": "Impresora encontrada" + } + }, + "title": "Protocolo de Impresi\u00f3n de Internet (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/lb.json b/homeassistant/components/ipp/.translations/lb.json new file mode 100644 index 0000000000000..bdda2cf1c14be --- /dev/null +++ b/homeassistant/components/ipp/.translations/lb.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse Printer ass scho konfigur\u00e9iert.", + "connection_error": "Feeler beim verbannen mam Printer.", + "connection_upgrade": "Feeler beim verbannen mam Printer well eng Aktualis\u00e9ierung vun der Verbindung erfuerderlech ass." + }, + "error": { + "connection_error": "Feeler beim verbannen mam Printer.", + "connection_upgrade": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol mat aktiv\u00e9ierter SSL/TLS Optioun." + }, + "flow_title": "Printer: {name}", + "step": { + "user": { + "data": { + "base_path": "Relative Pad zum Printer", + "host": "Numm oder IP Adresse", + "port": "Port", + "ssl": "Printer \u00ebnnerst\u00ebtze Kommunikatioun iwwer SSL/TLS", + "verify_ssl": "Printer benotzt ee g\u00ebltegen SSL Zertifikat" + }, + "description": "Konfigur\u00e9ier d\u00e4in Printer mat Internet Printing Protocol (IPP) fir en am Home Assistant z'int\u00e9gr\u00e9ieren.", + "title": "\u00c4re Printer verbannen" + }, + "zeroconf_confirm": { + "description": "W\u00ebllt dir de Printer mam Numm `{name}` am Home Assistant dob\u00e4isetzen?", + "title": "Entdeckte Printer" + } + }, + "title": "Internet Printing Protocol (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/no.json b/homeassistant/components/ipp/.translations/no.json new file mode 100644 index 0000000000000..2357aaaa86df8 --- /dev/null +++ b/homeassistant/components/ipp/.translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Denne skriveren er allerede konfigurert.", + "connection_error": "Klarte ikke \u00e5 koble til skriveren.", + "connection_upgrade": "Kunne ikke koble til skriveren fordi tilkoblingsoppgradering var n\u00f8dvendig." + }, + "error": { + "connection_error": "Klarte ikke \u00e5 koble til skriveren.", + "connection_upgrade": "Kunne ikke koble til skriveren. Vennligst pr\u00f8v igjen med alternativet SSL / TLS merket." + }, + "flow_title": "Skriver: {name}", + "step": { + "user": { + "data": { + "base_path": "Relativ bane til skriveren", + "host": "Vert eller IP-adresse", + "port": "Port", + "ssl": "Skriveren st\u00f8tter kommunikasjon over SSL/TLS", + "verify_ssl": "Skriveren bruker et riktig SSL-sertifikat" + }, + "description": "Konfigurer skriveren din via Internet Printing Protocol (IPP) for \u00e5 integrere med Home Assistant.", + "title": "Koble til skriveren din" + }, + "zeroconf_confirm": { + "description": "\u00d8nsker du \u00e5 legge skriveren med navnet {name} til Home Assistant?", + "title": "Oppdaget skriver" + } + }, + "title": "Internet Printing Protocol (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/ru.json b/homeassistant/components/ipp/.translations/ru.json new file mode 100644 index 0000000000000..902289b2e8fd9 --- /dev/null +++ b/homeassistant/components/ipp/.translations/ru.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443.", + "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443 \u0438\u0437-\u0437\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f." + }, + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443.", + "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 SSL/TLS." + }, + "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440: {name}", + "step": { + "user": { + "data": { + "base_path": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443", + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u044f\u0437\u044c \u043f\u043e SSL/TLS", + "verify_ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "title": "Internet Printing Protocol (IPP)" + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440 `{name}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u0438\u043d\u0442\u0435\u0440" + } + }, + "title": "Internet Printing Protocol (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/zh-Hant.json b/homeassistant/components/ipp/.translations/zh-Hant.json new file mode 100644 index 0000000000000..fe79b4b88cd00 --- /dev/null +++ b/homeassistant/components/ipp/.translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u5370\u8868\u6a5f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "connection_error": "\u5370\u8868\u6a5f\u9023\u7dda\u5931\u6557\u3002", + "connection_upgrade": "\u7531\u65bc\u9700\u8981\u5148\u5347\u7d1a\u9023\u7dda\u3001\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002" + }, + "error": { + "connection_error": "\u5370\u8868\u6a5f\u9023\u7dda\u5931\u6557\u3002", + "connection_upgrade": "\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002\u8acb\u52fe\u9078 SSL/TLS \u9078\u9805\u5f8c\u518d\u8a66\u4e00\u6b21\u3002" + }, + "flow_title": "\u5370\u8868\u6a5f\uff1a{name}", + "step": { + "user": { + "data": { + "base_path": "\u5370\u8868\u6a5f\u76f8\u5c0d\u8def\u5f91", + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u5370\u8868\u6a5f\u652f\u63f4 SSL/TLS \u901a\u8a0a", + "verify_ssl": "\u5370\u8868\u6a5f\u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49" + }, + "description": "\u900f\u904e\u7db2\u969b\u7db2\u8def\u5217\u5370\u5354\u5b9a\uff08IPP\uff09\u8a2d\u5b9a\u5370\u8868\u6a5f\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7d50\u5370\u8868\u6a5f" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u65b0\u589e\u540d\u7a31 `{name}` \u5370\u8868\u6a5f\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684\u5370\u8868\u6a5f" + } + }, + "title": "\u7db2\u969b\u7db2\u8def\u5217\u5370\u5354\u5b9a\uff08IPP\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index dde71d14a9c62..b17313925a8f7 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -175,7 +175,7 @@ def controller_update(ctrl: Controller) -> None: """Handle controller data updates.""" if ctrl is not self._controller: return - self.async_schedule_update_ha_state() + self.async_write_ha_state() for zone in self.zones.values(): zone.async_schedule_update_ha_state() @@ -210,7 +210,7 @@ def set_available(self, available: bool, ex: Exception = None) -> None: ) self._available = available - self.async_schedule_update_ha_state() + self.async_write_ha_state() for zone in self.zones.values(): zone.async_schedule_update_ha_state() @@ -439,7 +439,7 @@ def zone_update(ctrl: Controller, zone: Zone) -> None: if zone is not self._zone: return self._name = zone.name.title() - self.async_schedule_update_ha_state() + self.async_write_ha_state() self.async_on_remove( async_dispatcher_connect(self.hass, DISPATCH_ZONE_UPDATE, zone_update) @@ -549,7 +549,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target operation mode.""" mode = self._state_to_pizone[hvac_mode] await self._controller.wrap_and_catch(self._zone.set_mode(mode)) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def is_on(self): @@ -562,9 +562,9 @@ async def async_turn_on(self): await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.AUTO)) else: await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.OPEN)) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self): """Turn device off (close zone).""" await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.CLOSE)) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index b13906b44f5cb..4a58f8f43c236 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -94,7 +94,7 @@ def device_state_attributes(self): def clear_unlock_state(self, _): """Clear unlock state automatically.""" self._state = STATE_LOCKED - self.async_schedule_update_ha_state() + self.async_write_ha_state() def unlock(self, **kwargs): """Unlock the device.""" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index d0b8d8c1163f5..3c67e2fd55895 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -204,7 +204,7 @@ def stop_auto_updater(self): @callback def auto_updater_hook(self, now): """Call for the autoupdater.""" - self.async_schedule_update_ha_state() + self.async_write_ha_state() if self.device.position_reached(): self.stop_auto_updater() diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 4fd86d078a06e..dccc4ac07653b 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -364,14 +364,14 @@ def async_on_stop(self, sender, data): self._item = {} self._media_position_updated_at = None self._media_position = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def async_on_volume_changed(self, sender, data): """Handle the volume changes.""" self._app_properties["volume"] = data["volume"] self._app_properties["muted"] = data["muted"] - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def async_on_quit(self, sender, data): @@ -429,7 +429,7 @@ async def ws_loop_wrapper(): # to reconnect on the next poll. pass # Update HA state after Kodi disconnects - self.async_schedule_update_ha_state() + self.async_write_ha_state() # Create a task instead of adding a tracking job, since this task will # run until the websocket connection is closed. diff --git a/homeassistant/components/konnected/.translations/ca.json b/homeassistant/components/konnected/.translations/ca.json index fbfa91839413e..d5fbb60ae71a0 100644 --- a/homeassistant/components/konnected/.translations/ca.json +++ b/homeassistant/components/konnected/.translations/ca.json @@ -95,7 +95,7 @@ "pause": "Pausa entre polsos (ms) (opcional)", "repeat": "Repeticions (-1 = infinit) (opcional)" }, - "description": "Selecciona les opcions de sortida per a {zone}", + "description": "Selecciona les opcions de sortida per a {zone}: estat {state}", "title": "Configuraci\u00f3 de sortida commutable" } }, diff --git a/homeassistant/components/konnected/.translations/de.json b/homeassistant/components/konnected/.translations/de.json index ffd8f3219fe31..a0da84fd09898 100644 --- a/homeassistant/components/konnected/.translations/de.json +++ b/homeassistant/components/konnected/.translations/de.json @@ -34,6 +34,7 @@ "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t" }, "error": { + "bad_host": "Ung\u00fcltige Override-API-Host-URL", "one": "eins", "other": "andere" }, @@ -85,6 +86,10 @@ "title": "Konfigurieren Sie Erweiterte I/O" }, "options_misc": { + "data": { + "api_host": "API-Host-URL \u00fcberschreiben (optional)", + "override_api_host": "\u00dcberschreiben Sie die Standard-Host-Panel-URL der Home Assistant-API" + }, "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel" }, "options_switch": { diff --git a/homeassistant/components/konnected/.translations/es.json b/homeassistant/components/konnected/.translations/es.json index b6591b03d33a6..cfd05320e359c 100644 --- a/homeassistant/components/konnected/.translations/es.json +++ b/homeassistant/components/konnected/.translations/es.json @@ -95,6 +95,7 @@ "data": { "activation": "Salida cuando est\u00e1 activada", "momentary": "Duraci\u00f3n del pulso (ms) (opcional)", + "more_states": "Configurar estados adicionales para esta zona", "name": "Nombre (opcional)", "pause": "Pausa entre pulsos (ms) (opcional)", "repeat": "Tiempos de repetici\u00f3n (-1 = infinito) (opcional)" diff --git a/homeassistant/components/konnected/.translations/lb.json b/homeassistant/components/konnected/.translations/lb.json index 12493169691c7..984e3b79f54e4 100644 --- a/homeassistant/components/konnected/.translations/lb.json +++ b/homeassistant/components/konnected/.translations/lb.json @@ -34,6 +34,7 @@ "not_konn_panel": "Keen erkannten Konnected.io Apparat" }, "error": { + "bad_host": "Iwwerschriwwen API Host URL ong\u00eblteg", "one": "Ee", "other": "M\u00e9i" }, @@ -86,7 +87,9 @@ }, "options_misc": { "data": { - "blink": "Blink panel LED un wann Status \u00c4nnerung gesch\u00e9ckt g\u00ebtt" + "api_host": "API Host URL iwwerschr\u00e9iwen (optionell)", + "blink": "Blink panel LED un wann Status \u00c4nnerung gesch\u00e9ckt g\u00ebtt", + "override_api_host": "Standard Home Assistant API Host Tableau URL iwwerschr\u00e9iwen" }, "description": "Wielt w.e.g. dat gew\u00ebnschte Verhalen fir \u00c4re Panel aus", "title": "Divers Optioune astellen" @@ -95,6 +98,7 @@ "data": { "activation": "Ausgang wann un", "momentary": "Pulsatiounsdauer (ms) (optional)", + "more_states": "Zous\u00e4tzlesch Zoust\u00e4nn fir d\u00ebs Zon konfigur\u00e9ieren", "name": "Numm (optional)", "pause": "Pausen zw\u00ebscht den Impulser (ms) (optional)", "repeat": "Unzuel vu Widderhuelungen (-1= onendlech) (optional)" diff --git a/homeassistant/components/konnected/.translations/no.json b/homeassistant/components/konnected/.translations/no.json index 71c0fa1de6ea6..86d9fe877af1c 100644 --- a/homeassistant/components/konnected/.translations/no.json +++ b/homeassistant/components/konnected/.translations/no.json @@ -33,6 +33,9 @@ "abort": { "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet" }, + "error": { + "bad_host": "Ugyldig overstyr API-vertsadresse" + }, "step": { "options_binary": { "data": { @@ -82,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "Blink p\u00e5 LED-lampen n\u00e5r du sender statusendring" + "api_host": "Overstyre API-vert-URL (valgfritt)", + "blink": "Blink p\u00e5 LED-lampen n\u00e5r du sender statusendring", + "override_api_host": "Overstyre standard Home Assistant API-vertspanel-URL" }, "description": "Vennligst velg \u00f8nsket atferd for din panel", "title": "Konfigurere Diverse" diff --git a/homeassistant/components/konnected/.translations/ru.json b/homeassistant/components/konnected/.translations/ru.json index ba1b3c6abc9e8..75a879832a455 100644 --- a/homeassistant/components/konnected/.translations/ru.json +++ b/homeassistant/components/konnected/.translations/ru.json @@ -91,6 +91,7 @@ "data": { "activation": "\u0412\u044b\u0445\u043e\u0434 \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "momentary": "\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "more_states": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0437\u043e\u043d\u044b", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0435\u0436\u0434\u0443 \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "repeat": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0439 (-1 = \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" diff --git a/homeassistant/components/konnected/.translations/zh-Hant.json b/homeassistant/components/konnected/.translations/zh-Hant.json index f3aa89fe8775e..9c3e818e692eb 100644 --- a/homeassistant/components/konnected/.translations/zh-Hant.json +++ b/homeassistant/components/konnected/.translations/zh-Hant.json @@ -33,6 +33,9 @@ "abort": { "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099" }, + "error": { + "bad_host": "\u7121\u6548\u7684\u8986\u5beb API \u4e3b\u6a5f\u7aef URL" + }, "step": { "options_binary": { "data": { @@ -82,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "\u7576\u50b3\u9001\u72c0\u614b\u8b8a\u66f4\u6642\u3001\u9583\u720d\u9762\u677f LED" + "api_host": "\u8986\u5beb API \u4e3b\u6a5f\u7aef URL\uff08\u9078\u9805\uff09", + "blink": "\u7576\u50b3\u9001\u72c0\u614b\u8b8a\u66f4\u6642\u3001\u9583\u720d\u9762\u677f LED", + "override_api_host": "\u8986\u5beb\u9810\u8a2d Home Assistant API \u4e3b\u6a5f\u7aef\u9762\u677f URL" }, "description": "\u8acb\u9078\u64c7\u9762\u677f\u671f\u671b\u884c\u70ba", "title": "\u5176\u4ed6\u8a2d\u5b9a" @@ -96,7 +101,7 @@ "pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09\uff08\u9078\u9805\uff09", "repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09\uff08\u9078\u9805\uff09" }, - "description": "\u8acb\u9078\u64c7 {zone}\u8f38\u51fa\u9078\u9805", + "description": "\u8acb\u9078\u64c7 {zone}\u8f38\u51fa\u9078\u9805\uff1a\u72c0\u614b {state}", "title": "\u8a2d\u5b9a Switchable \u8f38\u51fa" } }, diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index 50f897e3a85a1..f2f79f5ed7d6b 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -87,4 +87,4 @@ async def async_added_to_hass(self): def async_set_state(self, state): """Update the sensor's state.""" self._state = state - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index e936898d7fbba..74554f2afc2b4 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -137,4 +137,4 @@ def async_set_state(self, state): self._state = int(float(state)) else: self._state = round(float(state), 1) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index b8ddec20440ca..afc83458aaf38 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -117,7 +117,7 @@ def _boolean_state(self, int_state): def _set_state(self, state): self._state = state - self.async_schedule_update_ha_state() + self.async_write_ha_state() _LOGGER.debug( "Setting status of %s actuator zone %s to %s", self._device_id, diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index 1d38764710a44..f0ec885a8fe20 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -171,7 +171,7 @@ def value_is_expired(self, *_): """Triggered when value is expired.""" self._expiration_trigger = None self._value = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() class LaCrosseTemperature(LaCrosseSensor): diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index e0039f29e5842..249adf04af8f3 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -68,7 +68,7 @@ def input_received(self, input_obj): return self._value = input_obj.get_value().is_locked_regulator() - self.async_schedule_update_ha_state() + self.async_write_ha_state() class LcnBinarySensor(LcnDevice, BinarySensorDevice): @@ -100,7 +100,7 @@ def input_received(self, input_obj): return self._value = input_obj.get_state(self.bin_sensor_port.value) - self.async_schedule_update_ha_state() + self.async_write_ha_state() class LcnLockKeysSensor(LcnDevice, BinarySensorDevice): @@ -135,4 +135,4 @@ def input_received(self, input_obj): key_id = int(self.source.name[1]) - 1 self._value = input_obj.get_state(table_id, key_id) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index aec68af6076e0..12fff2f479b8a 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -125,7 +125,7 @@ async def async_set_hvac_mode(self, hvac_mode): self.address_connection.lock_regulator(self.regulator_id, True) self._target_temperature = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_temperature(self, **kwargs): """Set new target temperature.""" @@ -137,7 +137,7 @@ async def async_set_temperature(self, **kwargs): self.address_connection.var_abs( self.setpoint, self._target_temperature, self.unit ) - self.async_schedule_update_ha_state() + self.async_write_ha_state() def input_received(self, input_obj): """Set temperature value when LCN input object is received.""" @@ -151,4 +151,4 @@ def input_received(self, input_obj): if self._is_on: self._target_temperature = input_obj.get_value().to_var_unit(self.unit) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index c1bd7070f5380..ba831c3a1a92b 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -108,7 +108,7 @@ def input_received(self, input_obj): elif self.state_down and not self.state_up: self._closed = True # Cover closed - self.async_schedule_update_ha_state() + self.async_write_ha_state() class LcnRelayCover(LcnDevice, CoverDevice): @@ -167,4 +167,4 @@ def input_received(self, input_obj): if states[self.motor_port_onoff]: # motor is on self._closed = states[self.motor_port_updown] # set direction - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 4c67156c78738..40a592f89c92b 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -132,7 +132,7 @@ def input_received(self, input_obj): self._is_dimming_to_zero = False if not self._is_dimming_to_zero: self._is_on = self.brightness > 0 - self.async_schedule_update_ha_state() + self.async_write_ha_state() class LcnRelayLight(LcnDevice, Light): @@ -182,4 +182,4 @@ def input_received(self, input_obj): return self._is_on = input_obj.get_state(self.output.value) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index d4bb5566e34c4..ddf7e61a3f60f 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -78,7 +78,7 @@ def input_received(self, input_obj): return self._value = input_obj.get_value().to_var_unit(self.unit) - self.async_schedule_update_ha_state() + self.async_write_ha_state() class LcnLedLogicSensor(LcnDevice): @@ -115,4 +115,4 @@ def input_received(self, input_obj): elif self.source in pypck.lcn_defs.LogicOpPort: self._value = input_obj.get_logic_op_state(self.source.value).name.lower() - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index f19548c4aee23..b0e16e412b325 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -76,7 +76,7 @@ def input_received(self, input_obj): return self._is_on = input_obj.get_percent() > 0 - self.async_schedule_update_ha_state() + self.async_write_ha_state() class LcnRelaySwitch(LcnDevice, SwitchDevice): @@ -124,4 +124,4 @@ def input_received(self, input_obj): return self._is_on = input_obj.get_state(self.output.value) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/lightwave/light.py b/homeassistant/components/lightwave/light.py index 4aabcbc75c521..78e4c43a0e77d 100644 --- a/homeassistant/components/lightwave/light.py +++ b/homeassistant/components/lightwave/light.py @@ -72,10 +72,10 @@ async def async_turn_on(self, **kwargs): else: self._lwlink.turn_on_light(self._device_id, self._name) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the LightWave light off.""" self._state = False self._lwlink.turn_off(self._device_id, self._name) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/lightwave/switch.py b/homeassistant/components/lightwave/switch.py index bfc4407458a3a..16c2aa534627e 100644 --- a/homeassistant/components/lightwave/switch.py +++ b/homeassistant/components/lightwave/switch.py @@ -49,10 +49,10 @@ async def async_turn_on(self, **kwargs): """Turn the LightWave switch on.""" self._state = True self._lwlink.turn_on_switch(self._device_id, self._name) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the LightWave switch off.""" self._state = False self._lwlink.turn_off(self._device_id, self._name) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 28d20d0db4b58..8db9cb6fa37c2 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -176,7 +176,7 @@ async def async_notify_received(notify): self.set_state(stb_state) _LOGGER.debug("STB(%s) is [%s]", self.host, self._state) self._available = True - self.async_schedule_update_ha_state() + self.async_write_ha_state() async_dispatcher_connect(self.hass, SIGNAL_STB_NOTIFY, async_notify_received) @@ -200,7 +200,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): self._available = True except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def unique_id(self): @@ -242,7 +242,7 @@ async def async_turn_on(self): self._available = True except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self): """Turn off the receiver.""" @@ -254,7 +254,7 @@ async def async_turn_off(self): self._available = True except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_media_play(self): """Send play command.""" @@ -267,7 +267,7 @@ async def async_media_play(self): self._available = True except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_media_pause(self): """Send pause command.""" @@ -279,7 +279,7 @@ async def async_media_pause(self): self._available = True except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_media_stop(self): """Send stop command.""" @@ -291,7 +291,7 @@ async def async_media_stop(self): self._available = True except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_media_previous_track(self): """Send Program Down command.""" @@ -303,7 +303,7 @@ async def async_media_previous_track(self): self._available = True except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_media_next_track(self): """Send Program Up command.""" @@ -315,7 +315,7 @@ async def async_media_next_track(self): self._available = True except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_volume_up(self): """Send volume up command.""" @@ -325,7 +325,7 @@ async def async_volume_up(self): self._available = True except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_volume_down(self): """Send volume up command.""" @@ -334,7 +334,7 @@ async def async_volume_down(self): await self.stb.send_cmd("VolDown") except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_mute_volume(self, mute): """Send mute command.""" @@ -343,4 +343,4 @@ async def async_mute_volume(self, mute): await self.stb.send_cmd("Mute") except PyMediaroomError: self._available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 5200c6b0c124f..7a12f617740ec 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -103,4 +103,4 @@ def _handle_update(self, data): return self._config = data - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ee14fe432b5f0..bc59be0d1f3e8 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -45,7 +45,8 @@ from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow -from . import config_flow, discovery, server # noqa: F401 pylint: disable=unused-import +from . import config_flow # noqa: F401 pylint: disable=unused-import +from . import debug_info, discovery, server from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC, @@ -56,6 +57,7 @@ DEFAULT_QOS, PROTOCOL_311, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash from .models import Message, MessageCallbackType, PublishPayloadType from .subscription import async_subscribe_topics, async_unsubscribe_topics @@ -513,6 +515,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_remove_device) + websocket_api.async_register_command(hass, websocket_mqtt_info) if conf is None: # If we have a config entry, setup is done by that config entry. @@ -1058,6 +1061,7 @@ async def _attributes_subscribe_topics(self): attr_tpl.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def attributes_message_received(msg: Message) -> None: try: payload = msg.payload @@ -1122,6 +1126,7 @@ async def _availability_subscribe_topics(self): """(Re)Subscribe to topics.""" @callback + @log_messages(self.hass, self.entity_id) def availability_message_received(msg: Message) -> None: """Handle a new received MQTT availability message.""" if msg.payload == self._avail_config[CONF_PAYLOAD_AVAILABLE]: @@ -1207,6 +1212,7 @@ async def discovery_callback(payload): _LOGGER.info( "Got update for entity with hash: %s '%s'", discovery_hash, payload, ) + debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) if not payload: # Empty payload: Remove component _LOGGER.info("Removing component: %s", self.entity_id) @@ -1219,6 +1225,9 @@ async def discovery_callback(payload): await self._discovery_update(payload) if discovery_hash: + debug_info.add_entity_discovery_data( + self.hass, self._discovery_data, self.entity_id + ) # Set in case the entity has been removed and is re-added set_discovery_hash(self.hass, discovery_hash) self._remove_signal = async_dispatcher_connect( @@ -1242,6 +1251,7 @@ async def async_will_remove_from_hass(self) -> None: def _cleanup_on_remove(self) -> None: """Stop listening to signal and cleanup discovery data.""" if self._discovery_data and not self._removed_from_hass: + debug_info.remove_entity_data(self.hass, self.entity_id) clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) self._removed_from_hass = True @@ -1303,6 +1313,18 @@ def device_info(self): return device_info_from_config(self._device_config) +@websocket_api.websocket_command( + {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} +) +@websocket_api.async_response +async def websocket_mqtt_info(hass, connection, msg): + """Get MQTT debug info for device.""" + device_id = msg["device_id"] + mqtt_info = await debug_info.info_for_device(hass, device_id) + + connection.send_result(msg["id"], mqtt_info) + + @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/remove", vol.Required("device_id"): str} ) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 043fa62f6effe..09f735c72a06b 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -41,6 +41,7 @@ MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -167,6 +168,7 @@ async def _subscribe_topics(self): command_template.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def message_received(msg): """Run when new MQTT message has been received.""" payload = msg.payload diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index d268c12aa8772..c7595de0eebbd 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -37,6 +37,7 @@ MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -155,6 +156,7 @@ def off_delay_listener(now): self.async_write_ha_state() @callback + @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle a new received MQTT state message.""" payload = msg.payload diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index a75ae33f86197..1bfb248d94ad8 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -21,6 +21,7 @@ MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -116,6 +117,7 @@ async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @callback + @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" self._last_image = msg.payload diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 46404de0c8a77..0216302c6515e 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -60,6 +60,7 @@ MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -384,6 +385,7 @@ def render_template(msg, template_name): return template(msg.payload) @callback + @log_messages(self.hass, self.entity_id) def handle_action_received(msg): """Handle receiving action via MQTT.""" payload = render_template(msg, CONF_ACTION_TEMPLATE) @@ -405,6 +407,7 @@ def handle_temperature_received(msg, template_name, attr): _LOGGER.error("Could not parse temperature from %s", payload) @callback + @log_messages(self.hass, self.entity_id) def handle_current_temperature_received(msg): """Handle current temperature coming via MQTT.""" handle_temperature_received( @@ -416,6 +419,7 @@ def handle_current_temperature_received(msg): ) @callback + @log_messages(self.hass, self.entity_id) def handle_target_temperature_received(msg): """Handle target temperature coming via MQTT.""" handle_temperature_received(msg, CONF_TEMP_STATE_TEMPLATE, "_target_temp") @@ -425,6 +429,7 @@ def handle_target_temperature_received(msg): ) @callback + @log_messages(self.hass, self.entity_id) def handle_temperature_low_received(msg): """Handle target temperature low coming via MQTT.""" handle_temperature_received( @@ -436,6 +441,7 @@ def handle_temperature_low_received(msg): ) @callback + @log_messages(self.hass, self.entity_id) def handle_temperature_high_received(msg): """Handle target temperature high coming via MQTT.""" handle_temperature_received( @@ -458,6 +464,7 @@ def handle_mode_received(msg, template_name, attr, mode_list): self.async_write_ha_state() @callback + @log_messages(self.hass, self.entity_id) def handle_current_mode_received(msg): """Handle receiving mode via MQTT.""" handle_mode_received( @@ -467,6 +474,7 @@ def handle_current_mode_received(msg): add_subscription(topics, CONF_MODE_STATE_TOPIC, handle_current_mode_received) @callback + @log_messages(self.hass, self.entity_id) def handle_fan_mode_received(msg): """Handle receiving fan mode via MQTT.""" handle_mode_received( @@ -479,6 +487,7 @@ def handle_fan_mode_received(msg): add_subscription(topics, CONF_FAN_MODE_STATE_TOPIC, handle_fan_mode_received) @callback + @log_messages(self.hass, self.entity_id) def handle_swing_mode_received(msg): """Handle receiving swing mode via MQTT.""" handle_mode_received( @@ -514,6 +523,7 @@ def handle_onoff_mode_received(msg, template_name, attr): self.async_write_ha_state() @callback + @log_messages(self.hass, self.entity_id) def handle_away_mode_received(msg): """Handle receiving away mode via MQTT.""" handle_onoff_mode_received(msg, CONF_AWAY_MODE_STATE_TEMPLATE, "_away") @@ -521,6 +531,7 @@ def handle_away_mode_received(msg): add_subscription(topics, CONF_AWAY_MODE_STATE_TOPIC, handle_away_mode_received) @callback + @log_messages(self.hass, self.entity_id) def handle_aux_mode_received(msg): """Handle receiving aux mode via MQTT.""" handle_onoff_mode_received(msg, CONF_AUX_STATE_TEMPLATE, "_aux") @@ -528,6 +539,7 @@ def handle_aux_mode_received(msg): add_subscription(topics, CONF_AUX_STATE_TOPIC, handle_aux_mode_received) @callback + @log_messages(self.hass, self.entity_id) def handle_hold_mode_received(msg): """Handle receiving hold mode via MQTT.""" payload = render_template(msg, CONF_HOLD_STATE_TEMPLATE) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6044ec2af6e5f..5d1fe2e2505ee 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -4,6 +4,7 @@ DEFAULT_DISCOVERY = False ATTR_DISCOVERY_HASH = "discovery_hash" +ATTR_DISCOVERY_PAYLOAD = "discovery_payload" ATTR_DISCOVERY_TOPIC = "discovery_topic" CONF_STATE_TOPIC = "state_topic" PROTOCOL_311 = "3.1.1" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index a7a396781928e..3081c1d0d9ff1 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -49,6 +49,7 @@ MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -268,7 +269,8 @@ async def _subscribe_topics(self): topics = {} @callback - def tilt_updated(msg): + @log_messages(self.hass, self.entity_id) + def tilt_message_received(msg): """Handle tilt updates.""" payload = msg.payload if tilt_status_template is not None: @@ -287,6 +289,7 @@ def tilt_updated(msg): self.async_write_ha_state() @callback + @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload @@ -311,6 +314,7 @@ def state_message_received(msg): self.async_write_ha_state() @callback + @log_messages(self.hass, self.entity_id) def position_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload @@ -354,7 +358,7 @@ def position_message_received(msg): self._tilt_value = STATE_UNKNOWN topics["tilt_status_topic"] = { "topic": self._config.get(CONF_TILT_STATUS_TOPIC), - "msg_callback": tilt_updated, + "msg_callback": tilt_message_received, "qos": self._config[CONF_QOS], } diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py new file mode 100644 index 0000000000000..ec4ff1676bb8e --- /dev/null +++ b/homeassistant/components/mqtt/debug_info.py @@ -0,0 +1,146 @@ +"""Helper to handle a set of topics to subscribe to.""" +from collections import deque +from functools import wraps +import logging +from typing import Any + +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC +from .models import MessageCallbackType + +_LOGGER = logging.getLogger(__name__) + +DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" +STORED_MESSAGES = 10 + + +def log_messages(hass: HomeAssistantType, entity_id: str) -> MessageCallbackType: + """Wrap an MQTT message callback to support message logging.""" + + def _log_message(msg): + """Log message.""" + debug_info = hass.data[DATA_MQTT_DEBUG_INFO] + messages = debug_info["entities"][entity_id]["topics"][msg.topic] + messages.append(msg.payload) + + def _decorator(msg_callback: MessageCallbackType): + @wraps(msg_callback) + def wrapper(msg: Any) -> None: + """Log message.""" + _log_message(msg) + msg_callback(msg) + + setattr(wrapper, "__entity_id", entity_id) + return wrapper + + return _decorator + + +def add_topic(hass, message_callback, topic): + """Prepare debug data for topic.""" + entity_id = getattr(message_callback, "__entity_id", None) + if entity_id: + debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + entity_info = debug_info["entities"].setdefault( + entity_id, {"topics": {}, "discovery_data": {}} + ) + entity_info["topics"][topic] = deque([], STORED_MESSAGES) + + +def remove_topic(hass, message_callback, topic): + """Remove debug data for topic.""" + entity_id = getattr(message_callback, "__entity_id", None) + if entity_id and entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]: + hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["topics"].pop(topic) + + +def add_entity_discovery_data(hass, discovery_data, entity_id): + """Add discovery data.""" + debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + entity_info = debug_info["entities"].setdefault( + entity_id, {"topics": {}, "discovery_data": {}} + ) + entity_info["discovery_data"] = discovery_data + + +def update_entity_discovery_data(hass, discovery_payload, entity_id): + """Update discovery data.""" + entity_info = hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id] + entity_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + + +def remove_entity_data(hass, entity_id): + """Remove discovery data.""" + hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id) + + +def add_trigger_discovery_data(hass, discovery_hash, discovery_data, device_id): + """Add discovery data.""" + debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + debug_info["triggers"][discovery_hash] = { + "device_id": device_id, + "discovery_data": discovery_data, + } + + +def update_trigger_discovery_data(hass, discovery_hash, discovery_payload): + """Update discovery data.""" + trigger_info = hass.data[DATA_MQTT_DEBUG_INFO]["triggers"][discovery_hash] + trigger_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + + +def remove_trigger_discovery_data(hass, discovery_hash): + """Remove discovery data.""" + hass.data[DATA_MQTT_DEBUG_INFO]["triggers"][discovery_hash]["discovery_data"] = None + + +async def info_for_device(hass, device_id): + """Get debug info for a device.""" + mqtt_info = {"entities": [], "triggers": []} + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = hass.helpers.entity_registry.async_entries_for_device( + entity_registry, device_id + ) + mqtt_debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + for entry in entries: + if entry.entity_id not in mqtt_debug_info["entities"]: + continue + + entity_info = mqtt_debug_info["entities"][entry.entity_id] + topics = [ + {"topic": topic, "messages": list(messages)} + for topic, messages in entity_info["topics"].items() + ] + discovery_data = { + "topic": entity_info["discovery_data"].get(ATTR_DISCOVERY_TOPIC, ""), + "payload": entity_info["discovery_data"].get(ATTR_DISCOVERY_PAYLOAD, ""), + } + mqtt_info["entities"].append( + { + "entity_id": entry.entity_id, + "topics": topics, + "discovery_data": discovery_data, + } + ) + + for trigger in mqtt_debug_info["triggers"].values(): + if trigger["device_id"] != device_id: + continue + + discovery_data = { + "topic": trigger["discovery_data"][ATTR_DISCOVERY_TOPIC], + "payload": trigger["discovery_data"][ATTR_DISCOVERY_PAYLOAD], + } + mqtt_info["triggers"].append({"discovery_data": discovery_data}) + + return mqtt_info diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 88c635ae3a8ea..3b65243d0786e 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -26,6 +26,7 @@ CONF_QOS, DOMAIN, cleanup_device_registry, + debug_info, ) from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash @@ -183,6 +184,7 @@ async def discovery_update(payload): if not payload: # Empty payload: Remove trigger _LOGGER.info("Removing trigger: %s", discovery_hash) + debug_info.remove_trigger_discovery_data(hass, discovery_hash) if discovery_id in hass.data[DEVICE_TRIGGERS]: device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] device_trigger.detach_trigger() @@ -192,6 +194,7 @@ async def discovery_update(payload): else: # Non-empty payload: Update trigger _LOGGER.info("Updating trigger: %s", discovery_hash) + debug_info.update_trigger_discovery_data(hass, discovery_hash, payload) config = TRIGGER_DISCOVERY_SCHEMA(payload) await _update_device(hass, config_entry, config) device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] @@ -230,6 +233,9 @@ async def discovery_update(payload): await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger( config, discovery_hash, remove_signal ) + debug_info.add_trigger_discovery_data( + hass, discovery_hash, discovery_data, device.id + ) async def async_device_removed(hass: HomeAssistant, device_id: str): @@ -241,6 +247,7 @@ async def async_device_removed(hass: HomeAssistant, device_id: str): discovery_hash = device_trigger.discovery_data[ATTR_DISCOVERY_HASH] discovery_topic = device_trigger.discovery_data[ATTR_DISCOVERY_TOPIC] + debug_info.remove_trigger_discovery_data(hass, discovery_hash) device_trigger.detach_trigger() clear_discovery_hash(hass, discovery_hash) device_trigger.remove_signal() diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3bcd8594ebe1e..689b279c5e777 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -11,7 +11,7 @@ from homeassistant.helpers.typing import HomeAssistantType from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS -from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC _LOGGER = logging.getLogger(__name__) @@ -135,6 +135,7 @@ async def async_device_message_received(msg): setattr(payload, "__configuration_source__", f"MQTT (topic: '{topic}')") discovery_data = { ATTR_DISCOVERY_HASH: discovery_hash, + ATTR_DISCOVERY_PAYLOAD: payload, ATTR_DISCOVERY_TOPIC: topic, } setattr(payload, "discovery_data", discovery_data) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index b50bdf9734bb0..ec7c729d5976d 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -40,6 +40,7 @@ MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -249,6 +250,7 @@ async def _subscribe_topics(self): templates[key] = tpl.async_render_with_possible_json_value @callback + @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new received MQTT message.""" payload = templates[CONF_STATE](msg.payload) @@ -266,6 +268,7 @@ def state_received(msg): } @callback + @log_messages(self.hass, self.entity_id) def speed_received(msg): """Handle new received MQTT message for the speed.""" payload = templates[ATTR_SPEED](msg.payload) @@ -288,6 +291,7 @@ def speed_received(msg): self._speed = SPEED_OFF @callback + @log_messages(self.hass, self.entity_id) def oscillation_received(msg): """Handle new received MQTT message for the oscillation.""" payload = templates[OSCILLATION](msg.payload) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 4b47014af486a..e6a7f827f1e2c 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -51,6 +51,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util +from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -292,6 +293,7 @@ async def _subscribe_topics(self): last_state = await self.async_get_last_state() @callback + @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new MQTT messages.""" payload = templates[CONF_STATE](msg.payload) @@ -315,6 +317,7 @@ def state_received(msg): self._state = last_state.state == STATE_ON @callback + @log_messages(self.hass, self.entity_id) def brightness_received(msg): """Handle new MQTT messages for the brightness.""" payload = templates[CONF_BRIGHTNESS](msg.payload) @@ -346,6 +349,7 @@ def brightness_received(msg): self._brightness = None @callback + @log_messages(self.hass, self.entity_id) def rgb_received(msg): """Handle new MQTT messages for RGB.""" payload = templates[CONF_RGB](msg.payload) @@ -377,6 +381,7 @@ def rgb_received(msg): self._hs = (0, 0) @callback + @log_messages(self.hass, self.entity_id) def color_temp_received(msg): """Handle new MQTT messages for color temperature.""" payload = templates[CONF_COLOR_TEMP](msg.payload) @@ -406,6 +411,7 @@ def color_temp_received(msg): self._color_temp = None @callback + @log_messages(self.hass, self.entity_id) def effect_received(msg): """Handle new MQTT messages for effect.""" payload = templates[CONF_EFFECT](msg.payload) @@ -435,6 +441,7 @@ def effect_received(msg): self._effect = None @callback + @log_messages(self.hass, self.entity_id) def hs_received(msg): """Handle new MQTT messages for hs color.""" payload = templates[CONF_HS](msg.payload) @@ -466,6 +473,7 @@ def hs_received(msg): self._hs = (0, 0) @callback + @log_messages(self.hass, self.entity_id) def white_value_received(msg): """Handle new MQTT messages for white value.""" payload = templates[CONF_WHITE_VALUE](msg.payload) @@ -497,6 +505,7 @@ def white_value_received(msg): self._white_value = None @callback + @log_messages(self.hass, self.entity_id) def xy_received(msg): """Handle new MQTT messages for xy color.""" payload = templates[CONF_XY](msg.payload) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 0af5aaf2c76ed..924559e3e9048 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -54,6 +54,7 @@ from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util +from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE @@ -234,6 +235,7 @@ async def _subscribe_topics(self): last_state = await self.async_get_last_state() @callback + @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new MQTT messages.""" values = json.loads(msg.payload) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index cd3e704f624a2..25de6862bdb65 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -45,6 +45,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util +from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -215,6 +216,7 @@ async def _subscribe_topics(self): last_state = await self.async_get_last_state() @callback + @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new MQTT messages.""" state = self._templates[ diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 89f005b74694e..378f1b8fbcb6d 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -29,6 +29,7 @@ MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -156,6 +157,7 @@ async def _subscribe_topics(self): value_template.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" payload = msg.payload diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 7be923927ca8f..2704c5ae3a1f7 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -35,6 +35,7 @@ MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -137,6 +138,7 @@ async def _subscribe_topics(self): template.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" payload = msg.payload diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index be48a769a234b..b4793a49dca1f 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -8,6 +8,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass +from . import debug_info from .const import DEFAULT_QOS from .models import MessageCallbackType @@ -18,6 +19,7 @@ class EntitySubscription: """Class to hold data about an active entity topic subscription.""" + hass = attr.ib(type=HomeAssistantType) topic = attr.ib(type=str) message_callback = attr.ib(type=MessageCallbackType) unsubscribe_callback = attr.ib(type=Optional[Callable[[], None]]) @@ -31,11 +33,16 @@ async def resubscribe_if_necessary(self, hass, other): if other is not None and other.unsubscribe_callback is not None: other.unsubscribe_callback() + # Clear debug data if it exists + debug_info.remove_topic(self.hass, other.message_callback, other.topic) if self.topic is None: # We were asked to remove the subscription or not to create it return + # Prepare debug data + debug_info.add_topic(self.hass, self.message_callback, self.topic) + self.unsubscribe_callback = await mqtt.async_subscribe( hass, self.topic, self.message_callback, self.qos, self.encoding ) @@ -77,6 +84,7 @@ async def async_subscribe_topics( unsubscribe_callback=None, qos=value.get("qos", DEFAULT_QOS), encoding=value.get("encoding", "utf-8"), + hass=hass, ) # Get the current subscription state current = current_subscriptions.pop(key, None) @@ -87,6 +95,8 @@ async def async_subscribe_topics( for remaining in current_subscriptions.values(): if remaining.unsubscribe_callback is not None: remaining.unsubscribe_callback() + # Clear debug data if it exists + debug_info.remove_topic(hass, remaining.message_callback, remaining.topic) return new_state diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 32066c67b7aca..bc1f4a038b42b 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -34,6 +34,7 @@ MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -162,6 +163,7 @@ async def _subscribe_topics(self): template.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 7679b97d62e4b..687fefa94e215 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -32,6 +32,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level +from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) @@ -280,6 +281,7 @@ async def _subscribe_topics(self): tpl.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT message.""" if ( diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 126a2432cb09c..cbaf8d43a77e3 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -45,6 +45,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) @@ -246,6 +247,7 @@ async def _subscribe_topics(self): topics = {} @callback + @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle state MQTT message.""" payload = msg.payload diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 580ffd606f377..0d07133b39653 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -92,7 +92,7 @@ def update_state(device_id, room, distance): self._distance = distance self._updated = dt.utcnow() - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def message_received(msg): diff --git a/homeassistant/components/mychevy/binary_sensor.py b/homeassistant/components/mychevy/binary_sensor.py index e5b0dc8b6eaaa..822a7988d0d25 100644 --- a/homeassistant/components/mychevy/binary_sensor.py +++ b/homeassistant/components/mychevy/binary_sensor.py @@ -73,7 +73,7 @@ def async_update_callback(self): """Update state.""" if self._car is not None: self._is_on = getattr(self._car, self._attr, None) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def should_poll(self): diff --git a/homeassistant/components/mychevy/sensor.py b/homeassistant/components/mychevy/sensor.py index f45c81a00072e..9f8ea5607d9dc 100644 --- a/homeassistant/components/mychevy/sensor.py +++ b/homeassistant/components/mychevy/sensor.py @@ -70,7 +70,7 @@ def success(self): if self._state != MYCHEVY_SUCCESS: _LOGGER.debug("Successfully connected to mychevy website") self._state = MYCHEVY_SUCCESS - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def error(self): @@ -80,7 +80,7 @@ def error(self): "This probably means the mychevy to OnStar link is down" ) self._state = MYCHEVY_ERROR - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def icon(self): @@ -156,7 +156,7 @@ def async_update_callback(self): self._state = getattr(self._car, self._attr, None) for attr in self._extra_attrs: self._state_attributes[attr] = getattr(self._car, attr) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def state(self): diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 4939c0c83e566..b00a0d0d9d54d 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -162,7 +162,7 @@ async def async_set_temperature(self, **kwargs): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[value_type] = value - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" @@ -173,7 +173,7 @@ async def async_set_fan_mode(self, fan_mode): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[set_req.V_HVAC_SPEED] = fan_mode - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_hvac_mode(self, hvac_mode): """Set new target temperature.""" @@ -187,7 +187,7 @@ async def async_set_hvac_mode(self, hvac_mode): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[self.value_type] = hvac_mode - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 6c02e430ba460..b60cf9457a95e 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -52,7 +52,7 @@ async def async_open_cover(self, **kwargs): self._values[set_req.V_DIMMER] = 100 else: self._values[set_req.V_LIGHT] = STATE_ON - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_close_cover(self, **kwargs): """Move the cover down.""" @@ -66,7 +66,7 @@ async def async_close_cover(self, **kwargs): self._values[set_req.V_DIMMER] = 0 else: self._values[set_req.V_LIGHT] = STATE_OFF - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" @@ -78,7 +78,7 @@ async def async_set_cover_position(self, **kwargs): if self.gateway.optimistic: # Optimistically assume that cover has changed state. self._values[set_req.V_DIMMER] = position - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_stop_cover(self, **kwargs): """Stop the device.""" diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index b25cf977d83bf..1585de4b46265 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -149,7 +149,7 @@ async def async_turn_off(self, **kwargs): # optimistically assume that light has changed state self._state = False self._values[value_type] = STATE_OFF - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def _async_update_light(self): @@ -189,7 +189,7 @@ async def async_turn_on(self, **kwargs): self._turn_on_light() self._turn_on_dimmer(**kwargs) if self.gateway.optimistic: - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" @@ -215,7 +215,7 @@ async def async_turn_on(self, **kwargs): self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w("%02x%02x%02x", **kwargs) if self.gateway.optimistic: - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" @@ -242,4 +242,4 @@ async def async_turn_on(self, **kwargs): self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w("%02x%02x%02x%02x", **kwargs) if self.gateway.optimistic: - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index ec28649d70f60..16bb1ee6deb0b 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -99,7 +99,7 @@ async def async_turn_on(self, **kwargs): if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the switch off.""" @@ -109,7 +109,7 @@ async def async_turn_off(self, **kwargs): if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF - self.async_schedule_update_ha_state() + self.async_write_ha_state() class MySensorsIRSwitch(MySensorsSwitch): @@ -141,7 +141,7 @@ async def async_turn_on(self, **kwargs): # Optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code self._values[set_req.V_LIGHT] = STATE_ON - self.async_schedule_update_ha_state() + self.async_write_ha_state() # Turn off switch after switch was turned on await self.async_turn_off() @@ -154,7 +154,7 @@ async def async_turn_off(self, **kwargs): if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index 3da77d6d943b2..c584440874a27 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -86,4 +86,4 @@ def is_on(self): def async_on_update(self, value): """Receive an update.""" self._state = value - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index f77244a584ed4..8b7867fdc062d 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -111,4 +111,4 @@ def _handle_arming_state_change(self, arming_state): else: _LOGGER.warning("Unhandled arming state: %s", arming_state) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/ness_alarm/binary_sensor.py b/homeassistant/components/ness_alarm/binary_sensor.py index 0f15b6e937b17..69acc97130d2e 100644 --- a/homeassistant/components/ness_alarm/binary_sensor.py +++ b/homeassistant/components/ness_alarm/binary_sensor.py @@ -79,4 +79,4 @@ def _handle_zone_change(self, data: ZoneChangedData): """Handle zone state update.""" if self._zone_id == data.zone_id: self._state = data.state - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/nut/.translations/ca.json b/homeassistant/components/nut/.translations/ca.json new file mode 100644 index 0000000000000..33d2268be5b53 --- /dev/null +++ b/homeassistant/components/nut/.translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "alias": "\u00c0lies", + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "resources": "Recursos", + "username": "Nom d'usuari" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Recursos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/da.json b/homeassistant/components/nut/.translations/da.json new file mode 100644 index 0000000000000..3e66091d85109 --- /dev/null +++ b/homeassistant/components/nut/.translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/de.json b/homeassistant/components/nut/.translations/de.json new file mode 100644 index 0000000000000..611db3acfd65c --- /dev/null +++ b/homeassistant/components/nut/.translations/de.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "resources": "Ressourcen", + "username": "Benutzername" + }, + "title": "Stellen Sie eine Verbindung zum NUT-Server her" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Ressourcen" + }, + "description": "W\u00e4hlen Sie Sensorressourcen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/en.json b/homeassistant/components/nut/.translations/en.json new file mode 100644 index 0000000000000..66ea276eca0c5 --- /dev/null +++ b/homeassistant/components/nut/.translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "resources": "Resources", + "username": "Username" + }, + "description": "If there are multiple UPSs attached to the NUT server, enter the name UPS to query in the 'Alias' field.", + "title": "Connect to the NUT server" + } + }, + "title": "Network UPS Tools (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Resources" + }, + "description": "Choose Sensor Resources" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/es.json b/homeassistant/components/nut/.translations/es.json new file mode 100644 index 0000000000000..34944816c818f --- /dev/null +++ b/homeassistant/components/nut/.translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "resources": "Recursos", + "username": "Usuario" + }, + "description": "Si hay varios UPS conectados al servidor NUT, introduzca el nombre UPS a buscar en el campo 'Alias'.", + "title": "Conectar con el servidor NUT" + } + }, + "title": "Herramientas de UPS de red (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Recursos" + }, + "description": "Elegir Recursos del Sensor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/lb.json b/homeassistant/components/nut/.translations/lb.json new file mode 100644 index 0000000000000..416d5c49aee9d --- /dev/null +++ b/homeassistant/components/nut/.translations/lb.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Apparat", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "resources": "Ressourcen", + "username": "Benotzernumm" + }, + "title": "Mam NUT Server verbannen" + } + }, + "title": "Network UPS Tools (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Ressourcen" + }, + "description": "Sensor Ressourcen auswielen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/no.json b/homeassistant/components/nut/.translations/no.json new file mode 100644 index 0000000000000..31fc3e513c121 --- /dev/null +++ b/homeassistant/components/nut/.translations/no.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "resources": "Ressurser", + "username": "Brukernavn" + }, + "description": "Hvis det er flere UPS-er knyttet til NUT-serveren, angir du navnet UPS for \u00e5 sp\u00f8rre i 'Alias' -feltet.", + "title": "Koble til NUT-serveren" + } + }, + "title": "Nettverk UPS-verkt\u00f8y (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Ressurser" + }, + "description": "Velg Sensorressurser" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/zh-Hant.json b/homeassistant/components/nut/.translations/zh-Hant.json new file mode 100644 index 0000000000000..760a66ba1a5bf --- /dev/null +++ b/homeassistant/components/nut/.translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "alias": "\u5225\u540d", + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "resources": "\u8cc7\u6e90", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u5047\u5982 NUT \u4f3a\u670d\u5668\u4e0b\u64c1\u6709\u591a\u7d44 UPS\uff0c\u65bc\u300c\u5225\u540d\u300d\u6b04\u4f4d\u8f38\u5165 UPS \u540d\u7a31\u3002", + "title": "\u9023\u7dda\u81f3 NUT \u4f3a\u670d\u5668" + } + }, + "title": "Network UPS Tools (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "\u8cc7\u6e90" + }, + "description": "\u9078\u64c7\u50b3\u611f\u5668\u8cc7\u6e90" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index e51145c8eaaed..a990cdf94b82b 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -1 +1,210 @@ """The nut component.""" +import asyncio +import logging + +from pynut2.nut2 import PyNUTClient, PyNUTError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ALIAS, + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_RESOURCES, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import ( + DOMAIN, + PLATFORMS, + PYNUT_DATA, + PYNUT_FIRMWARE, + PYNUT_MANUFACTURER, + PYNUT_MODEL, + PYNUT_STATUS, + PYNUT_UNIQUE_ID, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Network UPS Tools (NUT) component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Network UPS Tools (NUT) from a config entry.""" + + config = entry.data + host = config[CONF_HOST] + port = config[CONF_PORT] + + alias = config.get(CONF_ALIAS) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + data = PyNUTData(host, port, alias, username, password) + + status = await hass.async_add_executor_job(pynutdata_status, data) + + if not status: + _LOGGER.error("NUT Sensor has no data, unable to set up") + raise ConfigEntryNotReady + + _LOGGER.debug("NUT Sensors Available: %s", status) + + hass.data[DOMAIN][entry.entry_id] = { + PYNUT_DATA: data, + PYNUT_STATUS: status, + PYNUT_UNIQUE_ID: _unique_id_from_status(status), + PYNUT_MANUFACTURER: _manufacturer_from_status(status), + PYNUT_MODEL: _model_from_status(status), + PYNUT_FIRMWARE: _firmware_from_status(status), + } + + entry.add_update_listener(_async_update_listener) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +def _manufacturer_from_status(status): + """Find the best manufacturer value from the status.""" + return ( + status.get("device.mfr") + or status.get("ups.mfr") + or status.get("ups.vendorid") + or status.get("driver.version.data") + ) + + +def _model_from_status(status): + """Find the best model value from the status.""" + return ( + status.get("device.model") + or status.get("ups.model") + or status.get("ups.productid") + ) + + +def _firmware_from_status(status): + """Find the best firmware value from the status.""" + return status.get("ups.firmware") or status.get("ups.firmware.aux") + + +def _serial_from_status(status): + """Find the best serialvalue from the status.""" + serial = status.get("device.serial") or status.get("ups.serial") + if serial and serial == "unknown": + return None + return serial + + +def _unique_id_from_status(status): + """Find the best unique id value from the status.""" + serial = _serial_from_status(status) + # We must have a serial for this to be unique + if not serial: + return None + + manufacturer = _manufacturer_from_status(status) + model = _model_from_status(status) + + unique_id_group = [] + if manufacturer: + unique_id_group.append(manufacturer) + if model: + unique_id_group.append(model) + if serial: + unique_id_group.append(serial) + return "_".join(unique_id_group) + + +def find_resources_in_config_entry(config_entry): + """Find the configured resources in the config entry.""" + if CONF_RESOURCES in config_entry.options: + return config_entry.options[CONF_RESOURCES] + return config_entry.data[CONF_RESOURCES] + + +def pynutdata_status(data): + """Wrap for data update as a callable.""" + return data.status + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class PyNUTData: + """Stores the data retrieved from NUT. + + For each entity to use, acts as the single point responsible for fetching + updates from the server. + """ + + def __init__(self, host, port, alias, username, password): + """Initialize the data object.""" + + self._host = host + self._alias = alias + + # Establish client with persistent=False to open/close connection on + # each update call. This is more reliable with async. + self._client = PyNUTClient(self._host, port, username, password, 5, False) + self._status = None + + @property + def status(self): + """Get latest update if throttle allows. Return status.""" + self.update() + return self._status + + def _get_alias(self): + """Get the ups alias from NUT.""" + try: + return next(iter(self._client.list_ups())) + except PyNUTError as err: + _LOGGER.error("Failure getting NUT ups alias, %s", err) + return None + + def _get_status(self): + """Get the ups status from NUT.""" + if self._alias is None: + self._alias = self._get_alias() + + try: + return self._client.list_vars(self._alias) + except (PyNUTError, ConnectionResetError) as err: + _LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err) + return None + + def update(self, **kwargs): + """Fetch the latest status from NUT.""" + self._status = self._get_status() diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py new file mode 100644 index 0000000000000..04889bb3f3fc8 --- /dev/null +++ b/homeassistant/components/nut/config_flow.py @@ -0,0 +1,143 @@ +"""Config flow for Network UPS Tools (NUT) integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_ALIAS, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_RESOURCES, + CONF_USERNAME, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from . import PyNUTData, find_resources_in_config_entry, pynutdata_status +from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, SENSOR_TYPES +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +SENSOR_DICT = {sensor_id: SENSOR_TYPES[sensor_id][0] for sensor_id in SENSOR_TYPES} + +DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_RESOURCES): cv.multi_select(SENSOR_DICT), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_ALIAS): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + host = data[CONF_HOST] + port = data[CONF_PORT] + alias = data.get(CONF_ALIAS) + username = data.get(CONF_USERNAME) + password = data.get(CONF_PASSWORD) + + data = PyNUTData(host, port, alias, username, password) + + status = await hass.async_add_executor_job(pynutdata_status, data) + + if not status: + raise CannotConnect + + return {"title": _format_host_port_alias(host, port, alias)} + + +def _format_host_port_alias(host, port, alias): + """Format a host, port, and alias so it can be used for comparison or display.""" + if alias: + return f"{alias}@{host}:{port}" + return f"{host}:{port}" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Network UPS Tools (NUT).""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if self._host_port_alias_already_configured( + user_input[CONF_HOST], user_input[CONF_PORT], user_input.get(CONF_ALIAS) + ): + return self.async_abort(reason="already_configured") + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if "base" not in errors: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + def _host_port_alias_already_configured(self, host, port, alias): + """See if we already have a nut entry matching user input configured.""" + existing_host_port_aliases = { + _format_host_port_alias(host, port, alias) + for entry in self._async_current_entries() + } + return _format_host_port_alias(host, port, alias) in existing_host_port_aliases + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for nut.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + resources = find_resources_in_config_entry(self.config_entry) + + data_schema = vol.Schema( + { + vol.Required(CONF_RESOURCES, default=resources): cv.multi_select( + SENSOR_DICT + ), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py new file mode 100644 index 0000000000000..ea164e70b937f --- /dev/null +++ b/homeassistant/components/nut/const.py @@ -0,0 +1,125 @@ +"""The nut component.""" +from homeassistant.const import POWER_WATT, TEMP_CELSIUS, TIME_SECONDS, UNIT_PERCENTAGE + +DOMAIN = "nut" + +PLATFORMS = ["sensor"] + + +DEFAULT_NAME = "NUT UPS" +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 3493 + +KEY_STATUS = "ups.status" +KEY_STATUS_DISPLAY = "ups.status.display" + +PYNUT_DATA = "data" +PYNUT_STATUS = "status" +PYNUT_UNIQUE_ID = "unique_id" +PYNUT_MANUFACTURER = "manufacturer" +PYNUT_MODEL = "model" +PYNUT_FIRMWARE = "firmware" + +SENSOR_TYPES = { + "ups.status.display": ["Status", "", "mdi:information-outline"], + "ups.status": ["Status Data", "", "mdi:information-outline"], + "ups.alarm": ["Alarms", "", "mdi:alarm"], + "ups.temperature": ["UPS Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "ups.load": ["Load", UNIT_PERCENTAGE, "mdi:gauge"], + "ups.load.high": ["Overload Setting", UNIT_PERCENTAGE, "mdi:gauge"], + "ups.id": ["System identifier", "", "mdi:information-outline"], + "ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer"], + "ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer"], + "ups.delay.shutdown": ["UPS Shutdown Delay", TIME_SECONDS, "mdi:timer"], + "ups.timer.start": ["Load Start Timer", TIME_SECONDS, "mdi:timer"], + "ups.timer.reboot": ["Load Reboot Timer", TIME_SECONDS, "mdi:timer"], + "ups.timer.shutdown": ["Load Shutdown Timer", TIME_SECONDS, "mdi:timer"], + "ups.test.interval": ["Self-Test Interval", TIME_SECONDS, "mdi:timer"], + "ups.test.result": ["Self-Test Result", "", "mdi:information-outline"], + "ups.test.date": ["Self-Test Date", "", "mdi:calendar"], + "ups.display.language": ["Language", "", "mdi:information-outline"], + "ups.contacts": ["External Contacts", "", "mdi:information-outline"], + "ups.efficiency": ["Efficiency", UNIT_PERCENTAGE, "mdi:gauge"], + "ups.power": ["Current Apparent Power", "VA", "mdi:flash"], + "ups.power.nominal": ["Nominal Power", "VA", "mdi:flash"], + "ups.realpower": ["Current Real Power", POWER_WATT, "mdi:flash"], + "ups.realpower.nominal": ["Nominal Real Power", POWER_WATT, "mdi:flash"], + "ups.beeper.status": ["Beeper Status", "", "mdi:information-outline"], + "ups.type": ["UPS Type", "", "mdi:information-outline"], + "ups.watchdog.status": ["Watchdog Status", "", "mdi:information-outline"], + "ups.start.auto": ["Start on AC", "", "mdi:information-outline"], + "ups.start.battery": ["Start on Battery", "", "mdi:information-outline"], + "ups.start.reboot": ["Reboot on Battery", "", "mdi:information-outline"], + "ups.shutdown": ["Shutdown Ability", "", "mdi:information-outline"], + "battery.charge": ["Battery Charge", UNIT_PERCENTAGE, "mdi:gauge"], + "battery.charge.low": ["Low Battery Setpoint", UNIT_PERCENTAGE, "mdi:gauge"], + "battery.charge.restart": [ + "Minimum Battery to Start", + UNIT_PERCENTAGE, + "mdi:gauge", + ], + "battery.charge.warning": [ + "Warning Battery Setpoint", + UNIT_PERCENTAGE, + "mdi:gauge", + ], + "battery.charger.status": ["Charging Status", "", "mdi:information-outline"], + "battery.voltage": ["Battery Voltage", "V", "mdi:flash"], + "battery.voltage.nominal": ["Nominal Battery Voltage", "V", "mdi:flash"], + "battery.voltage.low": ["Low Battery Voltage", "V", "mdi:flash"], + "battery.voltage.high": ["High Battery Voltage", "V", "mdi:flash"], + "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash"], + "battery.current": ["Battery Current", "A", "mdi:flash"], + "battery.current.total": ["Total Battery Current", "A", "mdi:flash"], + "battery.temperature": ["Battery Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer"], + "battery.runtime.low": ["Low Battery Runtime", TIME_SECONDS, "mdi:timer"], + "battery.runtime.restart": [ + "Minimum Battery Runtime to Start", + TIME_SECONDS, + "mdi:timer", + ], + "battery.alarm.threshold": [ + "Battery Alarm Threshold", + "", + "mdi:information-outline", + ], + "battery.date": ["Battery Date", "", "mdi:calendar"], + "battery.mfr.date": ["Battery Manuf. Date", "", "mdi:calendar"], + "battery.packs": ["Number of Batteries", "", "mdi:information-outline"], + "battery.packs.bad": ["Number of Bad Batteries", "", "mdi:information-outline"], + "battery.type": ["Battery Chemistry", "", "mdi:information-outline"], + "input.sensitivity": ["Input Power Sensitivity", "", "mdi:information-outline"], + "input.transfer.low": ["Low Voltage Transfer", "V", "mdi:flash"], + "input.transfer.high": ["High Voltage Transfer", "V", "mdi:flash"], + "input.transfer.reason": ["Voltage Transfer Reason", "", "mdi:information-outline"], + "input.voltage": ["Input Voltage", "V", "mdi:flash"], + "input.voltage.nominal": ["Nominal Input Voltage", "V", "mdi:flash"], + "input.frequency": ["Input Line Frequency", "hz", "mdi:flash"], + "input.frequency.nominal": ["Nominal Input Line Frequency", "hz", "mdi:flash"], + "input.frequency.status": ["Input Frequency Status", "", "mdi:information-outline"], + "output.current": ["Output Current", "A", "mdi:flash"], + "output.current.nominal": ["Nominal Output Current", "A", "mdi:flash"], + "output.voltage": ["Output Voltage", "V", "mdi:flash"], + "output.voltage.nominal": ["Nominal Output Voltage", "V", "mdi:flash"], + "output.frequency": ["Output Frequency", "hz", "mdi:flash"], + "output.frequency.nominal": ["Nominal Output Frequency", "hz", "mdi:flash"], +} + +STATE_TYPES = { + "OL": "Online", + "OB": "On Battery", + "LB": "Low Battery", + "HB": "High Battery", + "RB": "Battery Needs Replaced", + "CHRG": "Battery Charging", + "DISCHRG": "Battery Discharging", + "BYPASS": "Bypass Active", + "CAL": "Runtime Calibration", + "OFF": "Offline", + "OVER": "Overloaded", + "TRIM": "Trimming Voltage", + "BOOST": "Boosting Voltage", + "FSD": "Forced Shutdown", + "ALARM": "Alarm", +} diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index a44e70f9aa966..26accb5edb8dd 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -2,7 +2,10 @@ "domain": "nut", "name": "Network UPS Tools (NUT)", "documentation": "https://www.home-assistant.io/integrations/nut", - "requirements": ["pynut2==2.1.2"], + "requirements": [ + "pynut2==2.1.2" + ], "dependencies": [], - "codeowners": [] + "codeowners": ["@bdraco"], + "config_flow": true } diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 1b6029544148f..a611c8d4268d7 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -2,10 +2,10 @@ from datetime import timedelta import logging -from pynut2.nut2 import PyNUTClient, PyNUTError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_STATE, CONF_ALIAS, @@ -15,140 +15,33 @@ CONF_PORT, CONF_RESOURCES, CONF_USERNAME, - POWER_WATT, STATE_UNKNOWN, - TEMP_CELSIUS, - TIME_SECONDS, - UNIT_PERCENTAGE, ) -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) +from .const import ( + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, + KEY_STATUS, + KEY_STATUS_DISPLAY, + PYNUT_DATA, + PYNUT_FIRMWARE, + PYNUT_MANUFACTURER, + PYNUT_MODEL, + PYNUT_STATUS, + PYNUT_UNIQUE_ID, + SENSOR_TYPES, + STATE_TYPES, +) -DEFAULT_NAME = "NUT UPS" -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 3493 +_LOGGER = logging.getLogger(__name__) -KEY_STATUS = "ups.status" -KEY_STATUS_DISPLAY = "ups.status.display" SCAN_INTERVAL = timedelta(seconds=60) -SENSOR_TYPES = { - "ups.status.display": ["Status", "", "mdi:information-outline"], - "ups.status": ["Status Data", "", "mdi:information-outline"], - "ups.alarm": ["Alarms", "", "mdi:alarm"], - "ups.time": ["Internal Time", "", "mdi:calendar-clock"], - "ups.date": ["Internal Date", "", "mdi:calendar"], - "ups.model": ["Model", "", "mdi:information-outline"], - "ups.mfr": ["Manufacturer", "", "mdi:information-outline"], - "ups.mfr.date": ["Manufacture Date", "", "mdi:calendar"], - "ups.serial": ["Serial Number", "", "mdi:information-outline"], - "ups.vendorid": ["Vendor ID", "", "mdi:information-outline"], - "ups.productid": ["Product ID", "", "mdi:information-outline"], - "ups.firmware": ["Firmware Version", "", "mdi:information-outline"], - "ups.firmware.aux": ["Firmware Version 2", "", "mdi:information-outline"], - "ups.temperature": ["UPS Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "ups.load": ["Load", UNIT_PERCENTAGE, "mdi:gauge"], - "ups.load.high": ["Overload Setting", UNIT_PERCENTAGE, "mdi:gauge"], - "ups.id": ["System identifier", "", "mdi:information-outline"], - "ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer"], - "ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer"], - "ups.delay.shutdown": ["UPS Shutdown Delay", TIME_SECONDS, "mdi:timer"], - "ups.timer.start": ["Load Start Timer", TIME_SECONDS, "mdi:timer"], - "ups.timer.reboot": ["Load Reboot Timer", TIME_SECONDS, "mdi:timer"], - "ups.timer.shutdown": ["Load Shutdown Timer", TIME_SECONDS, "mdi:timer"], - "ups.test.interval": ["Self-Test Interval", TIME_SECONDS, "mdi:timer"], - "ups.test.result": ["Self-Test Result", "", "mdi:information-outline"], - "ups.test.date": ["Self-Test Date", "", "mdi:calendar"], - "ups.display.language": ["Language", "", "mdi:information-outline"], - "ups.contacts": ["External Contacts", "", "mdi:information-outline"], - "ups.efficiency": ["Efficiency", UNIT_PERCENTAGE, "mdi:gauge"], - "ups.power": ["Current Apparent Power", "VA", "mdi:flash"], - "ups.power.nominal": ["Nominal Power", "VA", "mdi:flash"], - "ups.realpower": ["Current Real Power", POWER_WATT, "mdi:flash"], - "ups.realpower.nominal": ["Nominal Real Power", POWER_WATT, "mdi:flash"], - "ups.beeper.status": ["Beeper Status", "", "mdi:information-outline"], - "ups.type": ["UPS Type", "", "mdi:information-outline"], - "ups.watchdog.status": ["Watchdog Status", "", "mdi:information-outline"], - "ups.start.auto": ["Start on AC", "", "mdi:information-outline"], - "ups.start.battery": ["Start on Battery", "", "mdi:information-outline"], - "ups.start.reboot": ["Reboot on Battery", "", "mdi:information-outline"], - "ups.shutdown": ["Shutdown Ability", "", "mdi:information-outline"], - "battery.charge": ["Battery Charge", UNIT_PERCENTAGE, "mdi:gauge"], - "battery.charge.low": ["Low Battery Setpoint", UNIT_PERCENTAGE, "mdi:gauge"], - "battery.charge.restart": [ - "Minimum Battery to Start", - UNIT_PERCENTAGE, - "mdi:gauge", - ], - "battery.charge.warning": [ - "Warning Battery Setpoint", - UNIT_PERCENTAGE, - "mdi:gauge", - ], - "battery.charger.status": ["Charging Status", "", "mdi:information-outline"], - "battery.voltage": ["Battery Voltage", "V", "mdi:flash"], - "battery.voltage.nominal": ["Nominal Battery Voltage", "V", "mdi:flash"], - "battery.voltage.low": ["Low Battery Voltage", "V", "mdi:flash"], - "battery.voltage.high": ["High Battery Voltage", "V", "mdi:flash"], - "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash"], - "battery.current": ["Battery Current", "A", "mdi:flash"], - "battery.current.total": ["Total Battery Current", "A", "mdi:flash"], - "battery.temperature": ["Battery Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer"], - "battery.runtime.low": ["Low Battery Runtime", TIME_SECONDS, "mdi:timer"], - "battery.runtime.restart": [ - "Minimum Battery Runtime to Start", - TIME_SECONDS, - "mdi:timer", - ], - "battery.alarm.threshold": [ - "Battery Alarm Threshold", - "", - "mdi:information-outline", - ], - "battery.date": ["Battery Date", "", "mdi:calendar"], - "battery.mfr.date": ["Battery Manuf. Date", "", "mdi:calendar"], - "battery.packs": ["Number of Batteries", "", "mdi:information-outline"], - "battery.packs.bad": ["Number of Bad Batteries", "", "mdi:information-outline"], - "battery.type": ["Battery Chemistry", "", "mdi:information-outline"], - "input.sensitivity": ["Input Power Sensitivity", "", "mdi:information-outline"], - "input.transfer.low": ["Low Voltage Transfer", "V", "mdi:flash"], - "input.transfer.high": ["High Voltage Transfer", "V", "mdi:flash"], - "input.transfer.reason": ["Voltage Transfer Reason", "", "mdi:information-outline"], - "input.voltage": ["Input Voltage", "V", "mdi:flash"], - "input.voltage.nominal": ["Nominal Input Voltage", "V", "mdi:flash"], - "input.frequency": ["Input Line Frequency", "hz", "mdi:flash"], - "input.frequency.nominal": ["Nominal Input Line Frequency", "hz", "mdi:flash"], - "input.frequency.status": ["Input Frequency Status", "", "mdi:information-outline"], - "output.current": ["Output Current", "A", "mdi:flash"], - "output.current.nominal": ["Nominal Output Current", "A", "mdi:flash"], - "output.voltage": ["Output Voltage", "V", "mdi:flash"], - "output.voltage.nominal": ["Nominal Output Voltage", "V", "mdi:flash"], - "output.frequency": ["Output Frequency", "hz", "mdi:flash"], - "output.frequency.nominal": ["Nominal Output Frequency", "hz", "mdi:flash"], -} - -STATE_TYPES = { - "OL": "Online", - "OB": "On Battery", - "LB": "Low Battery", - "HB": "High Battery", - "RB": "Battery Needs Replaced", - "CHRG": "Battery Charging", - "DISCHRG": "Battery Discharging", - "BYPASS": "Bypass Active", - "CAL": "Runtime Calibration", - "OFF": "Offline", - "OVER": "Overloaded", - "TRIM": "Trimming Voltage", - "BOOST": "Boosting Voltage", - "FSD": "Forced Shutdown", - "ALARM": "Alarm", -} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -164,34 +57,48 @@ def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the NUT sensors.""" - name = config[CONF_NAME] - host = config[CONF_HOST] - port = config[CONF_PORT] + """Import the platform into a config entry.""" - alias = config.get(CONF_ALIAS) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) - data = PyNUTData(host, port, alias, username, password) - if data.status is None: - _LOGGER.error("NUT Sensor has no data, unable to set up") - raise PlatformNotReady +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the NUT sensors.""" - _LOGGER.debug("NUT Sensors Available: %s", data.status) + config = config_entry.data + pynut_data = hass.data[DOMAIN][config_entry.entry_id] + data = pynut_data[PYNUT_DATA] + status = pynut_data[PYNUT_STATUS] + unique_id = pynut_data[PYNUT_UNIQUE_ID] + manufacturer = pynut_data[PYNUT_MANUFACTURER] + model = pynut_data[PYNUT_MODEL] + firmware = pynut_data[PYNUT_FIRMWARE] entities = [] - for resource in config[CONF_RESOURCES]: + name = config[CONF_NAME] + if CONF_RESOURCES in config_entry.options: + resources = config_entry.options[CONF_RESOURCES] + else: + resources = config_entry.data[CONF_RESOURCES] + + for resource in resources: sensor_type = resource.lower() # Display status is a special case that falls back to the status value # of the UPS instead. - if sensor_type in data.status or ( - sensor_type == KEY_STATUS_DISPLAY and KEY_STATUS in data.status + if sensor_type in status or ( + sensor_type == KEY_STATUS_DISPLAY and KEY_STATUS in status ): - entities.append(NUTSensor(name, data, sensor_type)) + entities.append( + NUTSensor( + name, data, sensor_type, unique_id, manufacturer, model, firmware + ) + ) else: _LOGGER.warning( "Sensor type: %s does not appear in the NUT status " @@ -199,30 +106,53 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_type, ) - try: - data.update(no_throttle=True) - except data.pynuterror as err: - _LOGGER.error( - "Failure while testing NUT status retrieval. Cannot continue setup: %s", err - ) - raise PlatformNotReady - - add_entities(entities, True) + async_add_entities(entities, True) class NUTSensor(Entity): """Representation of a sensor entity for NUT status values.""" - def __init__(self, name, data, sensor_type): + def __init__( + self, name, data, sensor_type, unique_id, manufacturer, model, firmware + ): """Initialize the sensor.""" self._data = data - self.type = sensor_type + self._type = sensor_type + self._manufacturer = manufacturer + self._firmware = firmware + self._model = model + self._device_name = name self._name = "{} {}".format(name, SENSOR_TYPES[sensor_type][0]) self._unit = SENSOR_TYPES[sensor_type][1] self._state = None + self._unique_id = unique_id self._display_state = None self._available = False + @property + def device_info(self): + """Device info for the ups.""" + if not self._unique_id: + return None + device_info = { + "identifiers": {(DOMAIN, self._unique_id)}, + "name": self._device_name, + } + if self._model: + device_info["model"] = self._model + if self._manufacturer: + device_info["manufacturer"] = self._manufacturer + if self._firmware: + device_info["sw_version"] = self._firmware + return device_info + + @property + def unique_id(self): + """Sensor Unique id.""" + if not self._unique_id: + return None + return f"{self._unique_id}_{self._type}" + @property def name(self): """Return the name of the UPS sensor.""" @@ -231,7 +161,7 @@ def name(self): @property def icon(self): """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return SENSOR_TYPES[self._type][2] @property def state(self): @@ -265,12 +195,12 @@ def update(self): self._display_state = _format_display_state(status) # In case of the display status sensor, keep a human-readable form # as the sensor state. - if self.type == KEY_STATUS_DISPLAY: + if self._type == KEY_STATUS_DISPLAY: self._state = self._display_state - elif self.type not in status: + elif self._type not in status: self._state = None else: - self._state = status[self.type] + self._state = status[self._type] def _format_display_state(status): @@ -281,58 +211,3 @@ def _format_display_state(status): return " ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split()) except KeyError: return STATE_UNKNOWN - - -class PyNUTData: - """Stores the data retrieved from NUT. - - For each entity to use, acts as the single point responsible for fetching - updates from the server. - """ - - def __init__(self, host, port, alias, username, password): - """Initialize the data object.""" - - self._host = host - self._port = port - self._alias = alias - self._username = username - self._password = password - - self.pynuterror = PyNUTError - # Establish client with persistent=False to open/close connection on - # each update call. This is more reliable with async. - self._client = PyNUTClient( - self._host, self._port, self._username, self._password, 5, False - ) - - self._status = None - - @property - def status(self): - """Get latest update if throttle allows. Return status.""" - self.update() - return self._status - - def _get_alias(self): - """Get the ups alias from NUT.""" - try: - return next(iter(self._client.list_ups())) - except self.pynuterror as err: - _LOGGER.error("Failure getting NUT ups alias, %s", err) - return None - - def _get_status(self): - """Get the ups status from NUT.""" - if self._alias is None: - self._alias = self._get_alias() - - try: - return self._client.list_vars(self._alias) - except (self.pynuterror, ConnectionResetError) as err: - _LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err) - return None - - def update(self, **kwargs): - """Fetch the latest status from NUT.""" - self._status = self._get_status() diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json new file mode 100644 index 0000000000000..e37a019af78c0 --- /dev/null +++ b/homeassistant/components/nut/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "title": "Network UPS Tools (NUT)", + "step": { + "user": { + "title": "Connect to the NUT server", + "description": "If there are multiple UPSs attached to the NUT server, enter the name UPS to query in the 'Alias' field.", + "data": { + "name": "Name", + "host": "Host", + "port": "Port", + "alias": "Alias", + "username": "Username", + "password": "Password", + "resources": "Resources" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "description": "Choose Sensor Resources", + "data": { + "resources": "Resources" + } + } + } + } + +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index eff11554a397e..62c0d3dd2c11e 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -60,6 +60,11 @@ async def async_will_remove_from_hass(self): ) self._unsub_updates() + @property + def available(self): + """Return availability of the sensor.""" + return self._state is not None + @property def entity_registry_enabled_default(self): """Disable binary_sensors by default.""" @@ -68,8 +73,9 @@ def entity_registry_enabled_default(self): @callback def receive_report(self, status): """Handle status updates from the component.""" - self._state = bool(status.get(self._var)) - self.async_schedule_update_ha_state() + state = status.get(self._var) + self._state = None if state is None else bool(state) + self.async_write_ha_state() @property def name(self): diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 2db20662a774d..a7e7eedef34d0 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -63,6 +63,7 @@ def __init__(self, gw_dev, options): self.friendly_name = gw_dev.name self.floor_temp = options.get(CONF_FLOOR_TEMP, DEFAULT_FLOOR_TEMP) self.temp_precision = options.get(CONF_PRECISION, DEFAULT_PRECISION) + self._available = False self._current_operation = None self._current_temperature = None self._hvac_mode = HVAC_MODE_HEAT @@ -80,7 +81,7 @@ def update_options(self, entry): """Update climate entity options.""" self.floor_temp = entry.options[CONF_FLOOR_TEMP] self.temp_precision = entry.options[CONF_PRECISION] - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" @@ -101,6 +102,7 @@ async def async_will_remove_from_hass(self): @callback def receive_report(self, status): """Receive and handle a new report from the Gateway.""" + self._available = bool(status) ch_active = status.get(gw_vars.DATA_SLAVE_CH_ACTIVE) flame_on = status.get(gw_vars.DATA_SLAVE_FLAME_ON) cooling_active = status.get(gw_vars.DATA_SLAVE_COOLING_ACTIVE) @@ -144,7 +146,12 @@ def receive_report(self, status): self._away_state_b = ( status.get(gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b ) - self.async_schedule_update_ha_state() + self.async_write_ha_state() + + @property + def available(self): + """Return availability of the sensor.""" + return self._available @property def name(self): @@ -253,7 +260,7 @@ async def async_set_temperature(self, **kwargs): self._new_target_temperature = await self._gateway.gateway.set_target_temp( temp ) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def supported_features(self): diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 3739f77e69dfc..b2f8e27298379 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -61,6 +61,11 @@ async def async_will_remove_from_hass(self): _LOGGER.debug("Removing OpenTherm Gateway sensor %s", self._friendly_name) self._unsub_updates() + @property + def available(self): + """Return availability of the sensor.""" + return self._value is not None + @property def entity_registry_enabled_default(self): """Disable sensors by default.""" @@ -73,7 +78,7 @@ def receive_report(self, status): if isinstance(value, float): value = f"{value:2.1f}" self._value = value - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def name(self): diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 3c4cd464d444b..df5f6af16957e 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -54,7 +54,7 @@ async def async_added_to_hass(self): @callback def _call_loop(self): self._state = self._otp.now() - self.async_schedule_update_ha_state() + self.async_write_ha_state() # Update must occur at even TIME_STEP, e.g. 12:00:00, 12:00:30, # 12:01:00, etc. in order to have synced time (see RFC6238) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 006929c7345f2..8399719e861b5 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -484,7 +484,7 @@ def _update_state(self): self._longitude = None self._gps_accuracy = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def _parse_source_state(self, state): diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 30542db5e237e..c26c3f5e68a87 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -255,7 +255,7 @@ def _update_state(self): self._state = STATE_OK self._problems = PROBLEM_NONE _LOGGER.debug("New data processed") - self.async_schedule_update_ha_state() + self.async_write_ha_state() def _check_min(self, sensor_name, value, params): """If configured, check the value against the defined minimum value.""" @@ -322,7 +322,7 @@ async def _load_history_from_db(self): except ValueError: pass _LOGGER.debug("Initializing from database completed") - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def should_poll(self): diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index e86b3dd42e874..d84c408e43a44 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -72,7 +72,7 @@ def _webhook_event(self, data, webhook): _LOGGER.debug("Received webhook: %s", _type) self._home["alarm_status"] = _type self._changed_by = _changed_by - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def _home(self): diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index a08f7dbedc402..b417c128913dc 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -95,7 +95,7 @@ async def _update_callback(self): self._is_on = True else: self._is_on = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def _webhook_event(self, data, webhook): @@ -111,7 +111,7 @@ def _webhook_event(self, data, webhook): self._is_on = True if _type == self._events[1]: self._is_on = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def is_on(self): diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index 324565aae50c0..70fe1ef0b6dc2 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -62,7 +62,7 @@ async def _update_callback(self): self.device.sensor, self.device_class ) self._updated = parse_datetime(self.device.last_update) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def icon(self): diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index f78966253b7cc..31f2f88dac722 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -144,7 +144,7 @@ def reset_state(now): self._state = STATE_IDLE self._expired_listener = None _LOGGER.debug("Reset state") - self.async_schedule_update_ha_state() + self.async_write_ha_state() if self._expired_listener: self._expired_listener() @@ -153,7 +153,7 @@ def reset_state(now): self.hass, reset_state, dt_util.utcnow() + self._timeout ) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_camera_image(self): """Return a still image response.""" diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index 33392c51be8e0..c2d938f6ed77f 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -87,7 +87,7 @@ def unique_id(self): @callback def update_packet(self, packet): """Receive update packet from QSUSB. Match dispather_send signature.""" - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Listen for updates from QSUSb via dispatcher.""" diff --git a/homeassistant/components/qwikswitch/binary_sensor.py b/homeassistant/components/qwikswitch/binary_sensor.py index 054028b5629e8..b3635dcb1f47c 100644 --- a/homeassistant/components/qwikswitch/binary_sensor.py +++ b/homeassistant/components/qwikswitch/binary_sensor.py @@ -52,7 +52,7 @@ def update_packet(self, packet): ) if val is not None: self._val = bool(val) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def is_on(self): diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 4674da420b22b..9609af42f6543 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -51,7 +51,7 @@ def update_packet(self, packet): ) if val is not None: self._val = val - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def state(self): diff --git a/homeassistant/components/rachio/.translations/pl.json b/homeassistant/components/rachio/.translations/pl.json new file mode 100644 index 0000000000000..b186a764cd127 --- /dev/null +++ b/homeassistant/components/rachio/.translations/pl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API dla konta Rachio." + }, + "description": "B\u0119dziesz potrzebowa\u0142 klucza API ze strony https://app.rach.io/. Wybierz 'Account Settings', a nast\u0119pnie kliknij 'GET API KEY'.", + "title": "Po\u0142\u0105cz si\u0119 z urz\u0105dzeniem Rachio" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Jak d\u0142ugo, w minutach, nale\u017cy w\u0142\u0105czy\u0107 stacj\u0119, gdy prze\u0142\u0105cznik jest w\u0142\u0105czony." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 13e8029b512a2..2c439407c71d9 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -22,6 +22,7 @@ KEY_NAME = "name" KEY_MODEL = "model" KEY_ON = "on" +KEY_DURATION = "totalDuration" KEY_STATUS = "status" KEY_SUBTYPE = "subType" KEY_SUMMARY = "summary" @@ -33,6 +34,8 @@ KEY_ZONE_ID = "zoneId" KEY_ZONE_NUMBER = "zoneNumber" KEY_ZONES = "zones" +KEY_SCHEDULES = "scheduleRules" +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 949957ae8ec5e..fbf49ffc67f12 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -13,6 +13,7 @@ KEY_MAC_ADDRESS, KEY_MODEL, KEY_NAME, + KEY_SCHEDULES, KEY_SERIAL_NUMBER, KEY_STATUS, KEY_USERNAME, @@ -90,6 +91,7 @@ def __init__(self, hass, rachio, data, webhooks): self.mac_address = data[KEY_MAC_ADDRESS] self.model = data[KEY_MODEL] self._zones = data[KEY_ZONES] + self._schedules = data[KEY_SCHEDULES] self._init_data = data self._webhooks = webhooks _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) @@ -174,6 +176,10 @@ def get_zone(self, zone_id) -> Optional[dict]: return None + def list_schedules(self) -> list: + """Return a list of schedules.""" + return self._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 5df084a11a417..a4ba1a41fee0b 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -15,20 +15,26 @@ KEY_CUSTOM_CROP, KEY_CUSTOM_SHADE, KEY_DEVICE_ID, + KEY_DURATION, KEY_ENABLED, KEY_ID, KEY_IMAGE_URL, KEY_NAME, KEY_ON, + KEY_SCHEDULE_ID, KEY_SUBTYPE, KEY_SUMMARY, KEY_ZONE_ID, KEY_ZONE_NUMBER, SIGNAL_RACHIO_CONTROLLER_UPDATE, + SIGNAL_RACHIO_SCHEDULE_UPDATE, SIGNAL_RACHIO_ZONE_UPDATE, ) from .entity import RachioDevice from .webhooks import ( + SUBTYPE_SCHEDULE_COMPLETED, + SUBTYPE_SCHEDULE_STARTED, + SUBTYPE_SCHEDULE_STOPPED, SUBTYPE_SLEEP_MODE_OFF, SUBTYPE_SLEEP_MODE_ON, SUBTYPE_ZONE_COMPLETED, @@ -40,6 +46,9 @@ ATTR_ZONE_SUMMARY = "Summary" ATTR_ZONE_NUMBER = "Zone number" +ATTR_SCHEDULE_SUMMARY = "Summary" +ATTR_SCHEDULE_ENABLED = "Enabled" +ATTR_SCHEDULE_DURATION = "Duration" async def async_setup_entry(hass, config_entry, async_add_entities): @@ -58,11 +67,14 @@ def _create_entities(hass, config_entry): for controller in person.controllers: entities.append(RachioStandbySwitch(controller)) zones = controller.list_zones() + schedules = controller.list_schedules() current_schedule = controller.current_schedule - _LOGGER.debug("Rachio setting up zones: %s", zones) for zone in zones: _LOGGER.debug("Rachio setting up zone: %s", zone) entities.append(RachioZone(person, controller, zone, current_schedule)) + for sched in schedules: + _LOGGER.debug("Added schedule: %s", sched) + entities.append(RachioSchedule(person, controller, sched, current_schedule)) return entities @@ -276,3 +288,98 @@ 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_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 + + @property + def name(self) -> str: + """Return the friendly name of the schedule.""" + return f"{self._schedule_name} Schedule" + + @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}" + + @property + def icon(self) -> str: + """Return the icon to display.""" + return "mdi:water" + + @property + def device_state_attributes(self) -> dict: + """Return the optional state attributes.""" + return { + ATTR_SCHEDULE_SUMMARY: self._summary, + ATTR_SCHEDULE_ENABLED: self.schedule_is_enabled, + ATTR_SCHEDULE_DURATION: self._duration / 60, + } + + @property + def schedule_is_enabled(self) -> bool: + """Return whether the schedule is allowed to run.""" + return self._schedule_enabled + + def turn_on(self, **kwargs) -> None: + """Start this schedule.""" + + self._controller.rachio.schedulerule.start(self.schedule_id) + _LOGGER.debug( + "Schedule %s started on %s", self.name, self._controller.name, + ) + + def turn_off(self, **kwargs) -> None: + """Stop watering all zones.""" + self._controller.stop_watering() + + 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) + + def _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_SUBTYPE] in [SUBTYPE_SCHEDULE_STARTED]: + self._state = True + elif args[0][KEY_SUBTYPE] in [ + SUBTYPE_SCHEDULE_STOPPED, + SUBTYPE_SCHEDULE_COMPLETED, + ]: + self._state = False + except KeyError: + pass + + self.schedule_update_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 + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe from updates.""" + if self._undo_dispatcher: + self._undo_dispatcher() diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index c12f2ccfd3ed9..7051ec046d84e 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -50,7 +50,11 @@ SUBTYPE_ZONE_CYCLING_COMPLETED = "ZONE_CYCLING_COMPLETED" # Webhook callbacks -LISTEN_EVENT_TYPES = ["DEVICE_STATUS_EVENT", "ZONE_STATUS_EVENT"] +LISTEN_EVENT_TYPES = [ + "DEVICE_STATUS_EVENT", + "ZONE_STATUS_EVENT", + "SCHEDULE_STATUS_EVENT", +] WEBHOOK_CONST_ID = "homeassistant.rachio:" WEBHOOK_PATH = URL_API + DOMAIN diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index b8665fae9ef2f..9ba008a56ef5d 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -313,7 +313,7 @@ def handle_event_callback(self, event): self._handle_event(event) # Propagate changes through ha - self.async_schedule_update_ha_state() + self.async_write_ha_state() # Put command onto bus for user to subscribe to if self._should_fire_event and identify_event_type(event) == EVENT_KEY_COMMAND: @@ -360,7 +360,7 @@ def available(self): def _availability_callback(self, availability): """Update availability state.""" self._available = availability - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Register update callback.""" diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index 148a7db2b64c3..ab50e6dcc4b36 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -84,7 +84,7 @@ def off_delay_listener(now): """Switch device off after a delay.""" self._delay_listener = None self._state = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() if self._delay_listener is not None: self._delay_listener() diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index fa47ac35ee37b..3340e0ad603f8 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -82,7 +82,7 @@ def _set_light(self, new_state): self._light_on = new_state == ON_STATE self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY - self.async_schedule_update_ha_state() + self.async_write_ha_state() def turn_on(self, **kwargs): """Turn the light on for 30 seconds.""" diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index e9cdb89711593..43ccb6b8ad3a0 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["roku==4.0.0"], + "requirements": ["roku==4.1.0"], "dependencies": [], "ssdp": [ { diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index d4294788fdda6..6034c24e31a74 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -78,7 +78,7 @@ def _update_alarm_status(self): _LOGGER.debug("Got status update, current status: %s", state) if state != self._state: self._state = state - self.async_schedule_update_ha_state() + self.async_write_ha_state() else: _LOGGER.debug("Ignoring alarm status message, same state") diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index cbe760c06bf1e..5b268266dda1a 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -110,4 +110,4 @@ def _devices_updated(self, zones): """Update the zone's state, if needed.""" if self._device_number in zones and self._state != zones[self._device_number]: self._state = zones[self._device_number] - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 9233b3d152d9a..46f4de2784f42 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -65,13 +65,13 @@ def _devices_updated(self, zones): _LOGGER.debug("New state: %s", new_state) if new_state != self._state: self._state = new_state - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_on(self, **kwargs): """Turn the device on.""" _LOGGER.debug("Switch: %s status: %s, turning on", self._name, self._state) await self._satel.set_output(self._code, self._device_number, True) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" @@ -79,7 +79,7 @@ async def async_turn_off(self, **kwargs): "Switch name: %s status: %s, turning off", self._name, self._state ) await self._satel.set_output(self._code, self._device_number, False) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def is_on(self): diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index a08f9522c4b9d..bc9ef9f9e794f 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -84,7 +84,7 @@ async def serial_read(self, device, rate, **kwargs): _LOGGER.debug("Received: %s", line) self._state = line - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def stop_serial_read(self): """Close resources.""" diff --git a/homeassistant/components/sisyphus/light.py b/homeassistant/components/sisyphus/light.py index 7c1b0ece4bf27..271db41ac2254 100644 --- a/homeassistant/components/sisyphus/light.py +++ b/homeassistant/components/sisyphus/light.py @@ -36,7 +36,7 @@ def __init__(self, name, table): async def async_added_to_hass(self): """Add listeners after this object has been initialized.""" - self._table.add_listener(lambda: self.async_schedule_update_ha_state(False)) + self._table.add_listener(self.async_write_ha_state) @property def available(self): diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index e708504ff7ed4..103ec694d83e2 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -67,7 +67,7 @@ def __init__(self, name, host, table): async def async_added_to_hass(self): """Add listeners after this object has been initialized.""" - self._table.add_listener(lambda: self.async_schedule_update_ha_state(False)) + self._table.add_listener(self.async_write_ha_state) @property def unique_id(self): diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 232540ee47b03..83a1af981db82 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -336,7 +336,7 @@ async def async_set_fan_mode(self, fan_mode): await self._device.set_fan_mode(fan_mode, set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" @@ -355,7 +355,7 @@ async def async_set_hvac_mode(self, hvac_mode): await asyncio.gather(*tasks) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_temperature(self, **kwargs): """Set new target temperature.""" @@ -376,21 +376,21 @@ async def async_set_temperature(self, **kwargs): await asyncio.gather(*tasks) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_on(self): """Turn device on.""" await self._device.switch_on(set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self): """Turn device off.""" await self._device.switch_off(set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_update(self): """Update the calculated fields of the AC.""" diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index aad62aed486cf..e366f6bb3e3e0 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -48,7 +48,7 @@ async def async_set_speed(self, speed: str): await self._device.set_fan_speed(value, set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn the fan on.""" @@ -59,14 +59,14 @@ async def async_turn_on(self, speed: str = None, **kwargs) -> None: await self._device.switch_on(set_status=True) # State is set optimistically in the commands above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the fan off.""" await self._device.switch_off(set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def is_on(self) -> bool: diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index 2895bde0bf71b..d249cc5ac947a 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -44,12 +44,12 @@ class SmartThingsLock(SmartThingsEntity, LockDevice): async def async_lock(self, **kwargs): """Lock the device.""" await self._device.lock(set_status=True) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_unlock(self, **kwargs): """Unlock the device.""" await self._device.unlock(set_status=True) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def is_locked(self): diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index ace47a56d2cc8..eb9c9c90c4ba9 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -37,14 +37,14 @@ async def async_turn_off(self, **kwargs) -> None: await self._device.switch_off(set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_on(self, **kwargs) -> None: """Turn the switch on.""" await self._device.switch_on(set_status=True) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def current_power_w(self): diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index c3c9138eb89ca..004dd36ed4a99 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -176,17 +176,17 @@ async def async_select_source(self, source): streams = self._group.streams_by_name() if source in streams: await self._group.set_stream(streams[source].identifier) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_mute_volume(self, mute): """Send the mute command.""" await self._group.set_muted(mute) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_volume_level(self, volume): """Set the volume level.""" await self._group.set_volume(round(volume * 100)) - self.async_schedule_update_ha_state() + self.async_write_ha_state() def snapshot(self): """Snapshot the group state.""" @@ -273,17 +273,17 @@ async def async_select_source(self, source): streams = self._client.group.streams_by_name() if source in streams: await self._client.group.set_stream(streams[source].identifier) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_mute_volume(self, mute): """Send the mute command.""" await self._client.set_muted(mute) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_volume_level(self, volume): """Set the volume level.""" await self._client.set_volume(round(volume * 100)) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_join(self, master): """Join the group of the master player.""" @@ -293,12 +293,12 @@ async def async_join(self, master): if master.identifier in group.clients ] await master_group[0].add_client(self._client.identifier) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_unjoin(self): """Unjoin the group the player is currently in.""" await self._client.group.remove_client(self._client.identifier) - self.async_schedule_update_ha_state() + self.async_write_ha_state() def snapshot(self): """Snapshot the client state.""" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index bb1179bb1e702..d54e6dd968f98 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -464,7 +464,7 @@ async def async_seen(self): self._seen_timer() self.async_unseen() - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def async_unseen(self, now=None): @@ -483,7 +483,7 @@ def _unsub(subscriptions): self._subscriptions = [] - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def available(self) -> bool: @@ -725,7 +725,7 @@ def _async_regroup(group): self._coordinator = None self._sonos_group = sonos_group - self.async_schedule_update_ha_state() + self.async_write_ha_state() for slave_uid in group[1:]: slave = _get_entity_from_soco_uid(self.hass, slave_uid) diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 8c9c9e1a6fa37..4ace2c6eea1fc 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -121,7 +121,7 @@ async def async_update_data(self, device_data: "SwitcherV2Device") -> None: else: self._device_data = device_data self._state = self._device_data.state - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Dict) -> None: """Turn the entity on.""" @@ -149,4 +149,4 @@ async def _control_device(self, send_on: bool) -> None: if response and response.successful: self._self_initiated = True self._state = SWITCHER_STATE_ON if send_on else SWITCHER_STATE_OFF - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index 50a219bf7a15c..851823385dc49 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -43,7 +43,7 @@ def _update_callback(self): """Return the property of the device might have changed.""" if self.device.name: self._name = self.device.name - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def device_id(self): diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 45cccef9766e3..937119ff6d409 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -236,7 +236,7 @@ async def _async_alarm_arm(self, state, script=None, code=None): _LOGGER.error("No script action defined for %s", state) if optimistic_set: - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_alarm_arm_away(self, code=None): """Arm the panel to Away.""" diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 8991ce4c65b70..df918d3dd776d 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -283,7 +283,7 @@ def async_check_state(self): def set_state(): """Set state of template binary sensor.""" self._state = state - self.async_schedule_update_ha_state() + self.async_write_ha_state() # state without delay if (state and not self._delay_on) or (not state and not self._delay_off): diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 14fc699637873..3e3232f2b911e 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -322,7 +322,7 @@ async def async_open_cover(self, **kwargs): ) if self._optimistic: self._position = 100 - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_close_cover(self, **kwargs): """Move the cover down.""" @@ -334,7 +334,7 @@ async def async_close_cover(self, **kwargs): ) if self._optimistic: self._position = 0 - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_stop_cover(self, **kwargs): """Fire the stop action.""" @@ -348,7 +348,7 @@ async def async_set_cover_position(self, **kwargs): {"position": self._position}, context=self._context ) if self._optimistic: - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" @@ -357,7 +357,7 @@ async def async_open_cover_tilt(self, **kwargs): {"tilt": self._tilt_value}, context=self._context ) if self._tilt_optimistic: - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" @@ -366,7 +366,7 @@ async def async_close_cover_tilt(self, **kwargs): {"tilt": self._tilt_value}, context=self._context ) if self._tilt_optimistic: - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" @@ -375,7 +375,7 @@ async def async_set_cover_tilt_position(self, **kwargs): {"tilt": self._tilt_value}, context=self._context ) if self._tilt_optimistic: - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_update(self): """Update the state from the template.""" diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 7948782479b7f..52560ea27329c 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -316,14 +316,14 @@ async def async_turn_on(self, **kwargs): await self._on_script.async_run() if optimistic_set: - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the light off.""" await self._off_script.async_run(context=self._context) if self._template is None: self._state = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_update(self): """Update from templates.""" diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index f4a6b55dd18f2..a5caac0012316 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -174,12 +174,12 @@ async def async_lock(self, **kwargs): """Lock the device.""" if self._optimistic: self._state = True - self.async_schedule_update_ha_state() + self.async_write_ha_state() await self._command_lock.async_run(context=self._context) async def async_unlock(self, **kwargs): """Unlock the device.""" if self._optimistic: self._state = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() await self._command_unlock.async_run(context=self._context) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index a5a7f320d9331..9e95f3cc05b17 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -169,7 +169,7 @@ async def _async_callback(self, payload): continue self._device_state_attributes[key] = value - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def available(self): diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 1deb564133edb..6081f1dfca64d 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -136,7 +136,7 @@ def _update_internal_state(self, time_date): def point_in_time_listener(self, time_date): """Get the latest data and update state.""" self._update_internal_state(time_date) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async_track_point_in_utc_time( self.hass, self.point_in_time_listener, self.get_next_interval() ) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 72507b3d1481c..ee9969c9974a9 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -234,7 +234,7 @@ def _calculate_next_update(self): def _point_in_time_listener(self, now): """Run when the state of the sensor should be updated.""" self._calculate_next_update() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async_track_point_in_utc_time( self.hass, self._point_in_time_listener, self.next_update diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index f084c135e474f..dbb650a8b48db 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -143,4 +143,4 @@ def icon(self): def async_on_update(self, value): """Receive an update.""" self._state = value - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 358056d7ef601..0850bec6c9bcb 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -33,7 +33,7 @@ def __init__(self, device, api, gateway_id): def _async_start_observe(self, exc=None): """Start observation of device.""" if exc: - self.async_schedule_update_ha_state() + self.async_write_ha_state() _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) try: @@ -70,7 +70,7 @@ def unique_id(self): def _observe_update(self, device): """Receive new state data for this device.""" self._refresh(device) - self.async_schedule_update_ha_state() + self.async_write_ha_state() def _refresh(self, device): """Refresh the device data.""" diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 07e96a45fce57..43494ec485fc1 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -217,7 +217,7 @@ def _scheduled_update(now): """Scheduled callback for update.""" self.is_disconnected = True self.cancel_scheduled_update = None - self.async_schedule_update_ha_state() + self.async_write_ha_state() if ( not self.is_wired @@ -323,7 +323,7 @@ def async_update_callback(self): """Update the sensor's state.""" LOGGER.debug("Updating UniFi tracked device %s", self.entity_id) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def is_connected(self): diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index b46771e574bcf..5efb73c2a01f0 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -79,7 +79,7 @@ def async_update_callback(self) -> None: self.is_blocked = self.client.event.event in CLIENT_BLOCKED LOGGER.debug("Updating client %s %s", self.entity_id, self.client.mac) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def name(self) -> str: diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index ad82cd9e79f5d..ad1b27f1fe09e 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -163,7 +163,7 @@ def async_reading(self, entity, old_state, new_state): _LOGGER.warning( "Invalid state (%s > %s): %s", old_state.state, new_state.state, err ) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def async_tariff_change(self, entity, old_state, new_state): @@ -184,7 +184,7 @@ def async_tariff_change(self, entity, old_state, new_state): self._sensor_source_id, ) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def _async_reset_meter(self, event): """Determine cycle - Helper function for larger than daily cycles.""" diff --git a/homeassistant/components/vizio/.translations/lb.json b/homeassistant/components/vizio/.translations/lb.json index 034e2b3df0080..79dfa120db261 100644 --- a/homeassistant/components/vizio/.translations/lb.json +++ b/homeassistant/components/vizio/.translations/lb.json @@ -37,7 +37,8 @@ "data": { "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?" - } + }, + "title": "Apps fir Smart TV konfigur\u00e9ieren" }, "user": { "data": { @@ -53,7 +54,8 @@ "data": { "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?" - } + }, + "title": "Apps fir Smart TV konfigur\u00e9ieren" } }, "title": "Vizio SmartCast" diff --git a/homeassistant/components/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json index c5b260427996c..91ed0b2dd536d 100644 --- a/homeassistant/components/vizio/.translations/pl.json +++ b/homeassistant/components/vizio/.translations/pl.json @@ -33,7 +33,7 @@ "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", "include_or_exclude": "Do\u0142\u0105czanie lub wykluczanie aplikacji" }, - "description": "Je\u015bli masz telewizor Smart TV, mo\u017cesz opcjonalnie filtrowa\u0107 list\u0119 \u017ar\u00f3de\u0142, wybieraj\u0105c aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone na li\u015bcie \u017ar\u00f3d\u0142owej. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", + "description": "Je\u015bli telewizor obs\u0142uguje aplikacje, mo\u017cesz opcjonalnie filtrowa\u0107 aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z listy \u017ar\u00f3de\u0142. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", "title": "Konfigurowanie aplikacji dla smart TV" }, "user": { @@ -50,7 +50,7 @@ "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", "include_or_exclude": "Do\u0142\u0105czy\u0107 czy wykluczy\u0107 aplikacje?" }, - "description": "Je\u015bli masz telewizor Smart TV, mo\u017cesz opcjonalnie filtrowa\u0107 list\u0119 \u017ar\u00f3de\u0142, wybieraj\u0105c aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone na li\u015bcie \u017ar\u00f3d\u0142owej. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", + "description": "Je\u015bli telewizor obs\u0142uguje aplikacje, mo\u017cesz opcjonalnie filtrowa\u0107 aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z listy \u017ar\u00f3de\u0142. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", "title": "Skonfiguruj aplikacje dla Smart TV" } }, @@ -61,9 +61,11 @@ "init": { "data": { "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", + "include_or_exclude": "Do\u0142\u0105czanie lub wykluczanie aplikacji", "timeout": "Limit czasu \u017c\u0105dania API (sekundy)", "volume_step": "Skok g\u0142o\u015bno\u015bci" }, + "description": "Je\u015bli telewizor obs\u0142uguje aplikacje, mo\u017cesz opcjonalnie filtrowa\u0107 aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z listy \u017ar\u00f3de\u0142. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", "title": "Aktualizacja opcji Vizo SmartCast" } }, diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index 9c83dbce804ff..16239e0c98ca5 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -130,7 +130,7 @@ def binary_sensor_update(self, event): def update_state(self, state): """Update the state of the device.""" self._state = state - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Register update callback.""" diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index e2c92d07f9c08..14f3549b2a37a 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -114,4 +114,4 @@ def async_update_callback(self): """Update state.""" if self.client.data is not None: self._state = getattr(self.client.data, self._attr, None) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index f4d9f97fe4230..87f55fd6b2d6f 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -153,7 +153,7 @@ async def async_handle_state_update(self): """Update state from WebOsClient.""" self.update_sources() - self.async_schedule_update_ha_state(False) + self.async_write_ha_state() def update_sources(self): """Update list of sources from current source, apps, inputs and configured list.""" diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 4ae39787335ad..a74381b8a856a 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -54,4 +54,4 @@ def unit_of_measurement(self): @callback def _update_count(self): self.count = self.hass.data.get(DATA_CONNECTIONS, 0) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index db1ba60364e6e..0ca1f950448fd 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -55,7 +55,7 @@ async def _async_locked_subscription_callback(self, force_update): return await self._async_locked_update(force_update) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Wemo sensor added to Home Assistant.""" diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index cec481a2eb4a6..24ed68c792dec 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -172,7 +172,7 @@ async def _async_locked_subscription_callback(self, force_update): return await self._async_locked_update(force_update) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def unique_id(self): diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 5988019e66f88..b8c05ead07634 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -236,7 +236,7 @@ async def _async_locked_subscription_callback(self, force_update): return await self._async_locked_update(force_update) - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Wemo dimmer added to Home Assistant.""" diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index ad8ea45ffd6b3..4b6d99da200c9 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -77,7 +77,7 @@ async def _async_locked_subscription_callback(self, force_update): return await self._async_locked_update(force_update) - self.async_schedule_update_ha_state() + self.async_write_ha_state() @property def unique_id(self): diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index 4fcebe73478c7..07acf6057a105 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -140,4 +140,4 @@ def _on_binary_event_callback(self, event): """Update state from arrived push notification.""" # state should be 'on' or 'off' self._state = event.data.get("state") - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index fa72ab184e168..7a41d23778167 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -111,4 +111,4 @@ def _update_tag_info_callback(self, event): _LOGGER.debug("Entity to update state: %s event data: %s", self, event.data) new_value = self._sensor.value_from_update_event(event.data) self._state = self.decorate_value(new_value) - self.async_schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index ae032a8b35f2a..533238ac5e762 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -286,7 +286,7 @@ def _async_set_unavailable(self, now): """Set state to UNAVAILABLE.""" self._remove_unavailability_tracker = None self._is_available = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def _async_track_unavailable(self): @@ -308,7 +308,7 @@ def push_data(self, data, raw_data): is_data = self.parse_data(data, raw_data) is_voltage = self.parse_voltage(data) if is_data or is_voltage or was_unavailable: - self.async_schedule_update_ha_state() + self.async_write_ha_state() def parse_voltage(self, data): """Parse battery level data sent by gateway.""" diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index a7e49f681c495..2c95198f34868 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -197,7 +197,7 @@ def _async_set_no_motion(self, now): """Set state to False.""" self._unsub_set_no_motion = None self._state = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() def parse_data(self, data, raw_data): """Parse data sent by gateway. diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index befbfc001ba4b..ed71e05bf5f02 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -63,7 +63,7 @@ def device_state_attributes(self) -> dict: def clear_unlock_state(self, _): """Clear unlock state automatically.""" self._state = STATE_LOCKED - self.async_schedule_update_ha_state() + self.async_write_ha_state() def parse_data(self, data, raw_data): """Parse data sent by gateway.""" diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 29e24b510e552..3c06a75fb71a9 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -2,7 +2,6 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DATA_UPDATED, DATA_YEELIGHT @@ -28,19 +27,21 @@ class YeelightNightlightModeSensor(BinarySensorDevice): def __init__(self, device): """Initialize nightlight mode sensor.""" self._device = device - - @callback - def _schedule_immediate_update(self): - self.async_schedule_update_ha_state() + self._unsub_disp = None async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( + self._unsub_disp = async_dispatcher_connect( self.hass, DATA_UPDATED.format(self._device.ipaddr), - self._schedule_immediate_update, + 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/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index e718e688c50df..a5255e7f756d5 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -20,9 +20,6 @@ ATTR_UNIQUE_ID, ATTR_VALUE, CHANNEL_ZDO, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_RPT_CHANGE, SIGNAL_ATTR_UPDATED, ) from ..helpers import LogMixin, safe_read @@ -149,57 +146,47 @@ async def bind(self): "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex) ) - async def configure_reporting( - self, - attr, - report_config=( - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE, - ), - ): + async def configure_reporting(self) -> None: """Configure attribute reporting for a cluster. This also swallows DeliveryError exceptions that are thrown when devices are unreachable. """ - attr_name = self.cluster.attributes.get(attr, [attr])[0] - kwargs = {} if self.cluster.cluster_id >= 0xFC00 and self._ch_pool.manufacturer_code: kwargs["manufacturer"] = self._ch_pool.manufacturer_code - min_report_int, max_report_int, reportable_change = report_config - try: - res = await self.cluster.configure_reporting( - attr, min_report_int, max_report_int, reportable_change, **kwargs - ) - self.debug( - "reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'", - attr_name, - self.cluster.ep_attribute, - min_report_int, - max_report_int, - reportable_change, - res, - ) - except (zigpy.exceptions.DeliveryError, asyncio.TimeoutError) as ex: - self.debug( - "failed to set reporting for '%s' attr on '%s' cluster: %s", - attr_name, - self.cluster.ep_attribute, - str(ex), - ) + for report in self._report_config: + attr = report["attr"] + attr_name = self.cluster.attributes.get(attr, [attr])[0] + min_report_int, max_report_int, reportable_change = report["config"] + try: + res = await self.cluster.configure_reporting( + attr, min_report_int, max_report_int, reportable_change, **kwargs + ) + self.debug( + "reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'", + attr_name, + self.cluster.ep_attribute, + min_report_int, + max_report_int, + reportable_change, + res, + ) + except (zigpy.exceptions.DeliveryError, asyncio.TimeoutError) as ex: + self.debug( + "failed to set reporting for '%s' attr on '%s' cluster: %s", + attr_name, + self.cluster.ep_attribute, + str(ex), + ) async def async_configure(self): """Set cluster binding and attribute reporting.""" if not self._ch_pool.skip_configuration: await self.bind() if self.cluster.is_server: - for report_config in self._report_config: - await self.configure_reporting( - report_config["attr"], report_config["config"] - ) + await self.configure_reporting() self.debug("finished channel configuration") else: self.debug("skipping channel configuration") diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 21f2f63612839..e032de4d94c59 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -5,6 +5,7 @@ import itertools import logging import os +import time import traceback from typing import List, Optional @@ -478,7 +479,9 @@ async def async_update_device_storage(self): async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType): """Handle device joined and basic information discovered (async).""" zha_device = self._async_get_or_create_device(device) - + # This is an active device so set a last seen if it is none + if zha_device.last_seen is None: + zha_device.async_update_last_seen(time.time()) _LOGGER.debug( "device - %s:%s entering async_device_initialized - is_new_join: %s", device.nwk, diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 3838e9b6a5081..0171ded67fe92 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -46,8 +46,8 @@ def async_create_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: name=device.name, ieee=str(device.ieee), last_seen=device.last_seen ) self.devices[device_entry.ieee] = device_entry - - return self.async_update_device(device) + self.async_schedule_save() + return device_entry @callback def async_get_or_create_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ea1bc1bbb2ff9..09dcf71d027d6 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.14.0", - "zha-quirks==0.0.37", + "bellows-homeassistant==0.15.1", + "zha-quirks==0.0.38", "zigpy-cc==0.3.1", - "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.16.0", - "zigpy-xbee-homeassistant==0.10.0", + "zigpy-deconz==0.8.0", + "zigpy-homeassistant==0.18.0", + "zigpy-xbee-homeassistant==0.11.0", "zigpy-zigate==0.5.1" ], "dependencies": [], diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 1491c10777ffc..57c4fbe61998a 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -1234,7 +1234,7 @@ async def value_renamed(self, update_ids=False): ent_reg.async_update_entity(self.entity_id, new_entity_id=new_entity_id) return # else for the above two ifs, update if not using update_entity - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Add device to dict.""" diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 3b94991312a63..3fb5491ea6263 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -273,7 +273,7 @@ async def node_renamed(self, update_ids=False): ent_reg.async_update_entity(self.entity_id, new_entity_id=new_entity_id) return # else for the above two ifs, update if not using update_entity - self.async_schedule_update_ha_state() + self.async_write_ha_state() def network_node_event(self, node, value): """Handle a node activated event on the network.""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2b96c63f4d7ff..dd0342a06a31f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -81,6 +81,7 @@ "nexia", "notion", "nuheat", + "nut", "opentherm_gw", "openuv", "owntracks", diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index a761273fd2562..76c2cb9889e44 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -128,7 +128,7 @@ async def async_setup(self, config: ConfigType) -> None: tasks.append(self.async_setup_platform(p_type, p_config)) if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) # Generic discovery listener for loading platform dynamically # Refer to: homeassistant.components.discovery.load_platform() @@ -263,7 +263,7 @@ async def _async_reset(self) -> None: tasks.append(platform.async_destroy()) if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) self._platforms = {self.domain: self._platforms[self.domain]} self.config = None diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 0aebaff14def9..4cbb7a23496c5 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -183,7 +183,7 @@ async def _async_setup_platform(self, async_create_setup_task, tries=0): self._tasks.clear() if pending: - await asyncio.wait(pending) + await asyncio.gather(*pending) hass.config.components.add(full_name) return True @@ -292,7 +292,7 @@ async def async_add_entities( if not tasks: return - await asyncio.wait(tasks) + await asyncio.gather(*tasks) if self._async_unsub_polling is not None or not any( entity.should_poll for entity in self.entities.values() @@ -431,10 +431,11 @@ async def _async_add_entity( already_exists = True if already_exists: - msg = f"Entity id already exists: {entity.entity_id}" + msg = f"Entity id already exists - ignoring: {entity.entity_id}" if entity.unique_id is not None: msg += f". Platform {self.platform_name} does not generate unique IDs" - raise HomeAssistantError(msg) + self.logger.error(msg) + return entity_id = entity.entity_id self.entities[entity_id] = entity @@ -459,7 +460,7 @@ async def async_reset(self) -> None: tasks = [self.async_remove_entity(entity_id) for entity_id in self.entities] - await asyncio.wait(tasks) + await asyncio.gather(*tasks) if self._async_unsub_polling is not None: self._async_unsub_polling() @@ -548,7 +549,7 @@ async def _update_entity_states(self, now: datetime) -> None: tasks.append(entity.async_update_ha_state(True)) # type: ignore if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar( diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 35423033cf934..f450fb6283c93 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,8 +11,8 @@ ciso8601==2.1.3 cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 -hass-nabucasa==0.32.2 -home-assistant-frontend==20200330.0 +hass-nabucasa==0.33.0 +home-assistant-frontend==20200401.0 importlib-metadata==1.5.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py index cc028478e516a..e4d376dc487ab 100644 --- a/homeassistant/util/network.py +++ b/homeassistant/util/network.py @@ -1,18 +1,41 @@ """Network utilities.""" -from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network +from ipaddress import IPv4Address, IPv6Address, ip_network from typing import Union -# IP addresses of loopback interfaces -LOCAL_IPS = (ip_address("127.0.0.1"), ip_address("::1")) +# RFC6890 - IP addresses of loopback interfaces +LOOPBACK_NETWORKS = ( + ip_network("127.0.0.0/8"), + ip_network("::1/128"), + ip_network("::ffff:127.0.0.0/104"), +) -# RFC1918 - Address allocation for Private Internets -LOCAL_NETWORKS = ( +# RFC6890 - Address allocation for Private Internets +PRIVATE_NETWORKS = ( + ip_network("fd00::/8"), ip_network("10.0.0.0/8"), ip_network("172.16.0.0/12"), ip_network("192.168.0.0/16"), ) +# RFC6890 - Link local ranges +LINK_LOCAL_NETWORK = ip_network("169.254.0.0/16") + + +def is_loopback(address: Union[IPv4Address, IPv6Address]) -> bool: + """Check if an address is a loopback address.""" + return any(address in network for network in LOOPBACK_NETWORKS) + + +def is_private(address: Union[IPv4Address, IPv6Address]) -> bool: + """Check if an address is a private address.""" + return any(address in network for network in PRIVATE_NETWORKS) + + +def is_link_local(address: Union[IPv4Address, IPv6Address]) -> bool: + """Check if an address is link local.""" + return address in LINK_LOCAL_NETWORK + def is_local(address: Union[IPv4Address, IPv6Address]) -> bool: - """Check if an address is local.""" - return address in LOCAL_IPS or any(address in network for network in LOCAL_NETWORKS) + """Check if an address is loopback or private.""" + return is_loopback(address) or is_private(address) diff --git a/requirements_all.txt b/requirements_all.txt index 9a420d98d2d41..d3d1f669df950 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -317,7 +317,7 @@ beautifulsoup4==4.8.2 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.14.0 +bellows-homeassistant==0.15.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.1 @@ -358,7 +358,7 @@ bravia-tv==1.0.1 broadlink==0.13.0 # homeassistant.components.brother -brother==0.1.9 +brother==0.1.11 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -674,7 +674,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.32.2 +hass-nabucasa==0.33.0 # homeassistant.components.mqtt hbmqtt==0.9.5 @@ -704,7 +704,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200330.0 +home-assistant-frontend==20200401.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -1807,7 +1807,7 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==4.0.0 +roku==4.1.0 # homeassistant.components.roomba roombapy==1.4.3 @@ -2176,7 +2176,7 @@ zengge==0.2 zeroconf==0.24.5 # homeassistant.components.zha -zha-quirks==0.0.37 +zha-quirks==0.0.38 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2188,13 +2188,13 @@ ziggo-mediabox-xl==1.1.0 zigpy-cc==0.3.1 # homeassistant.components.zha -zigpy-deconz==0.7.0 +zigpy-deconz==0.8.0 # homeassistant.components.zha -zigpy-homeassistant==0.16.0 +zigpy-homeassistant==0.18.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.10.0 +zigpy-xbee-homeassistant==0.11.0 # homeassistant.components.zha zigpy-zigate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84b177eb809cd..24986a574a17e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.14.0 +bellows-homeassistant==0.15.1 # homeassistant.components.bom bomradarloop==0.1.4 @@ -140,7 +140,7 @@ bomradarloop==0.1.4 broadlink==0.13.0 # homeassistant.components.brother -brother==0.1.9 +brother==0.1.11 # homeassistant.components.buienradar buienradar==1.0.4 @@ -264,7 +264,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.32.2 +hass-nabucasa==0.33.0 # homeassistant.components.mqtt hbmqtt==0.9.5 @@ -282,7 +282,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200330.0 +home-assistant-frontend==20200401.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -554,6 +554,9 @@ pymonoprice==0.3 # homeassistant.components.myq pymyq==2.0.1 +# homeassistant.components.nut +pynut2==2.1.2 + # homeassistant.components.nws pynws==0.10.4 @@ -665,7 +668,7 @@ rflink==0.0.52 ring_doorbell==0.6.0 # homeassistant.components.roku -roku==4.0.0 +roku==4.1.0 # homeassistant.components.yamaha rxv==0.6.0 @@ -795,19 +798,19 @@ yahooweather==0.10 zeroconf==0.24.5 # homeassistant.components.zha -zha-quirks==0.0.37 +zha-quirks==0.0.38 # homeassistant.components.zha zigpy-cc==0.3.1 # homeassistant.components.zha -zigpy-deconz==0.7.0 +zigpy-deconz==0.8.0 # homeassistant.components.zha -zigpy-homeassistant==0.16.0 +zigpy-homeassistant==0.18.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.10.0 +zigpy-xbee-homeassistant==0.11.0 # homeassistant.components.zha zigpy-zigate==0.5.1 diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index 6611cbaf9eb58..ec9c6bb1f3685 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -55,5 +55,5 @@ def player_fixture(hass, state): player = ArcamFmj(state, MOCK_NAME, None) player.entity_id = MOCK_ENTITY_ID player.hass = hass - player.async_schedule_update_ha_state = Mock() + player.async_write_ha_state = Mock() return player diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 2ff31a8fd4f1c..a6b36a71d1cf4 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -126,7 +126,7 @@ async def test_mute_volume(player, state, mute): """Test mute functionality.""" await player.async_mute_volume(mute) state.set_mute.assert_called_with(mute) - player.async_schedule_update_ha_state.assert_called_with() + player.async_write_ha_state.assert_called_with() async def test_name(player): @@ -203,14 +203,14 @@ async def test_volume_up(player, state): """Test mute functionality.""" await player.async_volume_up() state.inc_volume.assert_called_with() - player.async_schedule_update_ha_state.assert_called_with() + player.async_write_ha_state.assert_called_with() async def test_volume_down(player, state): """Test mute functionality.""" await player.async_volume_down() state.dec_volume.assert_called_with() - player.async_schedule_update_ha_state.assert_called_with() + player.async_write_ha_state.assert_called_with() @pytest.mark.parametrize( diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index 095b7b76d6037..b91b815d58e85 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -19,9 +19,7 @@ async def test_password_or_pub_key_required(hass): AsusWrt().connection.async_connect = mock_coro_func() AsusWrt().is_connected = False result = await async_setup_component( - hass, - DOMAIN, - {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}}, + hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} ) assert not result @@ -62,7 +60,7 @@ async def test_specify_non_directory_path_for_dnsmasq(hass): CONF_HOST: "fake_host", CONF_USERNAME: "fake_user", CONF_PASSWORD: "4321", - CONF_DNSMASQ: "?non_directory?", + CONF_DNSMASQ: 1234, } }, ) diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 24b0563890bf5..c4ad22abb2f29 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,24 +1,23 @@ """Tests for the cloud binary sensor.""" from unittest.mock import Mock +from asynctest import patch + from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE from homeassistant.setup import async_setup_component async def test_remote_connection_sensor(hass): """Test the remote connection sensor.""" - from homeassistant.components.cloud import binary_sensor as bin_sensor - - bin_sensor.WAIT_UNTIL_CHANGE = 0 - assert await async_setup_component(hass, "cloud", {"cloud": {}}) await hass.async_block_till_done() assert hass.states.get("binary_sensor.remote_ui") is None # Fake connection/discovery - org_cloud = hass.data["cloud"] - await org_cloud.iot._on_connect[-1]() + await hass.helpers.discovery.async_load_platform( + "binary_sensor", "cloud", {}, {"cloud": {}} + ) # Mock test env cloud = hass.data["cloud"] = Mock() @@ -29,17 +28,18 @@ async def test_remote_connection_sensor(hass): assert state is not None assert state.state == "unavailable" - cloud.remote.is_connected = False - cloud.remote.certificate = object() - hass.helpers.dispatcher.async_dispatcher_send(DISPATCHER_REMOTE_UPDATE, {}) - await hass.async_block_till_done() + with patch("homeassistant.components.cloud.binary_sensor.WAIT_UNTIL_CHANGE", 0): + cloud.remote.is_connected = False + cloud.remote.certificate = object() + hass.helpers.dispatcher.async_dispatcher_send(DISPATCHER_REMOTE_UPDATE, {}) + await hass.async_block_till_done() - state = hass.states.get("binary_sensor.remote_ui") - assert state.state == "off" + state = hass.states.get("binary_sensor.remote_ui") + assert state.state == "off" - cloud.remote.is_connected = True - hass.helpers.dispatcher.async_dispatcher_send(DISPATCHER_REMOTE_UPDATE, {}) - await hass.async_block_till_done() + cloud.remote.is_connected = True + hass.helpers.dispatcher.async_dispatcher_send(DISPATCHER_REMOTE_UPDATE, {}) + await hass.async_block_till_done() - state = hass.states.get("binary_sensor.remote_ui") - assert state.state == "on" + state = hass.states.get("binary_sensor.remote_ui") + assert state.state == "on" diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 9dd8695b9b219..10a7bc38c0572 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -6,7 +6,7 @@ from homeassistant.components import cloud from homeassistant.components.cloud.const import DOMAIN from homeassistant.components.cloud.prefs import STORAGE_KEY -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component @@ -103,12 +103,6 @@ async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): async def test_startup_shutdown_events(hass, mock_cloud_fixture): """Test if the cloud will start on startup event.""" - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()) as mock_start: - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - assert mock_start.called - with patch("hass_nabucasa.Cloud.stop", return_value=mock_coro()) as mock_stop: hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 6b9004595bb0b..2ec3467e0db2b 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -5,25 +5,6 @@ from homeassistant.setup import async_setup_component -from tests.common import MockDependency, mock_coro - - -@pytest.fixture(autouse=True) -def zeroconf_mock(): - """Mock zeroconf.""" - with MockDependency("zeroconf") as mocked_zeroconf: - mocked_zeroconf.Zeroconf.return_value.register_service.return_value = mock_coro( - True - ) - yield - - -@pytest.fixture(autouse=True) -def netdisco_mock(): - """Mock netdisco.""" - with MockDependency("netdisco", "discovery"): - yield - @pytest.fixture(autouse=True) def recorder_url_mock(): diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 49cd953a69796..fa7c4ac473dfc 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -1,11 +1,95 @@ """Test helpers for Hue.""" -from unittest.mock import patch +from collections import deque +from unittest.mock import Mock, patch +from aiohue.groups import Groups +from aiohue.lights import Lights +from aiohue.sensors import Sensors import pytest +from homeassistant import config_entries +from homeassistant.components import hue +from homeassistant.components.hue import sensor_base as hue_sensor_base + @pytest.fixture(autouse=True) def no_request_delay(): """Make the request refresh delay 0 for instant tests.""" with patch("homeassistant.components.hue.light.REQUEST_REFRESH_DELAY", 0): yield + + +def create_mock_bridge(hass): + """Create a mock Hue bridge.""" + bridge = Mock( + hass=hass, + available=True, + authorized=True, + allow_unreachable=False, + allow_groups=False, + api=Mock(), + reset_jobs=[], + spec=hue.HueBridge, + ) + bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) + bridge.mock_requests = [] + # We're using a deque so we can schedule multiple responses + # and also means that `popleft()` will blow up if we get more updates + # than expected. + bridge.mock_light_responses = deque() + bridge.mock_group_responses = deque() + bridge.mock_sensor_responses = deque() + + async def mock_request(method, path, **kwargs): + kwargs["method"] = method + kwargs["path"] = path + bridge.mock_requests.append(kwargs) + + if path == "lights": + return bridge.mock_light_responses.popleft() + if path == "groups": + return bridge.mock_group_responses.popleft() + if path == "sensors": + return bridge.mock_sensor_responses.popleft() + return None + + async def async_request_call(task): + await task() + + bridge.async_request_call = async_request_call + bridge.api.config.apiversion = "9.9.9" + bridge.api.lights = Lights({}, mock_request) + bridge.api.groups = Groups({}, mock_request) + bridge.api.sensors = Sensors({}, mock_request) + return bridge + + +@pytest.fixture +def mock_bridge(hass): + """Mock a Hue bridge.""" + return create_mock_bridge(hass) + + +async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None): + """Load the Hue platform with the provided bridge for sensor-related platforms.""" + if hostname is None: + hostname = "mock-host" + hass.config.components.add(hue.DOMAIN) + config_entry = config_entries.ConfigEntry( + 1, + hue.DOMAIN, + "Mock Title", + {"host": hostname}, + "test", + config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + ) + mock_bridge.config_entry = config_entry + hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} + await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") + await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + # simulate a full setup by manually adding the bridge config entry + hass.config_entries._entries.append(config_entry) + + # and make sure it completes before going further + await hass.async_block_till_done() diff --git a/tests/components/hue/test_device_trigger.py b/tests/components/hue/test_device_trigger.py new file mode 100644 index 0000000000000..b6d3f4f2f5095 --- /dev/null +++ b/tests/components/hue/test_device_trigger.py @@ -0,0 +1,169 @@ +"""The tests for Philips Hue device triggers.""" +import pytest + +from homeassistant.components import hue +import homeassistant.components.automation as automation +from homeassistant.components.hue import device_trigger +from homeassistant.setup import async_setup_component + +from .conftest import setup_bridge_for_sensors as setup_bridge +from .test_sensor_base import HUE_DIMMER_REMOTE_1, HUE_TAP_REMOTE_1 + +from tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, +) + +REMOTES_RESPONSE = {"7": HUE_TAP_REMOTE_1, "8": HUE_DIMMER_REMOTE_1} + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, mock_bridge, device_reg): + """Test we get the expected triggers from a hue remote.""" + mock_bridge.mock_sensor_responses.append(REMOTES_RESPONSE) + await setup_bridge(hass, mock_bridge) + + assert len(mock_bridge.mock_requests) == 1 + # 2 remotes, just 1 battery sensor + assert len(hass.states.async_all()) == 1 + + # Get triggers for specific tap switch + hue_tap_device = device_reg.async_get_device( + {(hue.DOMAIN, "00:00:00:00:00:44:23:08")}, connections={} + ) + triggers = await async_get_device_automations(hass, "trigger", hue_tap_device.id) + + expected_triggers = [ + { + "platform": "device", + "domain": hue.DOMAIN, + "device_id": hue_tap_device.id, + "type": t_type, + "subtype": t_subtype, + } + for t_type, t_subtype in device_trigger.HUE_TAP_REMOTE.keys() + ] + assert_lists_same(triggers, expected_triggers) + + # Get triggers for specific dimmer switch + hue_dimmer_device = device_reg.async_get_device( + {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")}, connections={} + ) + triggers = await async_get_device_automations(hass, "trigger", hue_dimmer_device.id) + + trigger_batt = { + "platform": "device", + "domain": "sensor", + "device_id": hue_dimmer_device.id, + "type": "battery_level", + "entity_id": "sensor.hue_dimmer_switch_1_battery_level", + } + expected_triggers = [ + trigger_batt, + *[ + { + "platform": "device", + "domain": hue.DOMAIN, + "device_id": hue_dimmer_device.id, + "type": t_type, + "subtype": t_subtype, + } + for t_type, t_subtype in device_trigger.HUE_DIMMER_REMOTE.keys() + ], + ] + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, mock_bridge, device_reg, calls): + """Test for button press trigger firing.""" + mock_bridge.mock_sensor_responses.append(REMOTES_RESPONSE) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 1 + + # Set an automation with a specific tap switch trigger + hue_tap_device = device_reg.async_get_device( + {(hue.DOMAIN, "00:00:00:00:00:44:23:08")}, connections={} + ) + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": hue.DOMAIN, + "device_id": hue_tap_device.id, + "type": "remote_button_short_press", + "subtype": "button_4", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "B4 - {{ trigger.event.data.event }}" + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": hue.DOMAIN, + "device_id": "mock-device-id", + "type": "remote_button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "B1 - {{ trigger.event.data.event }}" + }, + }, + }, + ] + }, + ) + + # Fake that the remote is being pressed. + new_sensor_response = dict(REMOTES_RESPONSE) + new_sensor_response["7"]["state"] = { + "buttonevent": 18, + "lastupdated": "2019-12-28T22:58:02", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 2 + + assert len(calls) == 1 + assert calls[0].data["some"] == "B4 - 18" + + # Fake another button press. + new_sensor_response = dict(REMOTES_RESPONSE) + new_sensor_response["7"]["state"] = { + "buttonevent": 34, + "lastupdated": "2019-12-28T22:58:05", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + assert len(mock_bridge.mock_requests) == 3 + assert len(calls) == 1 diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index a99b947e48e92..998e3cdea5033 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -1,13 +1,9 @@ """Philips Hue lights platform tests.""" import asyncio -from collections import deque import logging from unittest.mock import Mock import aiohue -from aiohue.groups import Groups -from aiohue.lights import Lights -import pytest from homeassistant import config_entries from homeassistant.components import hue @@ -175,48 +171,6 @@ LIGHT_GAMUT_TYPE = "A" -@pytest.fixture -def mock_bridge(hass): - """Mock a Hue bridge.""" - bridge = Mock( - hass=hass, - available=True, - authorized=True, - allow_unreachable=False, - allow_groups=False, - api=Mock(), - reset_jobs=[], - spec=hue.HueBridge, - ) - bridge.mock_requests = [] - # We're using a deque so we can schedule multiple responses - # and also means that `popleft()` will blow up if we get more updates - # than expected. - bridge.mock_light_responses = deque() - bridge.mock_group_responses = deque() - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - bridge.mock_requests.append(kwargs) - - if path == "lights": - return bridge.mock_light_responses.popleft() - if path == "groups": - return bridge.mock_group_responses.popleft() - return None - - async def async_request_call(task): - await task() - - bridge.async_request_call = async_request_call - bridge.api.config.apiversion = "9.9.9" - bridge.api.lights = Lights({}, mock_request) - bridge.api.groups = Groups({}, mock_request) - - return bridge - - async def setup_bridge(hass, mock_bridge): """Load the Hue light platform with the provided bridge.""" hass.config.components.add(hue.DOMAIN) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index cf1a4ab798378..576bc365d508b 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -1,18 +1,14 @@ """Philips Hue sensors platform tests.""" import asyncio -from collections import deque import logging from unittest.mock import Mock import aiohue -from aiohue.sensors import Sensors -import pytest -from homeassistant import config_entries -from homeassistant.components import hue -from homeassistant.components.hue import sensor_base as hue_sensor_base from homeassistant.components.hue.hue_event import CONF_HUE_EVENT +from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge + _LOGGER = logging.getLogger(__name__) PRESENCE_SENSOR_1_PRESENT = { @@ -281,71 +277,6 @@ } -def create_mock_bridge(hass): - """Create a mock Hue bridge.""" - bridge = Mock( - hass=hass, - available=True, - authorized=True, - allow_unreachable=False, - allow_groups=False, - api=Mock(), - reset_jobs=[], - spec=hue.HueBridge, - ) - bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) - bridge.mock_requests = [] - # We're using a deque so we can schedule multiple responses - # and also means that `popleft()` will blow up if we get more updates - # than expected. - bridge.mock_sensor_responses = deque() - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - bridge.mock_requests.append(kwargs) - - if path == "sensors": - return bridge.mock_sensor_responses.popleft() - return None - - async def async_request_call(task): - await task() - - bridge.async_request_call = async_request_call - bridge.api.config.apiversion = "9.9.9" - bridge.api.sensors = Sensors({}, mock_request) - return bridge - - -@pytest.fixture -def mock_bridge(hass): - """Mock a Hue bridge.""" - return create_mock_bridge(hass) - - -async def setup_bridge(hass, mock_bridge, hostname=None): - """Load the Hue platform with the provided bridge.""" - if hostname is None: - hostname = "mock-host" - hass.config.components.add(hue.DOMAIN) - config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": hostname}, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, - ) - mock_bridge.config_entry = config_entry - hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} - await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") - await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - # and make sure it completes before going further - await hass.async_block_till_done() - - async def test_no_sensors(hass, mock_bridge): """Test the update_items function when no sensors are found.""" mock_bridge.allow_groups = True diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 505ba61850535..5a2744eac516d 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -2,12 +2,15 @@ import aiohttp from pyipp import IPPConnectionUpgradeRequired -from homeassistant import data_entry_flow -from homeassistant.components.ipp import config_flow -from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID +from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from . import ( MOCK_USER_INPUT, @@ -23,25 +26,11 @@ async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None: - """Test that the zeroconf confirmation form is served.""" - flow = config_flow.IPPFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - flow.discovery_info = {CONF_NAME: "EPSON123456"} - - result = await flow.async_step_zeroconf_confirm() - - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} + assert result["type"] == RESULT_TYPE_FORM async def test_show_zeroconf_form( @@ -54,18 +43,13 @@ async def test_show_zeroconf_form( headers={"Content-Type": "application/ipp"}, ) - flow = config_flow.IPPFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() - result = await flow.async_step_zeroconf(discovery_info) - - assert flow.discovery_info[CONF_HOST] == "EPSON123456.local" - assert flow.discovery_info[CONF_NAME] == "EPSON123456" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} @@ -79,11 +63,11 @@ async def test_connection_error( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, context={"source": SOURCE_USER}, data=user_input, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "connection_error"} @@ -95,10 +79,10 @@ async def test_zeroconf_connection_error( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "connection_error" @@ -110,7 +94,7 @@ async def test_zeroconf_confirm_connection_error( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={ "source": SOURCE_ZEROCONF, CONF_HOST: "EPSON123456.local", @@ -119,7 +103,7 @@ async def test_zeroconf_confirm_connection_error( data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "connection_error" @@ -133,11 +117,11 @@ async def test_user_connection_upgrade_required( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, context={"source": SOURCE_USER}, data=user_input, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "connection_upgrade"} @@ -151,10 +135,10 @@ async def test_zeroconf_connection_upgrade_required( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "connection_upgrade" @@ -166,10 +150,10 @@ async def test_user_device_exists_abort( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, context={"source": SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -181,10 +165,10 @@ async def test_zeroconf_device_exists_abort( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -197,10 +181,10 @@ async def test_zeroconf_with_uuid_device_exists_abort( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() discovery_info["properties"]["UUID"] = "cfe92100-67c4-11d4-a45f-f8d027761251" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -215,18 +199,18 @@ async def test_full_user_flow_implementation( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "EPSON123456.local", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "EPSON123456.local" assert result["data"] @@ -244,25 +228,19 @@ async def test_full_zeroconf_flow_implementation( headers={"Content-Type": "application/ipp"}, ) - flow = config_flow.IPPFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() - result = await flow.async_step_zeroconf(discovery_info) - - assert flow.discovery_info - assert flow.discovery_info[CONF_HOST] == "EPSON123456.local" - assert flow.discovery_info[CONF_NAME] == "EPSON123456" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM - result = await flow.async_step_zeroconf_confirm( - user_input={CONF_HOST: "EPSON123456.local"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "EPSON123456" assert result["data"] @@ -281,22 +259,20 @@ async def test_full_zeroconf_tls_flow_implementation( headers={"Content-Type": "application/ipp"}, ) - flow = config_flow.IPPFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - discovery_info = MOCK_ZEROCONF_IPPS_SERVICE_INFO.copy() - result = await flow.async_step_zeroconf(discovery_info) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} - result = await flow.async_step_zeroconf_confirm( - user_input={CONF_HOST: "EPSON123456.local"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "EPSON123456" assert result["data"] diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 45c123fa2fe49..ba1855247fe45 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -21,6 +21,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -494,3 +495,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index a73919844c1c1..07a1c8b6e5f97 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -23,6 +23,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -508,3 +509,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 21f552b4163fd..cefeda0409786 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -13,6 +13,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -207,3 +208,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, "test_topic", b"ON" + ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index ce21aa53d2759..d5b303a51f07d 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -33,6 +33,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -908,6 +909,20 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): ) +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + config = { + CLIMATE_DOMAIN: { + "platform": "mqtt", + "name": "test", + "mode_state_topic": "test-topic", + } + } + await help_test_entity_debug_info_message( + hass, mqtt_mock, CLIMATE_DOMAIN, config, "test-topic" + ) + + async def test_precision_default(hass, mqtt_mock): """Test that setting precision to tenths works as intended.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 2f4371742993c..0d1b892611d2b 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -4,6 +4,7 @@ from unittest.mock import ANY from homeassistant.components import mqtt +from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE @@ -519,3 +520,232 @@ async def help_test_entity_id_update_discovery_update( async_fire_mqtt_message(hass, f"{topic}_2", "online") state = hass.states.get(f"{domain}.milk") assert state.state != STATE_UNAVAILABLE + + +async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + + +async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, config): + """Test debug_info message overflow. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + + for i in range(0, debug_info.STORED_MESSAGES + 1): + async_fire_mqtt_message(hass, "test-topic", f"{i}") + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert ( + len(debug_info_data["entities"][0]["topics"][0]["messages"]) + == debug_info.STORED_MESSAGES + ) + messages = [f"{i}" for i in range(1, debug_info.STORED_MESSAGES + 1)] + assert {"topic": "test-topic", "messages": messages} in debug_info_data["entities"][ + 0 + ]["topics"] + + +async def help_test_entity_debug_info_message( + hass, mqtt_mock, domain, config, topic=None, payload=None +): + """Test debug_info message overflow. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + if topic is None: + # Add default topic to config + config["state_topic"] = "state-topic" + topic = "state-topic" + + if payload is None: + payload = "ON" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) >= 1 + assert {"topic": topic, "messages": []} in debug_info_data["entities"][0]["topics"] + + async_fire_mqtt_message(hass, topic, payload) + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) >= 1 + assert {"topic": topic, "messages": [payload]} in debug_info_data["entities"][0][ + "topics" + ] + + +async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + entity_id = debug_info_data["entities"][0]["entity_id"] + + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") + await hass.async_block_till_done() + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 0 + assert entity_id not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] + + +async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, config): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + dev_registry = await hass.helpers.device_registry.async_get_registry() + ent_registry = mock_registry(hass, {}) + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + + ent_registry.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk") + await hass.async_block_till_done() + await hass.async_block_till_done() + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.milk" + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + assert ( + f"{domain}.test" not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] + ) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 7749c419ca035..7d0753a9de2e7 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -30,6 +30,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -1762,3 +1763,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 6766002717dae..7274badbed94d 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -4,7 +4,7 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.mqtt import DOMAIN +from homeassistant.components.mqtt import DOMAIN, debug_info from homeassistant.components.mqtt.device_trigger import async_attach_trigger from homeassistant.components.mqtt.discovery import async_start from homeassistant.setup import async_setup_component @@ -1104,3 +1104,44 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo # Verify device registry entry is cleared device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) assert device_entry is None + + +async def test_trigger_debug_info(hass, mqtt_mock): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + "platform": "mqtt", + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": { + "connections": [["mac", "02:5b:26:a8:dc:12"]], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", + }, + } + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")}) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 1 + assert ( + debug_info_data["triggers"][0]["discovery_data"]["topic"] + == "homeassistant/device_automation/bla/config" + ) + assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 460c99618bd48..6b774e9d5ce4b 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -11,6 +11,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -509,3 +510,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7d06c62b915a3..7aa185c2c391e 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,5 +1,6 @@ """The tests for the MQTT component.""" from datetime import timedelta +import json import ssl import unittest from unittest import mock @@ -934,3 +935,48 @@ async def test_mqtt_ws_remove_non_mqtt_device( response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND + + +async def test_mqtt_ws_get_device_debug_info( + hass, device_reg, hass_ws_client, mqtt_mock +): + """Test MQTT websocket device debug info.""" + config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + config = { + "device": {"identifiers": ["0AFFD2"]}, + "platform": "mqtt", + "state_topic": "foobar/sensor", + "unique_id": "unique", + } + data = json.dumps(config) + + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + # Verify device entry is created + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_entry is not None + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 5, "type": "mqtt/device/debug_info", "device_id": device_entry.id} + ) + response = await client.receive_json() + assert response["success"] + expected_result = { + "entities": [ + { + "entity_id": "sensor.mqtt_sensor", + "topics": [{"topic": "foobar/sensor", "messages": []}], + "discovery_data": { + "payload": config, + "topic": "homeassistant/sensor/bla/config", + }, + } + ], + "triggers": [], + } + assert response["result"] == expected_result diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 14ab79b2d202a..0ddb661cf8575 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -27,6 +27,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -629,3 +630,20 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + config = { + vacuum.DOMAIN: { + "platform": "mqtt", + "name": "test", + "battery_level_topic": "test-topic", + "battery_level_template": "{{ value_json.battery_level }}", + "command_topic": "command-topic", + "availability_topic": "avty-topic", + } + } + await help_test_entity_debug_info_message( + hass, mqtt_mock, vacuum.DOMAIN, config, "test-topic" + ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index bc4f5fc339394..45473d6f448e9 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -170,6 +170,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -1358,3 +1359,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index f71791e019fc0..1223f3525c4ee 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -109,6 +109,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -1134,3 +1135,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index c9612a7ded787..37617192dd568 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -46,6 +46,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -957,3 +958,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 151021a45f802..803fd6c3ab388 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -11,6 +11,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -399,3 +400,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 061e53250cb2b..34d3c33f8d72f 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -19,6 +19,11 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info, + help_test_entity_debug_info_max_messages, + help_test_entity_debug_info_message, + help_test_entity_debug_info_remove, + help_test_entity_debug_info_update_entity_id, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -437,3 +442,36 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock): device = registry.async_get_device({("mqtt", "helloworld")}, set()) assert device is not None assert device.via_device_id == hub.id + + +async def test_entity_debug_info(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + + +async def test_entity_debug_info_max_messages(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info_max_messages( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_remove(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info_remove( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_update_entity_id(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info_update_entity_id( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index ecb38ef5774ea..367b9ceda8a8d 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -38,6 +38,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -454,3 +455,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index d8ca803139011..1aaeb154dc250 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -15,6 +15,7 @@ help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -366,3 +367,10 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): await help_test_entity_id_update_discovery_update( hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/nut/__init__.py b/tests/components/nut/__init__.py new file mode 100644 index 0000000000000..61ddfb4c07a08 --- /dev/null +++ b/tests/components/nut/__init__.py @@ -0,0 +1 @@ +"""Tests for the Network UPS Tools (NUT) integration.""" diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py new file mode 100644 index 0000000000000..362f6c0b2ba2e --- /dev/null +++ b/tests/components/nut/test_config_flow.py @@ -0,0 +1,122 @@ +"""Test the Network UPS Tools (NUT) config flow.""" +from asynctest import MagicMock, patch + +from homeassistant import config_entries, setup +from homeassistant.components.nut.const import DOMAIN + + +def _get_mock_pynutclient(list_vars=None): + pynutclient = MagicMock() + type(pynutclient).list_ups = MagicMock(return_value=["ups1"]) + type(pynutclient).list_vars = MagicMock(return_value=list_vars) + return pynutclient + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_pynut = _get_mock_pynutclient(list_vars={"battery.voltage": "voltage"}) + + with patch( + "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + ), patch( + "homeassistant.components.nut.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nut.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "port": 2222, + "alias": "ups1", + "resources": ["battery.charge"], + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "ups1@1.1.1.1:2222" + assert result2["data"] == { + "alias": "ups1", + "host": "1.1.1.1", + "name": "NUT UPS", + "password": "test-password", + "port": 2222, + "resources": ["battery.charge"], + "username": "test-username", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_pynut = _get_mock_pynutclient(list_vars={"battery.voltage": "serial"}) + + with patch( + "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + ), patch( + "homeassistant.components.nut.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nut.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": "localhost", + "port": 123, + "name": "name", + "resources": ["battery.charge"], + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "localhost:123" + assert result["data"] == { + "host": "localhost", + "port": 123, + "name": "name", + "resources": ["battery.charge"], + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_pynut = _get_mock_pynutclient() + + with patch( + "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "port": 2222, + "alias": "ups1", + "resources": ["battery.charge"], + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 21bb5bcf9939f..ae999afe30568 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -16,7 +16,7 @@ BASE_URL = "http://example.com" CLOUDHOOK = False -SECRET = "secret" +SECRET = "test-secret" WEBHOOK_ID = "webhook_id" WEBHOOK_URL = f"{BASE_URL}/api/webhook/webhook_id" @@ -33,7 +33,7 @@ def mock_webhook_id(): @pytest.fixture(name="secret") def mock_secret(): """Mock secret.""" - with patch("binascii.hexlify", return_value=str.encode(SECRET)): + with patch("secrets.token_hex", return_value=SECRET): yield diff --git a/tests/fixtures/brother_printer_data.json b/tests/fixtures/brother_printer_data.json index f4c36d988b1e5..a70d87673d0e1 100644 --- a/tests/fixtures/brother_printer_data.json +++ b/tests/fixtures/brother_printer_data.json @@ -71,5 +71,6 @@ "a60100a70100a0" ], "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.1.0": "0123456789", - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.4.5.2.0": "WAITING " + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.4.5.2.0": "WAITING ", + "1.3.6.1.2.1.43.7.1.1.4.1.1": "2004" } \ No newline at end of file diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 199284c680bc2..df247d82d5c2b 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -168,7 +168,7 @@ async def test_adding_entities_with_generator_and_thread_callback(hass): def create_entity(number): """Create entity helper.""" - entity = MockEntity() + entity = MockEntity(unique_id=f"unique{number}") entity.entity_id = async_generate_entity_id(DOMAIN + ".{}", "Number", hass=hass) return entity diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 26170ac2b8621..428de1a683c13 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -4,7 +4,6 @@ ("tests.components.cast.test_media_player", "test_entry_setup_single_config"), ("tests.components.cast.test_media_player", "test_entry_setup_list_config"), ("tests.components.cast.test_media_player", "test_entry_setup_platform_not_ready"), - ("tests.components.config.test_automation", "test_delete_automation"), ("tests.components.config.test_group", "test_update_device_config"), ("tests.components.default_config.test_init", "test_setup"), ("tests.components.demo.test_init", "test_setting_up_demo"), @@ -46,20 +45,9 @@ ("tests.components.dyson.test_fan", "test_purecool_update_state_filter_inv"), ("tests.components.dyson.test_fan", "test_purecool_component_setup_only_once"), ("tests.components.dyson.test_sensor", "test_purecool_component_setup_only_once"), - ("test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup"), ("tests.components.ios.test_init", "test_creating_entry_sets_up_sensor"), ("tests.components.ios.test_init", "test_not_configuring_ios_not_creates_entry"), ("tests.components.local_file.test_camera", "test_file_not_readable"), - ("tests.components.meteo_france.test_config_flow", "test_user"), - ("tests.components.meteo_france.test_config_flow", "test_import"), - ("tests.components.mikrotik.test_device_tracker", "test_restoring_devices"), - ("tests.components.mikrotik.test_hub", "test_arp_ping"), - ("tests.components.mqtt.test_alarm_control_panel", "test_unique_id"), - ("tests.components.mqtt.test_binary_sensor", "test_unique_id"), - ("tests.components.mqtt.test_camera", "test_unique_id"), - ("tests.components.mqtt.test_climate", "test_unique_id"), - ("tests.components.mqtt.test_cover", "test_unique_id"), - ("tests.components.mqtt.test_fan", "test_unique_id"), ( "tests.components.mqtt.test_init", "test_setup_uses_certificate_on_certificate_set_to_auto", @@ -80,22 +68,14 @@ "tests.components.mqtt.test_init", "test_setup_with_tls_config_of_v1_under_python36_only_uses_v1", ), - ("tests.components.mqtt.test_legacy_vacuum", "test_unique_id"), - ("tests.components.mqtt.test_light", "test_unique_id"), ("tests.components.mqtt.test_light", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_light_json", "test_unique_id"), ("tests.components.mqtt.test_light_json", "test_entity_device_info_remove"), ("tests.components.mqtt.test_light_template", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_lock", "test_unique_id"), - ("tests.components.mqtt.test_sensor", "test_unique_id"), - ("tests.components.mqtt.test_state_vacuum", "test_unique_id"), - ("tests.components.mqtt.test_switch", "test_unique_id"), ("tests.components.mqtt.test_switch", "test_entity_device_info_remove"), ("tests.components.qwikswitch.test_init", "test_binary_sensor_device"), ("tests.components.qwikswitch.test_init", "test_sensor_device"), ("tests.components.rflink.test_init", "test_send_command_invalid_arguments"), ("tests.components.samsungtv.test_media_player", "test_update_connection_failure"), - ("tests.components.tplink.test_init", "test_configuring_device_types"), ( "tests.components.tplink.test_init", "test_configuring_devices_from_multiple_sources", @@ -108,18 +88,8 @@ ("tests.components.unifi_direct.test_device_tracker", "test_get_scanner"), ("tests.components.upnp.test_init", "test_async_setup_entry_default"), ("tests.components.upnp.test_init", "test_async_setup_entry_port_mapping"), - ("tests.components.vera.test_init", "test_init"), - ("tests.components.wunderground.test_sensor", "test_fails_because_of_unique_id"), ("tests.components.yr.test_sensor", "test_default_setup"), ("tests.components.yr.test_sensor", "test_custom_setup"), ("tests.components.yr.test_sensor", "test_forecast_setup"), ("tests.components.zwave.test_init", "test_power_schemes"), - ( - "tests.helpers.test_entity_platform", - "test_adding_entities_with_generator_and_thread_callback", - ), - ( - "tests.helpers.test_entity_platform", - "test_not_adding_duplicate_entities_with_unique_id", - ), ] diff --git a/tests/util/test_network.py b/tests/util/test_network.py new file mode 100644 index 0000000000000..c4c33c8d1871d --- /dev/null +++ b/tests/util/test_network.py @@ -0,0 +1,40 @@ +"""Test Home Assistant volume utility functions.""" + +from ipaddress import ip_address + +import homeassistant.util.network as network_util + + +def test_is_loopback(): + """Test loopback addresses.""" + assert network_util.is_loopback(ip_address("127.0.0.2")) + assert network_util.is_loopback(ip_address("127.0.0.1")) + assert network_util.is_loopback(ip_address("::1")) + assert network_util.is_loopback(ip_address("::ffff:127.0.0.0")) + assert network_util.is_loopback(ip_address("0:0:0:0:0:0:0:1")) + assert network_util.is_loopback(ip_address("0:0:0:0:0:ffff:7f00:1")) + assert not network_util.is_loopback(ip_address("104.26.5.238")) + assert not network_util.is_loopback(ip_address("2600:1404:400:1a4::356e")) + + +def test_is_private(): + """Test private addresses.""" + assert network_util.is_private(ip_address("192.168.0.1")) + assert network_util.is_private(ip_address("172.16.12.0")) + assert network_util.is_private(ip_address("10.5.43.3")) + assert network_util.is_private(ip_address("fd12:3456:789a:1::1")) + assert not network_util.is_private(ip_address("127.0.0.1")) + assert not network_util.is_private(ip_address("::1")) + + +def test_is_link_local(): + """Test link local addresses.""" + assert network_util.is_link_local(ip_address("169.254.12.3")) + assert not network_util.is_link_local(ip_address("127.0.0.1")) + + +def test_is_local(): + """Test local addresses.""" + assert network_util.is_local(ip_address("192.168.0.1")) + assert network_util.is_local(ip_address("127.0.0.1")) + assert not network_util.is_local(ip_address("208.5.4.2"))