forked from home-assistant/core
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14af3661f3 | |||
| af733425c2 | |||
| 4fc89e8861 | |||
| bcec268c04 | |||
| becf9fcce2 | |||
| ad9e0ef8e4 | |||
| f58eafe8fc | |||
| a7246400b3 | |||
| 38a30b343d | |||
| 08a0eaf184 | |||
| 3ee8f6edba | |||
| e866417c01 | |||
| 05c63eb884 | |||
| bb52bfd73d | |||
| 7319492bf3 | |||
| 66932e3d9a | |||
| 0ec07001bd | |||
| 0dcfd38cdc | |||
| b45eff9a2b | |||
| ec577c7bd3 | |||
| 723c4a1eb5 | |||
| b30b4d5a3a | |||
| 8165acddeb | |||
| 0f3ed3bb67 | |||
| d1a96ef362 | |||
| 917eeba984 | |||
| 59bb8b360e | |||
| 6028e5b77a | |||
| 83df470307 | |||
| 20ac0aa7b1 | |||
| f57c942901 | |||
| 8994ab1686 | |||
| b350ba9657 | |||
| 5fd589053a | |||
| 2d5961fa4f | |||
| d7a59748cf | |||
| cada78496b | |||
| c5fa9ad272 | |||
| fe8b5656dd | |||
| 0ae11b0335 | |||
| 76780ca04e | |||
| 3932ce57b9 | |||
| 35d145d3bc | |||
| 1227d56aa2 | |||
| ef3ecb6183 | |||
| ca515f740e | |||
| 876fb234ce | |||
| f28cbf1909 | |||
| 9b5d0f72dc | |||
| 23056f839b | |||
| 0b8dd738f1 | |||
| 411633d3b3 | |||
| f3ab3bd5cb | |||
| 476b9909ac | |||
| e756328d52 | |||
| b9c9921847 | |||
| 09dbd8e7eb | |||
| 07dd832c58 | |||
| f9c5661c66 | |||
| 94f8f8281f | |||
| f6aa25c717 | |||
| f9ca85735d | |||
| be086c581c | |||
| 03d198dd64 | |||
| a8d6866f9f | |||
| 0e1dc9878f | |||
| 6849597764 | |||
| 3022d3bfa0 | |||
| 4836d6620b | |||
| b290e95350 | |||
| 89ac3ce832 | |||
| 1933454b76 | |||
| 38601d48ff | |||
| 7256f23376 | |||
| 7519603bf5 | |||
| ef47daad9d | |||
| 18d283bed6 | |||
| 210e906a4d | |||
| dcffd6bd7a | |||
| 2c2261254b | |||
| 53e49861a1 | |||
| 3da8d0a741 | |||
| 0701b0daa9 | |||
| bea6fe30b8 | |||
| 7d5d81b229 | |||
| 242b3fa609 | |||
| 74204e2ee6 | |||
| da01635a07 | |||
| b5c34808e6 | |||
| 80e70993c8 | |||
| 2e01e169ef | |||
| b35442ed2d | |||
| 1b45069620 | |||
| d3d0e05817 | |||
| 3d164c6721 | |||
| 6dd1e09354 | |||
| ba456f2564 | |||
| 3492171ff8 |
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["adguardhome"],
|
||||
"requirements": ["adguardhome==0.6.3"]
|
||||
"requirements": ["adguardhome==0.7.0"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,34 @@
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"led_bar_brightness": {
|
||||
"default": "mdi:brightness-percent"
|
||||
},
|
||||
"display_brightness": {
|
||||
"default": "mdi:brightness-percent"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"configuration_control": {
|
||||
"default": "mdi:cloud-cog"
|
||||
},
|
||||
"display_temperature_unit": {
|
||||
"default": "mdi:thermometer-lines"
|
||||
},
|
||||
"led_bar_mode": {
|
||||
"default": "mdi:led-strip"
|
||||
},
|
||||
"nox_index_learning_time_offset": {
|
||||
"default": "mdi:clock-outline"
|
||||
},
|
||||
"voc_index_learning_time_offset": {
|
||||
"default": "mdi:clock-outline"
|
||||
},
|
||||
"co2_automatic_baseline_calibration": {
|
||||
"default": "mdi:molecule-co2"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"total_volatile_organic_component_index": {
|
||||
"default": "mdi:molecule"
|
||||
@@ -17,6 +45,32 @@
|
||||
},
|
||||
"pm003_count": {
|
||||
"default": "mdi:blur"
|
||||
},
|
||||
"led_bar_brightness": {
|
||||
"default": "mdi:brightness-percent"
|
||||
},
|
||||
"display_brightness": {
|
||||
"default": "mdi:brightness-percent"
|
||||
},
|
||||
"display_temperature_unit": {
|
||||
"default": "mdi:thermometer-lines"
|
||||
},
|
||||
"led_bar_mode": {
|
||||
"default": "mdi:led-strip"
|
||||
},
|
||||
"nox_index_learning_time_offset": {
|
||||
"default": "mdi:clock-outline"
|
||||
},
|
||||
"voc_index_learning_time_offset": {
|
||||
"default": "mdi:clock-outline"
|
||||
},
|
||||
"co2_automatic_baseline_calibration": {
|
||||
"default": "mdi:molecule-co2"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"post_data_to_airgradient": {
|
||||
"default": "mdi:cogs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ LEARNING_TIME_OFFSET_OPTIONS = [
|
||||
]
|
||||
|
||||
ABC_DAYS = [
|
||||
"1",
|
||||
"8",
|
||||
"30",
|
||||
"90",
|
||||
|
||||
@@ -91,8 +91,9 @@
|
||||
}
|
||||
},
|
||||
"co2_automatic_baseline_calibration": {
|
||||
"name": "CO2 automatic baseline calibration",
|
||||
"name": "CO2 automatic baseline duration",
|
||||
"state": {
|
||||
"1": "1 day",
|
||||
"8": "8 days",
|
||||
"30": "30 days",
|
||||
"90": "90 days",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["airtouch5py"],
|
||||
"requirements": ["airtouch5py==0.2.8"]
|
||||
"requirements": ["airtouch5py==0.2.10"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anova",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["anova_wifi"],
|
||||
"requirements": ["anova-wifi==0.12.0"]
|
||||
"requirements": ["anova-wifi==0.14.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.7.0"]
|
||||
"requirements": ["pyaprilaire==0.7.4"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["apsystems-ez1==1.3.1"]
|
||||
"requirements": ["apsystems-ez1==1.3.3"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/arve",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["asyncarve==0.0.9"]
|
||||
"requirements": ["asyncarve==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -316,7 +316,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
@callback
|
||||
def _async_update_playback_error(self, data: PlaybackError) -> None:
|
||||
"""Show playback error."""
|
||||
_LOGGER.error(data.error)
|
||||
raise HomeAssistantError(data.error)
|
||||
|
||||
@callback
|
||||
def _async_update_playback_progress(self, data: PlaybackProgress) -> None:
|
||||
@@ -516,7 +516,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
|
||||
self.async_write_ha_state()
|
||||
else:
|
||||
_LOGGER.error("Seeking is currently only supported when using Deezer")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="non_deezer_seeking"
|
||||
)
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send the previous track command."""
|
||||
@@ -529,12 +531,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select an input source."""
|
||||
if source not in self._sources.values():
|
||||
_LOGGER.error(
|
||||
"Invalid source: %s. Valid sources are: %s",
|
||||
source,
|
||||
list(self._sources.values()),
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_source",
|
||||
translation_placeholders={
|
||||
"invalid_source": source,
|
||||
"valid_sources": ",".join(list(self._sources.values())),
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
key = [x for x in self._sources if self._sources[x] == source][0]
|
||||
|
||||
@@ -559,12 +563,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
media_type = MediaType.MUSIC
|
||||
|
||||
if media_type not in VALID_MEDIA_TYPES:
|
||||
_LOGGER.error(
|
||||
"%s is an invalid type. Valid values are: %s",
|
||||
media_type,
|
||||
VALID_MEDIA_TYPES,
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_media_type",
|
||||
translation_placeholders={
|
||||
"invalid_media_type": media_type,
|
||||
"valid_media_types": ",".join(VALID_MEDIA_TYPES),
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
if media_source.is_media_source_id(media_id):
|
||||
sourced_media = await media_source.async_resolve_media(
|
||||
@@ -681,7 +687,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
)
|
||||
|
||||
except ApiException as error:
|
||||
_LOGGER.error(json.loads(error.body)["message"])
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="play_media_error",
|
||||
translation_placeholders={
|
||||
"media_type": media_type,
|
||||
"error_message": json.loads(error.body)["message"],
|
||||
},
|
||||
) from error
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
|
||||
@@ -28,6 +28,18 @@
|
||||
"exceptions": {
|
||||
"m3u_invalid_format": {
|
||||
"message": "Media sources with the .m3u extension are not supported."
|
||||
},
|
||||
"non_deezer_seeking": {
|
||||
"message": "Seeking is currently only supported when using Deezer"
|
||||
},
|
||||
"invalid_source": {
|
||||
"message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}"
|
||||
},
|
||||
"invalid_media_type": {
|
||||
"message": "{invalid_media_type} is an invalid type. Valid values are: {valid_media_types}."
|
||||
},
|
||||
"play_media_error": {
|
||||
"message": "An error occurred while attempting to play {media_type}: {error_message}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.21"]
|
||||
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"]
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pizzapi import Address, Customer, Order
|
||||
from pizzapi.address import StoreException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import http
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -118,7 +118,7 @@ class Dominos:
|
||||
self.country = conf.get(ATTR_COUNTRY)
|
||||
try:
|
||||
self.closest_store = self.address.closest_store()
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self.closest_store = None
|
||||
|
||||
def handle_order(self, call: ServiceCall) -> None:
|
||||
@@ -139,7 +139,7 @@ class Dominos:
|
||||
"""Update the shared closest store (if open)."""
|
||||
try:
|
||||
self.closest_store = self.address.closest_store()
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self.closest_store = None
|
||||
return False
|
||||
return True
|
||||
@@ -219,7 +219,7 @@ class DominosOrder(Entity):
|
||||
"""Update the order state and refreshes the store."""
|
||||
try:
|
||||
self.dominos.update_closest_store()
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self._orderable = False
|
||||
return
|
||||
|
||||
@@ -227,13 +227,13 @@ class DominosOrder(Entity):
|
||||
order = self.order()
|
||||
order.pay_with()
|
||||
self._orderable = True
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self._orderable = False
|
||||
|
||||
def order(self):
|
||||
"""Create the order object."""
|
||||
if self.dominos.closest_store is None:
|
||||
raise StoreException
|
||||
raise HomeAssistantError("No store available")
|
||||
|
||||
order = Order(
|
||||
self.dominos.closest_store,
|
||||
@@ -252,7 +252,7 @@ class DominosOrder(Entity):
|
||||
try:
|
||||
order = self.order()
|
||||
order.place()
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self._orderable = False
|
||||
_LOGGER.warning(
|
||||
"Attempted to order Dominos - Order invalid or store closed"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dominos",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pizzapi"],
|
||||
"requirements": ["pizzapi==0.0.3"]
|
||||
"requirements": ["pizzapi==0.0.6"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["easyenergy==2.1.1"]
|
||||
"requirements": ["easyenergy==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sense_energy"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["sense-energy==0.12.2"]
|
||||
"requirements": ["sense-energy==0.12.4"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/energyzero",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["energyzero==2.1.0"]
|
||||
"requirements": ["energyzero==2.1.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.6.3"]
|
||||
"requirements": ["env-canada==0.7.1"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
UV_INDEX,
|
||||
UnitOfLength,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
@@ -114,14 +113,6 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: data.conditions.get("pop", {}).get("value"),
|
||||
),
|
||||
ECSensorEntityDescription(
|
||||
key="precip_yesterday",
|
||||
translation_key="precip_yesterday",
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"),
|
||||
),
|
||||
ECSensorEntityDescription(
|
||||
key="pressure",
|
||||
translation_key="pressure",
|
||||
|
||||
@@ -52,9 +52,6 @@
|
||||
"pop": {
|
||||
"name": "Chance of precipitation"
|
||||
},
|
||||
"precip_yesterday": {
|
||||
"name": "Precipitation yesterday"
|
||||
},
|
||||
"pressure": {
|
||||
"name": "Barometric pressure"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==24.6.1",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
|
||||
@@ -98,7 +98,7 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
# The related binary sensors (leak detected, high flow, low battery)
|
||||
# will be active until the notification is deleted in the Flume app.
|
||||
self.notifications = pyflume.FlumeNotificationList(
|
||||
self.auth, read=None, sort_direction="DESC"
|
||||
self.auth, read=None
|
||||
).notification_list
|
||||
_LOGGER.debug("Notifications %s", self.notifications)
|
||||
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flume",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyflume"],
|
||||
"requirements": ["PyFlume==0.8.7"]
|
||||
"requirements": ["PyFlume==0.6.5"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyfritzhome"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["pyfritzhome==0.6.11"],
|
||||
"requirements": ["pyfritzhome==0.6.12"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:fritzbox:1"
|
||||
|
||||
@@ -81,6 +81,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"manual_switching_disabled": {
|
||||
"message": "Can't toggle switch while manual switching is disabled for the device."
|
||||
},
|
||||
"change_preset_while_active_mode": {
|
||||
"message": "Can't change preset while holiday or summer mode is active on the device."
|
||||
},
|
||||
|
||||
@@ -6,9 +6,11 @@ from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import FritzBoxDeviceEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FritzboxConfigEntry
|
||||
|
||||
|
||||
@@ -48,10 +50,20 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
self.check_lock_state()
|
||||
await self.hass.async_add_executor_job(self.data.set_switch_state_on)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
self.check_lock_state()
|
||||
await self.hass.async_add_executor_job(self.data.set_switch_state_off)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
def check_lock_state(self) -> None:
|
||||
"""Raise an Error if manual switching via FRITZ!Box user interface is disabled."""
|
||||
if self.data.lock:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="manual_switching_disabled",
|
||||
)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240626.0"]
|
||||
"requirements": ["home-assistant-frontend==20240628.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["odp-amsterdam==6.0.1"]
|
||||
"requirements": ["odp-amsterdam==6.0.2"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.1"]
|
||||
"requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"]
|
||||
}
|
||||
|
||||
@@ -95,9 +95,12 @@ def _format_tool(
|
||||
) -> dict[str, Any]:
|
||||
"""Format tool specification."""
|
||||
|
||||
parameters = _format_schema(
|
||||
convert(tool.parameters, custom_serializer=custom_serializer)
|
||||
)
|
||||
if tool.parameters.schema:
|
||||
parameters = _format_schema(
|
||||
convert(tool.parameters, custom_serializer=custom_serializer)
|
||||
)
|
||||
else:
|
||||
parameters = None
|
||||
|
||||
return protos.Tool(
|
||||
{
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==1.5.0"]
|
||||
"requirements": ["govee-local-api==1.5.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/gree",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["greeclimate"],
|
||||
"requirements": ["greeclimate==1.4.1"]
|
||||
"requirements": ["greeclimate==1.4.6"]
|
||||
}
|
||||
|
||||
@@ -71,13 +71,12 @@ class HoneywellSwitch(SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on if heat mode is enabled."""
|
||||
if self._device.system_mode == "heat":
|
||||
try:
|
||||
await self._device.set_system_mode("emheat")
|
||||
except SomeComfortError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="switch_failed_on"
|
||||
) from err
|
||||
try:
|
||||
await self._device.set_system_mode("emheat")
|
||||
except SomeComfortError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="switch_failed_on"
|
||||
) from err
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off if on."""
|
||||
|
||||
@@ -483,7 +483,7 @@ class HomeAssistantHTTP:
|
||||
frame.report(
|
||||
"calls hass.http.register_static_path which is deprecated because "
|
||||
"it does blocking I/O in the event loop, instead "
|
||||
"call `await hass.http.async_register_static_path("
|
||||
"call `await hass.http.async_register_static_paths("
|
||||
f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`; '
|
||||
"This function will be removed in 2025.7",
|
||||
exclude_integrations={"http"},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"requirements": ["aioautomower==2024.6.1"]
|
||||
"requirements": ["aioautomower==2024.6.3"]
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ from homeassistant.const import (
|
||||
CONF_METHOD,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
EVENT_STATE_CHANGED,
|
||||
EVENT_STATE_REPORTED,
|
||||
STATE_UNAVAILABLE,
|
||||
UnitOfTime,
|
||||
)
|
||||
@@ -45,7 +43,11 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.device import async_device_info_to_link_from_entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.event import (
|
||||
async_call_later,
|
||||
async_track_state_change_event,
|
||||
async_track_state_report_event,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
@@ -440,23 +442,17 @@ class IntegrationSensor(RestoreSensor):
|
||||
self._derive_and_set_attributes_from_state(state)
|
||||
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen(
|
||||
EVENT_STATE_CHANGED,
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
self._sensor_source_id,
|
||||
handle_state_change,
|
||||
event_filter=callback(
|
||||
lambda event_data: event_data["entity_id"] == self._sensor_source_id
|
||||
),
|
||||
run_immediately=True,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen(
|
||||
EVENT_STATE_REPORTED,
|
||||
async_track_state_report_event(
|
||||
self.hass,
|
||||
self._sensor_source_id,
|
||||
handle_state_report,
|
||||
event_filter=callback(
|
||||
lambda event_data: event_data["entity_id"] == self._sensor_source_id
|
||||
),
|
||||
run_immediately=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class JewishCalendarEntity(Entity):
|
||||
) -> None:
|
||||
"""Initialize a Jewish Calendar entity."""
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{description.key}"
|
||||
self._attr_unique_id = f"{config_entry.entry_id}-{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
|
||||
@@ -48,6 +48,7 @@ class KnockiTrigger(EventEntity):
|
||||
|
||||
_attr_event_types = [EVENT_TRIGGERED]
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_attr_translation_key = "knocki"
|
||||
|
||||
def __init__(self, trigger: Trigger, client: KnockiClient) -> None:
|
||||
|
||||
@@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version
|
||||
if version.parse(gateway_version) < version.parse("v3.5-rc5"):
|
||||
if version.parse(gateway_version) < version.parse("v3.4-rc5"):
|
||||
# incompatible gateway firmware, create an issue
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
|
||||
@@ -9,6 +9,7 @@ from lmcloud.lm_machine import LaMarzoccoMachine
|
||||
from lmcloud.models import LaMarzoccoMachineConfig
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
@@ -105,6 +106,7 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity):
|
||||
super().__init__(coordinator, f"auto_on_off_{identifier}")
|
||||
self._identifier = identifier
|
||||
self._attr_translation_placeholders = {"id": identifier}
|
||||
self.entity_category = EntityCategory.CONFIG
|
||||
|
||||
async def _async_enable(self, state: bool) -> None:
|
||||
"""Enable or disable the auto on/off schedule."""
|
||||
|
||||
@@ -30,8 +30,8 @@ async def async_setup_entry(
|
||||
|
||||
def _get_event_from_mealplan(mealplan: Mealplan) -> CalendarEvent:
|
||||
"""Create a CalendarEvent from a Mealplan."""
|
||||
description: str | None = None
|
||||
name = "No recipe"
|
||||
description: str | None = mealplan.description
|
||||
name = mealplan.title or "No recipe"
|
||||
if mealplan.recipe:
|
||||
name = mealplan.recipe.name
|
||||
description = mealplan.recipe.description
|
||||
@@ -50,12 +50,9 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity):
|
||||
self, coordinator: MealieCoordinator, entry_type: MealplanEntryType
|
||||
) -> None:
|
||||
"""Create the Calendar entity."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, entry_type.name.lower())
|
||||
self._entry_type = entry_type
|
||||
self._attr_translation_key = entry_type.name.lower()
|
||||
self._attr_unique_id = (
|
||||
f"{self.coordinator.config_entry.entry_id}_{entry_type.name.lower()}"
|
||||
)
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
|
||||
@@ -28,14 +28,13 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
client = MealieClient(
|
||||
user_input[CONF_HOST],
|
||||
token=user_input[CONF_API_TOKEN],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
await client.get_mealplan_today()
|
||||
info = await client.get_user_info()
|
||||
except MealieConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except MealieAuthenticationError:
|
||||
@@ -44,6 +43,8 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.exception("Unexpected error")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(info.user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title="Mealie",
|
||||
data=user_input,
|
||||
|
||||
@@ -12,10 +12,13 @@ class MealieEntity(CoordinatorEntity[MealieCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: MealieCoordinator) -> None:
|
||||
def __init__(self, coordinator: MealieCoordinator, key: str) -> None:
|
||||
"""Initialize Mealie entity."""
|
||||
super().__init__(coordinator)
|
||||
unique_id = coordinator.config_entry.unique_id
|
||||
assert unique_id is not None
|
||||
self._attr_unique_id = f"{unique_id}_{key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mealie",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aiomealie==0.4.0"]
|
||||
"requirements": ["aiomealie==0.5.0"]
|
||||
}
|
||||
|
||||
@@ -421,11 +421,6 @@ class MpdDevice(MediaPlayerEntity):
|
||||
"""Name of the current input source."""
|
||||
return self._current_playlist
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""Return the list of available input sources."""
|
||||
return self._playlists
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Choose a different available playlist and play it."""
|
||||
await self.async_play_media(MediaType.PLAYLIST, source)
|
||||
|
||||
@@ -36,7 +36,7 @@ from .const import (
|
||||
)
|
||||
from .models import DATA_MQTT, DATA_MQTT_AVAILABLE, ReceiveMessage
|
||||
|
||||
AVAILABILITY_TIMEOUT = 30.0
|
||||
AVAILABILITY_TIMEOUT = 50.0
|
||||
|
||||
TEMP_DIR_NAME = f"home-assistant-{DOMAIN}"
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from nextdns import (
|
||||
NextDns,
|
||||
Settings,
|
||||
)
|
||||
from tenacity import RetryError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
@@ -84,9 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> b
|
||||
|
||||
websession = async_get_clientsession(hass)
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
nextdns = await NextDns.create(websession, api_key)
|
||||
except (ApiError, ClientConnectorError, TimeoutError) as err:
|
||||
nextdns = await NextDns.create(websession, api_key)
|
||||
except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
tasks = []
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from nextdns import ApiError, InvalidApiKeyError, NextDns
|
||||
from tenacity import RetryError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -37,13 +37,12 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
self.api_key = user_input[CONF_API_KEY]
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
self.nextdns = await NextDns.create(
|
||||
websession, user_input[CONF_API_KEY]
|
||||
)
|
||||
self.nextdns = await NextDns.create(
|
||||
websession, user_input[CONF_API_KEY]
|
||||
)
|
||||
except InvalidApiKeyError:
|
||||
errors["base"] = "invalid_api_key"
|
||||
except (ApiError, ClientConnectorError, TimeoutError):
|
||||
except (ApiError, ClientConnectorError, RetryError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
errors["base"] = "unknown"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""NextDns coordinator."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TypeVar
|
||||
@@ -19,6 +18,7 @@ from nextdns import (
|
||||
Settings,
|
||||
)
|
||||
from nextdns.model import NextDnsData
|
||||
from tenacity import RetryError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -58,9 +58,13 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]):
|
||||
async def _async_update_data(self) -> CoordinatorDataT:
|
||||
"""Update data via internal method."""
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
return await self._async_update_data_internal()
|
||||
except (ApiError, ClientConnectorError, InvalidApiKeyError) as err:
|
||||
return await self._async_update_data_internal()
|
||||
except (
|
||||
ApiError,
|
||||
ClientConnectorError,
|
||||
InvalidApiKeyError,
|
||||
RetryError,
|
||||
) as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
async def _async_update_data_internal(self) -> CoordinatorDataT:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nextdns"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["nextdns==3.0.0"]
|
||||
"requirements": ["nextdns==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ class NMBSSensor(SensorEntity):
|
||||
attrs["via_arrival_platform"] = via["arrival"]["platform"]
|
||||
attrs["via_transfer_platform"] = via["departure"]["platform"]
|
||||
attrs["via_transfer_time"] = get_delay_in_minutes(
|
||||
via["timeBetween"]
|
||||
via["timebetween"]
|
||||
) + get_delay_in_minutes(via["departure"]["delay"])
|
||||
|
||||
if delay > 0:
|
||||
|
||||
@@ -78,8 +78,8 @@ HOURLY = "hourly"
|
||||
|
||||
OBSERVATION_VALID_TIME = timedelta(minutes=60)
|
||||
FORECAST_VALID_TIME = timedelta(minutes=45)
|
||||
# A lot of stations update once hourly plus some wiggle room
|
||||
UPDATE_TIME_PERIOD = timedelta(minutes=70)
|
||||
# Ask for observations for last four hours
|
||||
UPDATE_TIME_PERIOD = timedelta(minutes=240)
|
||||
|
||||
DEBOUNCE_TIME = 10 * 60 # in seconds
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/opensky",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["python-opensky==1.0.0"]
|
||||
"requirements": ["python-opensky==1.0.1"]
|
||||
}
|
||||
|
||||
@@ -109,17 +109,20 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
|
||||
key=OverkizState.CORE_HEATING_STATUS,
|
||||
name="Heating status",
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
value_fn=lambda state: cast(str, state).lower()
|
||||
in (OverkizCommandParam.ON, OverkizCommandParam.HEATING),
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE,
|
||||
name="Absence mode",
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
value_fn=lambda state: state
|
||||
in (OverkizCommandParam.ON, OverkizCommandParam.PROG),
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.MODBUSLINK_DHW_BOOST_MODE,
|
||||
name="Boost mode",
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
value_fn=lambda state: state
|
||||
in (OverkizCommandParam.ON, OverkizCommandParam.PROG),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -182,6 +182,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.MODBUSLINK_POWER_HEAT_ELECTRICAL,
|
||||
name="Electric power consumption",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF1,
|
||||
name="Consumption tariff 1",
|
||||
@@ -413,6 +420,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_REMAINING_HOT_WATER,
|
||||
name="Warm water remaining",
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
),
|
||||
# Cover
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_TARGET_CLOSURE,
|
||||
|
||||
@@ -9,7 +9,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import HomeAssistantOverkizData
|
||||
from .const import DOMAIN
|
||||
from .water_heater_entities import WIDGET_TO_WATER_HEATER_ENTITY
|
||||
from .entity import OverkizEntity
|
||||
from .water_heater_entities import (
|
||||
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY,
|
||||
WIDGET_TO_WATER_HEATER_ENTITY,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -19,11 +23,20 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Overkiz DHW from a config entry."""
|
||||
data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id]
|
||||
entities: list[OverkizEntity] = []
|
||||
|
||||
async_add_entities(
|
||||
WIDGET_TO_WATER_HEATER_ENTITY[device.widget](
|
||||
device.device_url, data.coordinator
|
||||
)
|
||||
for device in data.platforms[Platform.WATER_HEATER]
|
||||
if device.widget in WIDGET_TO_WATER_HEATER_ENTITY
|
||||
)
|
||||
for device in data.platforms[Platform.WATER_HEATER]:
|
||||
if device.controllable_name in CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY:
|
||||
entities.append(
|
||||
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY[device.controllable_name](
|
||||
device.device_url, data.coordinator
|
||||
)
|
||||
)
|
||||
elif device.widget in WIDGET_TO_WATER_HEATER_ENTITY:
|
||||
entities.append(
|
||||
WIDGET_TO_WATER_HEATER_ENTITY[device.widget](
|
||||
device.device_url, data.coordinator
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from pyoverkiz.enums.ui import UIWidget
|
||||
|
||||
from .atlantic_domestic_hot_water_production_mlb_component import (
|
||||
AtlanticDomesticHotWaterProductionMBLComponent,
|
||||
)
|
||||
from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW
|
||||
from .domestic_hot_water_production import DomesticHotWaterProduction
|
||||
from .hitachi_dhw import HitachiDHW
|
||||
@@ -11,3 +14,7 @@ WIDGET_TO_WATER_HEATER_ENTITY = {
|
||||
UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: DomesticHotWaterProduction,
|
||||
UIWidget.HITACHI_DHW: HitachiDHW,
|
||||
}
|
||||
|
||||
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY = {
|
||||
"modbuslink:AtlanticDomesticHotWaterProductionMBLComponent": AtlanticDomesticHotWaterProductionMBLComponent,
|
||||
}
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
"""Support for AtlanticDomesticHotWaterProductionMBLComponent."""
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
|
||||
|
||||
from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
STATE_OFF,
|
||||
STATE_PERFORMANCE,
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
|
||||
from .. import OverkizDataUpdateCoordinator
|
||||
from ..entity import OverkizEntity
|
||||
|
||||
|
||||
class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterEntity):
|
||||
"""Representation of AtlanticDomesticHotWaterProductionMBLComponent (modbuslink)."""
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||
| WaterHeaterEntityFeature.AWAY_MODE
|
||||
| WaterHeaterEntityFeature.ON_OFF
|
||||
)
|
||||
_attr_operation_list = [
|
||||
OverkizCommandParam.PERFORMANCE,
|
||||
OverkizCommandParam.ECO,
|
||||
OverkizCommandParam.MANUAL,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Init method."""
|
||||
super().__init__(device_url, coordinator)
|
||||
self._attr_max_temp = cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE
|
||||
),
|
||||
)
|
||||
self._attr_min_temp = cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""Return the current temperature."""
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.MODBUSLINK_MIDDLE_WATER_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""Return the temperature corresponding to the PRESET."""
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(OverkizState.CORE_WATER_TARGET_TEMPERATURE),
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new temperature."""
|
||||
temperature = kwargs[ATTR_TEMPERATURE]
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_TARGET_DHW_TEMPERATURE, temperature
|
||||
)
|
||||
|
||||
@property
|
||||
def is_boost_mode_on(self) -> bool:
|
||||
"""Return true if boost mode is on."""
|
||||
return self.executor.select_state(OverkizState.MODBUSLINK_DHW_BOOST_MODE) in (
|
||||
OverkizCommandParam.ON,
|
||||
OverkizCommandParam.PROG,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_eco_mode_on(self) -> bool:
|
||||
"""Return true if eco mode is on."""
|
||||
return self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE) in (
|
||||
OverkizCommandParam.MANUAL_ECO_ACTIVE,
|
||||
OverkizCommandParam.AUTO_MODE,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self) -> bool:
|
||||
"""Return true if away mode is on."""
|
||||
return (
|
||||
self.executor.select_state(OverkizState.MODBUSLINK_DHW_ABSENCE_MODE)
|
||||
== OverkizCommandParam.ON
|
||||
)
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str:
|
||||
"""Return current operation."""
|
||||
if self.is_away_mode_on:
|
||||
return STATE_OFF
|
||||
|
||||
if self.is_boost_mode_on:
|
||||
return STATE_PERFORMANCE
|
||||
|
||||
if self.is_eco_mode_on:
|
||||
return STATE_ECO
|
||||
|
||||
if (
|
||||
cast(str, self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE))
|
||||
== OverkizCommandParam.MANUAL_ECO_INACTIVE
|
||||
):
|
||||
return OverkizCommandParam.MANUAL
|
||||
|
||||
return STATE_OFF
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
"""Set new operation mode."""
|
||||
if operation_mode in (STATE_PERFORMANCE, OverkizCommandParam.BOOST):
|
||||
if self.is_away_mode_on:
|
||||
await self.async_turn_away_mode_off()
|
||||
await self.async_turn_boost_mode_on()
|
||||
elif operation_mode in (
|
||||
OverkizCommandParam.ECO,
|
||||
OverkizCommandParam.MANUAL_ECO_ACTIVE,
|
||||
):
|
||||
if self.is_away_mode_on:
|
||||
await self.async_turn_away_mode_off()
|
||||
if self.is_boost_mode_on:
|
||||
await self.async_turn_boost_mode_off()
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DHW_MODE, OverkizCommandParam.AUTO_MODE
|
||||
)
|
||||
elif operation_mode in (
|
||||
OverkizCommandParam.MANUAL,
|
||||
OverkizCommandParam.MANUAL_ECO_INACTIVE,
|
||||
):
|
||||
if self.is_away_mode_on:
|
||||
await self.async_turn_away_mode_off()
|
||||
if self.is_boost_mode_on:
|
||||
await self.async_turn_boost_mode_off()
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DHW_MODE, OverkizCommandParam.MANUAL_ECO_INACTIVE
|
||||
)
|
||||
else:
|
||||
if self.is_away_mode_on:
|
||||
await self.async_turn_away_mode_off()
|
||||
if self.is_boost_mode_on:
|
||||
await self.async_turn_boost_mode_off()
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DHW_MODE, operation_mode
|
||||
)
|
||||
|
||||
async def async_turn_away_mode_on(self) -> None:
|
||||
"""Turn away mode on."""
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.ON
|
||||
)
|
||||
|
||||
async def async_turn_away_mode_off(self) -> None:
|
||||
"""Turn away mode off."""
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.OFF
|
||||
)
|
||||
|
||||
async def async_turn_boost_mode_on(self) -> None:
|
||||
"""Turn boost mode on."""
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.ON
|
||||
)
|
||||
|
||||
async def async_turn_boost_mode_off(self) -> None:
|
||||
"""Turn boost mode off."""
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.OFF
|
||||
)
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["p1monitor"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["p1monitor==3.0.0"]
|
||||
"requirements": ["p1monitor==3.0.1"]
|
||||
}
|
||||
|
||||
@@ -196,10 +196,10 @@ class Remote:
|
||||
self.muted = self._control.get_mute()
|
||||
self.volume = self._control.get_volume() / 100
|
||||
|
||||
async def async_send_key(self, key):
|
||||
async def async_send_key(self, key: Keys | str) -> None:
|
||||
"""Send a key to the TV and handle exceptions."""
|
||||
try:
|
||||
key = getattr(Keys, key)
|
||||
key = getattr(Keys, key.upper())
|
||||
except (AttributeError, TypeError):
|
||||
key = getattr(key, "value", key)
|
||||
|
||||
@@ -211,13 +211,13 @@ class Remote:
|
||||
await self._on_action.async_run(context=context)
|
||||
await self.async_update()
|
||||
elif self.state != STATE_ON:
|
||||
await self.async_send_key(Keys.power)
|
||||
await self.async_send_key(Keys.POWER)
|
||||
await self.async_update()
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn off the TV."""
|
||||
if self.state != STATE_OFF:
|
||||
await self.async_send_key(Keys.power)
|
||||
await self.async_send_key(Keys.POWER)
|
||||
self.state = STATE_OFF
|
||||
await self.async_update()
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/panasonic_viera",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["panasonic_viera"],
|
||||
"requirements": ["panasonic-viera==0.3.6"]
|
||||
"requirements": ["panasonic-viera==0.4.2"]
|
||||
}
|
||||
|
||||
@@ -126,11 +126,11 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up the media player."""
|
||||
await self._remote.async_send_key(Keys.volume_up)
|
||||
await self._remote.async_send_key(Keys.VOLUME_UP)
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
await self._remote.async_send_key(Keys.volume_down)
|
||||
await self._remote.async_send_key(Keys.VOLUME_DOWN)
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Send mute command."""
|
||||
@@ -143,33 +143,33 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Simulate play pause media player."""
|
||||
if self._remote.playing:
|
||||
await self._remote.async_send_key(Keys.pause)
|
||||
await self._remote.async_send_key(Keys.PAUSE)
|
||||
self._remote.playing = False
|
||||
else:
|
||||
await self._remote.async_send_key(Keys.play)
|
||||
await self._remote.async_send_key(Keys.PLAY)
|
||||
self._remote.playing = True
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
await self._remote.async_send_key(Keys.play)
|
||||
await self._remote.async_send_key(Keys.PLAY)
|
||||
self._remote.playing = True
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
await self._remote.async_send_key(Keys.pause)
|
||||
await self._remote.async_send_key(Keys.PAUSE)
|
||||
self._remote.playing = False
|
||||
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Stop playback."""
|
||||
await self._remote.async_send_key(Keys.stop)
|
||||
await self._remote.async_send_key(Keys.STOP)
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send the fast forward command."""
|
||||
await self._remote.async_send_key(Keys.fast_forward)
|
||||
await self._remote.async_send_key(Keys.FAST_FORWARD)
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send the rewind command."""
|
||||
await self._remote.async_send_key(Keys.rewind)
|
||||
await self._remote.async_send_key(Keys.REWIND)
|
||||
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/pure_energie",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["gridnet==5.0.0"],
|
||||
"requirements": ["gridnet==5.0.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -142,6 +142,13 @@ _DEFAULT_TABLE_ARGS = {
|
||||
"mariadb_engine": MYSQL_ENGINE,
|
||||
}
|
||||
|
||||
_MATCH_ALL_KEEP = {
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_STATE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
}
|
||||
|
||||
|
||||
class UnusedDateTime(DateTime):
|
||||
"""An unused column type that behaves like a datetime."""
|
||||
@@ -597,19 +604,8 @@ class StateAttributes(Base):
|
||||
if MATCH_ALL in unrecorded_attributes:
|
||||
# Don't exclude device class, state class, unit of measurement
|
||||
# or friendly name when using the MATCH_ALL exclude constant
|
||||
_exclude_attributes = {
|
||||
k: v
|
||||
for k, v in state.attributes.items()
|
||||
if k
|
||||
not in (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_STATE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
)
|
||||
}
|
||||
exclude_attrs.update(_exclude_attributes)
|
||||
|
||||
exclude_attrs.update(state.attributes)
|
||||
exclude_attrs -= _MATCH_ALL_KEEP
|
||||
else:
|
||||
exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS
|
||||
encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes
|
||||
|
||||
@@ -81,7 +81,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple(
|
||||
key="hvac_status",
|
||||
coordinator="hvac_status",
|
||||
on_key="hvacStatus",
|
||||
on_value=2,
|
||||
on_value="on",
|
||||
translation_key="hvac_status",
|
||||
),
|
||||
RenaultBinarySensorEntityDescription(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["renault-api==0.2.3"]
|
||||
"requirements": ["renault-api==0.2.4"]
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
"charge_mode": {
|
||||
"name": "Charge mode",
|
||||
"state": {
|
||||
"always": "Instant",
|
||||
"always_charging": "[%key:component::renault::entity::select::charge_mode::state::always%]",
|
||||
"schedule_mode": "Planner",
|
||||
"always": "Always",
|
||||
"always_charging": "Always charging",
|
||||
"schedule_mode": "Schedule mode",
|
||||
"scheduled": "Scheduled"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sense",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sense_energy"],
|
||||
"requirements": ["sense-energy==0.12.2"]
|
||||
"requirements": ["sense-energy==0.12.4"]
|
||||
}
|
||||
|
||||
@@ -83,11 +83,9 @@ REST_SENSORS_UPDATE_INTERVAL: Final = 60
|
||||
# Refresh interval for RPC polling sensors
|
||||
RPC_SENSORS_POLLING_INTERVAL: Final = 60
|
||||
|
||||
# Multiplier used to calculate the "update_interval" for sleeping devices.
|
||||
SLEEP_PERIOD_MULTIPLIER: Final = 1.2
|
||||
CONF_SLEEP_PERIOD: Final = "sleep_period"
|
||||
|
||||
# Multiplier used to calculate the "update_interval" for non-sleeping devices.
|
||||
# Multiplier used to calculate the "update_interval" for shelly devices.
|
||||
UPDATE_PERIOD_MULTIPLIER: Final = 2.2
|
||||
|
||||
# Reconnect interval for GEN2 devices
|
||||
|
||||
@@ -54,7 +54,6 @@ from .const import (
|
||||
RPC_RECONNECT_INTERVAL,
|
||||
RPC_SENSORS_POLLING_INTERVAL,
|
||||
SHBTN_MODELS,
|
||||
SLEEP_PERIOD_MULTIPLIER,
|
||||
UPDATE_PERIOD_MULTIPLIER,
|
||||
BLEScannerMode,
|
||||
)
|
||||
@@ -168,7 +167,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
|
||||
await self.device.initialize()
|
||||
update_device_fw_info(self.hass, self.device, self.entry)
|
||||
except DeviceConnectionError as err:
|
||||
LOGGER.debug(
|
||||
LOGGER.error(
|
||||
"Error connecting to Shelly device %s, error: %r", self.name, err
|
||||
)
|
||||
return False
|
||||
@@ -229,7 +228,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]):
|
||||
"""Initialize the Shelly block device coordinator."""
|
||||
self.entry = entry
|
||||
if self.sleep_period:
|
||||
update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period
|
||||
update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period
|
||||
else:
|
||||
update_interval = (
|
||||
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
|
||||
@@ -378,12 +377,13 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]):
|
||||
eager_start=True,
|
||||
)
|
||||
elif update_type is BlockUpdateType.COAP_PERIODIC:
|
||||
if self._push_update_failures >= MAX_PUSH_UPDATE_FAILURES:
|
||||
ir.async_delete_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
PUSH_UPDATE_ISSUE_ID.format(unique=self.mac),
|
||||
)
|
||||
self._push_update_failures = 0
|
||||
ir.async_delete_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
PUSH_UPDATE_ISSUE_ID.format(unique=self.mac),
|
||||
)
|
||||
elif update_type is BlockUpdateType.COAP_REPLY:
|
||||
self._push_update_failures += 1
|
||||
if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES:
|
||||
@@ -429,7 +429,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]):
|
||||
in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION
|
||||
):
|
||||
update_interval = (
|
||||
SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
|
||||
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
|
||||
)
|
||||
super().__init__(hass, entry, device, update_interval)
|
||||
|
||||
@@ -459,7 +459,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
"""Initialize the Shelly RPC device coordinator."""
|
||||
self.entry = entry
|
||||
if self.sleep_period:
|
||||
update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period
|
||||
update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period
|
||||
else:
|
||||
update_interval = RPC_RECONNECT_INTERVAL
|
||||
super().__init__(hass, entry, device, update_interval)
|
||||
@@ -486,7 +486,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
data[CONF_SLEEP_PERIOD] = wakeup_period
|
||||
self.hass.config_entries.async_update_entry(self.entry, data=data)
|
||||
|
||||
update_interval = SLEEP_PERIOD_MULTIPLIER * wakeup_period
|
||||
update_interval = UPDATE_PERIOD_MULTIPLIER * wakeup_period
|
||||
self.update_interval = timedelta(seconds=update_interval)
|
||||
|
||||
return True
|
||||
|
||||
@@ -24,6 +24,8 @@ async def async_get_config_entry_diagnostics(
|
||||
device_settings: str | dict = "not initialized"
|
||||
device_status: str | dict = "not initialized"
|
||||
bluetooth: str | dict = "not initialized"
|
||||
last_error: str = "not initialized"
|
||||
|
||||
if shelly_entry_data.block:
|
||||
block_coordinator = shelly_entry_data.block
|
||||
assert block_coordinator
|
||||
@@ -55,6 +57,10 @@ async def async_get_config_entry_diagnostics(
|
||||
"uptime",
|
||||
]
|
||||
}
|
||||
|
||||
if block_coordinator.device.last_error:
|
||||
last_error = repr(block_coordinator.device.last_error)
|
||||
|
||||
else:
|
||||
rpc_coordinator = shelly_entry_data.rpc
|
||||
assert rpc_coordinator
|
||||
@@ -79,6 +85,9 @@ async def async_get_config_entry_diagnostics(
|
||||
"scanner": await scanner.async_diagnostics(),
|
||||
}
|
||||
|
||||
if rpc_coordinator.device.last_error:
|
||||
last_error = repr(rpc_coordinator.device.last_error)
|
||||
|
||||
if isinstance(device_status, dict):
|
||||
device_status = async_redact_data(device_status, ["ssid"])
|
||||
|
||||
@@ -87,5 +96,6 @@ async def async_get_config_entry_diagnostics(
|
||||
"device_info": device_info,
|
||||
"device_settings": device_settings,
|
||||
"device_status": device_status,
|
||||
"last_error": last_error,
|
||||
"bluetooth": bluetooth,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""The statistics component."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import CONF_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
|
||||
DOMAIN = "statistics"
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
@@ -11,6 +14,12 @@ PLATFORMS = [Platform.SENSOR]
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Statistics from a config entry."""
|
||||
|
||||
async_remove_stale_devices_links_keep_entity_device(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
entry.options[CONF_ENTITY_ID],
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ from homeassistant.core import (
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.device import async_device_info_to_link_from_entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_utc_time,
|
||||
@@ -268,6 +269,7 @@ async def async_setup_platform(
|
||||
async_add_entities(
|
||||
new_entities=[
|
||||
StatisticsSensor(
|
||||
hass=hass,
|
||||
source_entity_id=config[CONF_ENTITY_ID],
|
||||
name=config[CONF_NAME],
|
||||
unique_id=config.get(CONF_UNIQUE_ID),
|
||||
@@ -304,6 +306,7 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
[
|
||||
StatisticsSensor(
|
||||
hass=hass,
|
||||
source_entity_id=entry.options[CONF_ENTITY_ID],
|
||||
name=entry.options[CONF_NAME],
|
||||
unique_id=entry.entry_id,
|
||||
@@ -327,6 +330,7 @@ class StatisticsSensor(SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
source_entity_id: str,
|
||||
name: str,
|
||||
unique_id: str | None,
|
||||
@@ -341,6 +345,10 @@ class StatisticsSensor(SensorEntity):
|
||||
self._attr_name: str = name
|
||||
self._attr_unique_id: str | None = unique_id
|
||||
self._source_entity_id: str = source_entity_id
|
||||
self._attr_device_info = async_device_info_to_link_from_entity(
|
||||
hass,
|
||||
source_entity_id,
|
||||
)
|
||||
self.is_binary: bool = (
|
||||
split_entity_id(self._source_entity_id)[0] == BINARY_SENSOR_DOMAIN
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ from .const import (
|
||||
TYPE_HEATING,
|
||||
)
|
||||
from .entity import TadoZoneEntity
|
||||
from .helper import decide_duration, decide_overlay_mode
|
||||
from .helper import decide_duration, decide_overlay_mode, generate_supported_fanmodes
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -200,15 +200,14 @@ def create_climate_entity(
|
||||
continue
|
||||
|
||||
if capabilities[mode].get("fanSpeeds"):
|
||||
supported_fan_modes = [
|
||||
TADO_TO_HA_FAN_MODE_MAP_LEGACY[speed]
|
||||
for speed in capabilities[mode]["fanSpeeds"]
|
||||
]
|
||||
supported_fan_modes = generate_supported_fanmodes(
|
||||
TADO_TO_HA_FAN_MODE_MAP_LEGACY, capabilities[mode]["fanSpeeds"]
|
||||
)
|
||||
|
||||
else:
|
||||
supported_fan_modes = [
|
||||
TADO_TO_HA_FAN_MODE_MAP[level]
|
||||
for level in capabilities[mode]["fanLevel"]
|
||||
]
|
||||
supported_fan_modes = generate_supported_fanmodes(
|
||||
TADO_TO_HA_FAN_MODE_MAP, capabilities[mode]["fanLevel"]
|
||||
)
|
||||
|
||||
cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"]
|
||||
else:
|
||||
|
||||
@@ -49,3 +49,15 @@ def decide_duration(
|
||||
)
|
||||
|
||||
return duration
|
||||
|
||||
|
||||
def generate_supported_fanmodes(tado_to_ha_mapping: dict[str, str], options: list[str]):
|
||||
"""Return correct list of fan modes or None."""
|
||||
supported_fanmodes = [
|
||||
tado_to_ha_mapping.get(option)
|
||||
for option in options
|
||||
if tado_to_ha_mapping.get(option) is not None
|
||||
]
|
||||
if not supported_fanmodes:
|
||||
return None
|
||||
return supported_fanmodes
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["hatasmota"],
|
||||
"mqtt": ["tasmota/discovery/#"],
|
||||
"requirements": ["HATasmota==0.8.0"]
|
||||
"requirements": ["HATasmota==0.9.2"]
|
||||
}
|
||||
|
||||
@@ -53,26 +53,10 @@ ICON = "icon"
|
||||
# A Tasmota sensor type may be mapped to either a device class or an icon,
|
||||
# both can only be set if the default device class icon is not appropriate
|
||||
SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
||||
hc.SENSOR_ACTIVE_ENERGYEXPORT: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL,
|
||||
},
|
||||
hc.SENSOR_ACTIVE_ENERGYIMPORT: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL,
|
||||
},
|
||||
hc.SENSOR_ACTIVE_POWERUSAGE: {
|
||||
DEVICE_CLASS: SensorDeviceClass.POWER,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_AMBIENT: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_APPARENT_POWERUSAGE: {
|
||||
DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_BATTERY: {
|
||||
DEVICE_CLASS: SensorDeviceClass.BATTERY,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
@@ -92,7 +76,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
||||
DEVICE_CLASS: SensorDeviceClass.CURRENT,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_CURRENTNEUTRAL: {
|
||||
hc.SENSOR_CURRENT_NEUTRAL: {
|
||||
DEVICE_CLASS: SensorDeviceClass.CURRENT,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
@@ -110,6 +94,34 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL,
|
||||
},
|
||||
hc.SENSOR_ENERGY_EXPORT_ACTIVE: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL,
|
||||
},
|
||||
hc.SENSOR_ENERGY_EXPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL},
|
||||
hc.SENSOR_ENERGY_EXPORT_TARIFF: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL,
|
||||
},
|
||||
hc.SENSOR_ENERGY_IMPORT_ACTIVE: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL,
|
||||
},
|
||||
hc.SENSOR_ENERGY_IMPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL},
|
||||
hc.SENSOR_ENERGY_IMPORT_TODAY: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
|
||||
},
|
||||
hc.SENSOR_ENERGY_IMPORT_TOTAL: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL,
|
||||
},
|
||||
hc.SENSOR_ENERGY_IMPORT_TOTAL_TARIFF: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL,
|
||||
},
|
||||
hc.SENSOR_ENERGY_IMPORT_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY},
|
||||
hc.SENSOR_ENERGY_TOTAL_START_TIME: {ICON: "mdi:progress-clock"},
|
||||
hc.SENSOR_FREQUENCY: {
|
||||
DEVICE_CLASS: SensorDeviceClass.FREQUENCY,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
@@ -122,6 +134,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
||||
DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_POWER_ACTIVE: {
|
||||
DEVICE_CLASS: SensorDeviceClass.POWER,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_POWER_APPARENT: {
|
||||
DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"},
|
||||
hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"},
|
||||
hc.SENSOR_MOISTURE: {DEVICE_CLASS: SensorDeviceClass.MOISTURE},
|
||||
@@ -144,11 +164,11 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
||||
DEVICE_CLASS: SensorDeviceClass.PM25,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_POWERFACTOR: {
|
||||
hc.SENSOR_POWER_FACTOR: {
|
||||
DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_POWERUSAGE: {
|
||||
hc.SENSOR_POWER: {
|
||||
DEVICE_CLASS: SensorDeviceClass.POWER,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
@@ -156,14 +176,12 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
||||
DEVICE_CLASS: SensorDeviceClass.PRESSURE,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_PRESSUREATSEALEVEL: {
|
||||
hc.SENSOR_PRESSURE_AT_SEA_LEVEL: {
|
||||
DEVICE_CLASS: SensorDeviceClass.PRESSURE,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"},
|
||||
hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL},
|
||||
hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL},
|
||||
hc.SENSOR_REACTIVE_POWERUSAGE: {
|
||||
hc.SENSOR_POWER_REACTIVE: {
|
||||
DEVICE_CLASS: SensorDeviceClass.REACTIVE_POWER,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
@@ -182,15 +200,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
||||
DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_TODAY: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
|
||||
},
|
||||
hc.SENSOR_TOTAL: {
|
||||
DEVICE_CLASS: SensorDeviceClass.ENERGY,
|
||||
STATE_CLASS: SensorStateClass.TOTAL,
|
||||
},
|
||||
hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"},
|
||||
hc.SENSOR_TVOC: {ICON: "mdi:air-filter"},
|
||||
hc.SENSOR_VOLTAGE: {
|
||||
DEVICE_CLASS: SensorDeviceClass.VOLTAGE,
|
||||
@@ -200,7 +209,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
|
||||
DEVICE_CLASS: SensorDeviceClass.WEIGHT,
|
||||
STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY},
|
||||
}
|
||||
|
||||
SENSOR_UNIT_MAP = {
|
||||
@@ -294,6 +302,15 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity):
|
||||
self._attr_native_unit_of_measurement = SENSOR_UNIT_MAP.get(
|
||||
self._tasmota_entity.unit, self._tasmota_entity.unit
|
||||
)
|
||||
if (
|
||||
self._attr_device_class is None
|
||||
and self._attr_state_class is None
|
||||
and self._attr_native_unit_of_measurement is None
|
||||
):
|
||||
# If the sensor has a numeric value, but we couldn't detect what it is,
|
||||
# set state class to measurement.
|
||||
if self._tasmota_entity.discovered_as_numeric:
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to MQTT events."""
|
||||
|
||||
@@ -702,7 +702,7 @@ class TelegramNotificationService:
|
||||
}
|
||||
if message_tag is not None:
|
||||
event_data[ATTR_MESSAGE_TAG] = message_tag
|
||||
if kwargs_msg[ATTR_MESSAGE_THREAD_ID] is not None:
|
||||
if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None:
|
||||
event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[
|
||||
ATTR_MESSAGE_THREAD_ID
|
||||
]
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["tesla-fleet-api==0.6.1"]
|
||||
"requirements": ["tesla-fleet-api==0.6.2"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from tesla_fleet_api import EnergySpecific, Tessie
|
||||
from tesla_fleet_api.const import Scope
|
||||
from tesla_fleet_api.exceptions import TeslaFleetError
|
||||
from tessie_api import get_state_of_all_vehicles
|
||||
|
||||
@@ -94,41 +95,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
|
||||
|
||||
# Energy Sites
|
||||
tessie = Tessie(session, api_key)
|
||||
energysites: list[TessieEnergyData] = []
|
||||
|
||||
try:
|
||||
products = (await tessie.products())["response"]
|
||||
scopes = await tessie.scopes()
|
||||
except TeslaFleetError as e:
|
||||
raise ConfigEntryNotReady from e
|
||||
|
||||
energysites: list[TessieEnergyData] = []
|
||||
for product in products:
|
||||
if "energy_site_id" in product:
|
||||
site_id = product["energy_site_id"]
|
||||
api = EnergySpecific(tessie.energy, site_id)
|
||||
energysites.append(
|
||||
TessieEnergyData(
|
||||
api=api,
|
||||
id=site_id,
|
||||
live_coordinator=TessieEnergySiteLiveCoordinator(hass, api),
|
||||
info_coordinator=TessieEnergySiteInfoCoordinator(hass, api),
|
||||
device=DeviceInfo(
|
||||
identifiers={(DOMAIN, str(site_id))},
|
||||
manufacturer="Tesla",
|
||||
name=product.get("site_name", "Energy Site"),
|
||||
),
|
||||
)
|
||||
)
|
||||
if Scope.ENERGY_DEVICE_DATA in scopes:
|
||||
try:
|
||||
products = (await tessie.products())["response"]
|
||||
except TeslaFleetError as e:
|
||||
raise ConfigEntryNotReady from e
|
||||
|
||||
# Populate coordinator data before forwarding to platforms
|
||||
await asyncio.gather(
|
||||
*(
|
||||
energysite.live_coordinator.async_config_entry_first_refresh()
|
||||
for energysite in energysites
|
||||
),
|
||||
*(
|
||||
energysite.info_coordinator.async_config_entry_first_refresh()
|
||||
for energysite in energysites
|
||||
),
|
||||
)
|
||||
for product in products:
|
||||
if "energy_site_id" in product:
|
||||
site_id = product["energy_site_id"]
|
||||
api = EnergySpecific(tessie.energy, site_id)
|
||||
energysites.append(
|
||||
TessieEnergyData(
|
||||
api=api,
|
||||
id=site_id,
|
||||
live_coordinator=TessieEnergySiteLiveCoordinator(hass, api),
|
||||
info_coordinator=TessieEnergySiteInfoCoordinator(hass, api),
|
||||
device=DeviceInfo(
|
||||
identifiers={(DOMAIN, str(site_id))},
|
||||
manufacturer="Tesla",
|
||||
name=product.get("site_name", "Energy Site"),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Populate coordinator data before forwarding to platforms
|
||||
await asyncio.gather(
|
||||
*(
|
||||
energysite.live_coordinator.async_config_entry_first_refresh()
|
||||
for energysite in energysites
|
||||
),
|
||||
*(
|
||||
energysite.info_coordinator.async_config_entry_first_refresh()
|
||||
for energysite in energysites
|
||||
),
|
||||
)
|
||||
|
||||
entry.runtime_data = TessieData(vehicles, energysites)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -132,6 +132,7 @@ class TessieEnergyEntity(TessieBaseEntity):
|
||||
self._attr_device_info = data.device
|
||||
|
||||
super().__init__(coordinator, key)
|
||||
self._async_update_attrs()
|
||||
|
||||
|
||||
class TessieWallConnectorEntity(TessieBaseEntity):
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie"],
|
||||
"requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.1"]
|
||||
"requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.2"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
@@ -43,6 +44,7 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_CREDENTIALS_HASH,
|
||||
CONF_DEVICE_CONFIG,
|
||||
CONNECT_TIMEOUT,
|
||||
DISCOVERY_TIMEOUT,
|
||||
@@ -73,6 +75,7 @@ def async_trigger_discovery(
|
||||
discovered_devices: dict[str, Device],
|
||||
) -> None:
|
||||
"""Trigger config flows for discovered devices."""
|
||||
|
||||
for formatted_mac, device in discovered_devices.items():
|
||||
discovery_flow.async_create_flow(
|
||||
hass,
|
||||
@@ -83,7 +86,6 @@ def async_trigger_discovery(
|
||||
CONF_HOST: device.host,
|
||||
CONF_MAC: formatted_mac,
|
||||
CONF_DEVICE_CONFIG: device.config.to_dict(
|
||||
credentials_hash=device.credentials_hash,
|
||||
exclude_credentials=True,
|
||||
),
|
||||
},
|
||||
@@ -133,6 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
||||
"""Set up TPLink from a config entry."""
|
||||
host: str = entry.data[CONF_HOST]
|
||||
credentials = await get_credentials(hass)
|
||||
entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH)
|
||||
|
||||
config: DeviceConfig | None = None
|
||||
if config_dict := entry.data.get(CONF_DEVICE_CONFIG):
|
||||
@@ -151,19 +154,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
||||
config.timeout = CONNECT_TIMEOUT
|
||||
if config.uses_http is True:
|
||||
config.http_client = create_async_tplink_clientsession(hass)
|
||||
|
||||
# If we have in memory credentials use them otherwise check for credentials_hash
|
||||
if credentials:
|
||||
config.credentials = credentials
|
||||
elif entry_credentials_hash:
|
||||
config.credentials_hash = entry_credentials_hash
|
||||
|
||||
try:
|
||||
device: Device = await Device.connect(config=config)
|
||||
except AuthenticationError as ex:
|
||||
# If the stored credentials_hash was used but doesn't work remove it
|
||||
if not credentials and entry_credentials_hash:
|
||||
data = {k: v for k, v in entry.data.items() if k != CONF_CREDENTIALS_HASH}
|
||||
hass.config_entries.async_update_entry(entry, data=data)
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except KasaException as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
device_config_dict = device.config.to_dict(
|
||||
credentials_hash=device.credentials_hash, exclude_credentials=True
|
||||
)
|
||||
device_credentials_hash = device.credentials_hash
|
||||
device_config_dict = device.config.to_dict(exclude_credentials=True)
|
||||
# Do not store the credentials hash inside the device_config
|
||||
device_config_dict.pop(CONF_CREDENTIALS_HASH, None)
|
||||
updates: dict[str, Any] = {}
|
||||
if device_credentials_hash and device_credentials_hash != entry_credentials_hash:
|
||||
updates[CONF_CREDENTIALS_HASH] = device_credentials_hash
|
||||
if device_config_dict != config_dict:
|
||||
updates[CONF_DEVICE_CONFIG] = device_config_dict
|
||||
if entry.data.get(CONF_ALIAS) != device.alias:
|
||||
@@ -268,6 +283,28 @@ def mac_alias(mac: str) -> str:
|
||||
return mac.replace(":", "")[-4:].upper()
|
||||
|
||||
|
||||
def _mac_connection_or_none(device: dr.DeviceEntry) -> str | None:
|
||||
return next(
|
||||
(
|
||||
conn
|
||||
for type_, conn in device.connections
|
||||
if type_ == dr.CONNECTION_NETWORK_MAC
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None:
|
||||
# Previously only iot devices had child devices and iot devices use
|
||||
# the upper and lcase MAC addresses as device_id so match on case
|
||||
# insensitive mac address as the parent device.
|
||||
upper_mac = mac.upper()
|
||||
return next(
|
||||
(device_id for device_id in device_ids if device_id.upper() == upper_mac),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
version = config_entry.version
|
||||
@@ -284,49 +321,66 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
# always be linked into one device.
|
||||
dev_reg = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id):
|
||||
new_identifiers: set[tuple[str, str]] | None = None
|
||||
if len(device.identifiers) > 1 and (
|
||||
mac := next(
|
||||
iter(
|
||||
[
|
||||
conn[1]
|
||||
for conn in device.connections
|
||||
if conn[0] == dr.CONNECTION_NETWORK_MAC
|
||||
]
|
||||
),
|
||||
None,
|
||||
original_identifiers = device.identifiers
|
||||
# Get only the tplink identifier, could be tapo or other integrations.
|
||||
tplink_identifiers = [
|
||||
ident[1] for ident in original_identifiers if ident[0] == DOMAIN
|
||||
]
|
||||
# Nothing to fix if there's only one identifier. mac connection
|
||||
# should never be none but if it is there's no problem.
|
||||
if len(tplink_identifiers) <= 1 or not (
|
||||
mac := _mac_connection_or_none(device)
|
||||
):
|
||||
continue
|
||||
if not (
|
||||
tplink_parent_device_id := _device_id_is_mac_or_none(
|
||||
mac, tplink_identifiers
|
||||
)
|
||||
):
|
||||
for identifier in device.identifiers:
|
||||
# Previously only iot devices that use the MAC address as
|
||||
# device_id had child devices so check for mac as the
|
||||
# parent device.
|
||||
if identifier[0] == DOMAIN and identifier[1].upper() == mac.upper():
|
||||
new_identifiers = {identifier}
|
||||
break
|
||||
if new_identifiers:
|
||||
dev_reg.async_update_device(
|
||||
device.id, new_identifiers=new_identifiers
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Replaced identifiers for device %s (%s): %s with: %s",
|
||||
device.name,
|
||||
device.model,
|
||||
device.identifiers,
|
||||
new_identifiers,
|
||||
)
|
||||
else:
|
||||
# No match on mac so raise an error.
|
||||
_LOGGER.error(
|
||||
"Unable to replace identifiers for device %s (%s): %s",
|
||||
device.name,
|
||||
device.model,
|
||||
device.identifiers,
|
||||
)
|
||||
# No match on mac so raise an error.
|
||||
_LOGGER.error(
|
||||
"Unable to replace identifiers for device %s (%s): %s",
|
||||
device.name,
|
||||
device.model,
|
||||
device.identifiers,
|
||||
)
|
||||
continue
|
||||
# Retain any identifiers for other domains
|
||||
new_identifiers = {
|
||||
ident for ident in device.identifiers if ident[0] != DOMAIN
|
||||
}
|
||||
new_identifiers.add((DOMAIN, tplink_parent_device_id))
|
||||
dev_reg.async_update_device(device.id, new_identifiers=new_identifiers)
|
||||
_LOGGER.debug(
|
||||
"Replaced identifiers for device %s (%s): %s with: %s",
|
||||
device.name,
|
||||
device.model,
|
||||
original_identifiers,
|
||||
new_identifiers,
|
||||
)
|
||||
|
||||
minor_version = 3
|
||||
hass.config_entries.async_update_entry(config_entry, minor_version=3)
|
||||
|
||||
_LOGGER.debug("Migration to version %s.%s successful", version, minor_version)
|
||||
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
|
||||
|
||||
if version == 1 and minor_version == 3:
|
||||
# credentials_hash stored in the device_config should be moved to data.
|
||||
updates: dict[str, Any] = {}
|
||||
if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
|
||||
assert isinstance(config_dict, dict)
|
||||
if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None):
|
||||
updates[CONF_CREDENTIALS_HASH] = credentials_hash
|
||||
updates[CONF_DEVICE_CONFIG] = config_dict
|
||||
minor_version = 4
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**config_entry.data,
|
||||
**updates,
|
||||
},
|
||||
minor_version=minor_version,
|
||||
)
|
||||
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
|
||||
|
||||
return True
|
||||
|
||||
@@ -44,7 +44,13 @@ from . import (
|
||||
mac_alias,
|
||||
set_credentials,
|
||||
)
|
||||
from .const import CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DOMAIN
|
||||
from .const import (
|
||||
CONF_CONNECTION_TYPE,
|
||||
CONF_CREDENTIALS_HASH,
|
||||
CONF_DEVICE_CONFIG,
|
||||
CONNECT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
STEP_AUTH_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
@@ -55,7 +61,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for tplink."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 3
|
||||
MINOR_VERSION = 4
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -95,9 +101,18 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG)
|
||||
if entry_config_dict == config and entry_data[CONF_HOST] == host:
|
||||
return None
|
||||
updates = {**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host}
|
||||
# If the connection parameters have changed the credentials_hash will be invalid.
|
||||
if (
|
||||
entry_config_dict
|
||||
and isinstance(entry_config_dict, dict)
|
||||
and entry_config_dict.get(CONF_CONNECTION_TYPE)
|
||||
!= config.get(CONF_CONNECTION_TYPE)
|
||||
):
|
||||
updates.pop(CONF_CREDENTIALS_HASH, None)
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data={**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host},
|
||||
data=updates,
|
||||
reason="already_configured",
|
||||
)
|
||||
|
||||
@@ -345,18 +360,22 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult:
|
||||
"""Create a config entry from a smart device."""
|
||||
# This is only ever called after a successful device update so we know that
|
||||
# the credential_hash is correct and should be saved.
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: device.host})
|
||||
data = {
|
||||
CONF_HOST: device.host,
|
||||
CONF_ALIAS: device.alias,
|
||||
CONF_MODEL: device.model,
|
||||
CONF_DEVICE_CONFIG: device.config.to_dict(
|
||||
exclude_credentials=True,
|
||||
),
|
||||
}
|
||||
if device.credentials_hash:
|
||||
data[CONF_CREDENTIALS_HASH] = device.credentials_hash
|
||||
return self.async_create_entry(
|
||||
title=f"{device.alias} {device.model}",
|
||||
data={
|
||||
CONF_HOST: device.host,
|
||||
CONF_ALIAS: device.alias,
|
||||
CONF_MODEL: device.model,
|
||||
CONF_DEVICE_CONFIG: device.config.to_dict(
|
||||
credentials_hash=device.credentials_hash,
|
||||
exclude_credentials=True,
|
||||
),
|
||||
},
|
||||
data=data,
|
||||
)
|
||||
|
||||
async def _async_try_discover_and_update(
|
||||
|
||||
@@ -20,6 +20,8 @@ ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh"
|
||||
ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh"
|
||||
|
||||
CONF_DEVICE_CONFIG: Final = "device_config"
|
||||
CONF_CREDENTIALS_HASH: Final = "credentials_hash"
|
||||
CONF_CONNECTION_TYPE: Final = "connection_type"
|
||||
|
||||
PLATFORMS: Final = [
|
||||
Platform.BINARY_SENSOR,
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/twinkly",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ttls"],
|
||||
"requirements": ["ttls==1.5.1"]
|
||||
"requirements": ["ttls==1.8.3"]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine SE"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine Pro Max"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ def async_device_uptime_value_fn(hub: UnifiHub, device: Device) -> datetime | No
|
||||
|
||||
|
||||
@callback
|
||||
def async_device_uptime_value_changed_fn(
|
||||
def async_uptime_value_changed_fn(
|
||||
old: StateType | date | datetime | Decimal, new: datetime | float | str | None
|
||||
) -> bool:
|
||||
"""Reject the new uptime value if it's too similar to the old one. Avoids unwanted fluctuation."""
|
||||
@@ -310,6 +310,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = (
|
||||
supported_fn=lambda hub, _: hub.config.option_allow_uptime_sensors,
|
||||
unique_id_fn=lambda hub, obj_id: f"uptime-{obj_id}",
|
||||
value_fn=async_client_uptime_value_fn,
|
||||
value_changed_fn=async_uptime_value_changed_fn,
|
||||
),
|
||||
UnifiSensorEntityDescription[Wlans, Wlan](
|
||||
key="WLAN clients",
|
||||
@@ -396,7 +397,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = (
|
||||
object_fn=lambda api, obj_id: api.devices[obj_id],
|
||||
unique_id_fn=lambda hub, obj_id: f"device_uptime-{obj_id}",
|
||||
value_fn=async_device_uptime_value_fn,
|
||||
value_changed_fn=async_device_uptime_value_changed_fn,
|
||||
value_changed_fn=async_uptime_value_changed_fn,
|
||||
),
|
||||
UnifiSensorEntityDescription[Devices, Device](
|
||||
key="Device temperature",
|
||||
|
||||
@@ -426,14 +426,12 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
icon="mdi:doorbell-video",
|
||||
ufp_required_field="feature_flags.is_doorbell",
|
||||
ufp_value="is_ringing",
|
||||
ufp_event_obj="last_ring_event",
|
||||
),
|
||||
ProtectBinaryEventEntityDescription(
|
||||
key="motion",
|
||||
name="Motion",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
ufp_value="is_motion_currently_detected",
|
||||
ufp_enabled="is_motion_detection_on",
|
||||
ufp_event_obj="last_motion_event",
|
||||
),
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==3.7.0", "unifi-discovery==1.1.8"],
|
||||
"requirements": ["uiprotect==4.2.0", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
@@ -53,6 +53,10 @@
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine SE"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine Pro Max"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/upb",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["upb_lib"],
|
||||
"requirements": ["upb-lib==0.5.6"]
|
||||
"requirements": ["upb-lib==0.5.7"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiowebostv"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiowebostv==0.4.0"],
|
||||
"requirements": ["aiowebostv==0.4.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:lge-com:service:webos-second-screen:1"
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiowithings"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiowithings==3.0.1"]
|
||||
"requirements": ["aiowithings==3.0.2"]
|
||||
}
|
||||
|
||||
@@ -305,16 +305,20 @@ async def async_send_message( # noqa: C901
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
async def upload_file_from_path(self, path, timeout=None):
|
||||
def _read_upload_file(self, path: str) -> bytes:
|
||||
"""Read file from path."""
|
||||
with open(path, "rb") as upfile:
|
||||
_LOGGER.debug("Reading file %s", path)
|
||||
return upfile.read()
|
||||
|
||||
async def upload_file_from_path(self, path: str, timeout=None):
|
||||
"""Upload a file from a local file path via XEP_0363."""
|
||||
_LOGGER.info("Uploading file from path, %s", path)
|
||||
|
||||
if not hass.config.is_allowed_path(path):
|
||||
raise PermissionError("Could not access file. Path not allowed")
|
||||
|
||||
with open(path, "rb") as upfile:
|
||||
_LOGGER.debug("Reading file %s", path)
|
||||
input_file = upfile.read()
|
||||
input_file = await hass.async_add_executor_job(self._read_upload_file, path)
|
||||
filesize = len(input_file)
|
||||
_LOGGER.debug("Filesize is %s bytes", filesize)
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
"requirements": [
|
||||
"bellows==0.39.1",
|
||||
"pyserial==3.5",
|
||||
"zha-quirks==0.0.116",
|
||||
"zigpy-deconz==0.23.1",
|
||||
"zha-quirks==0.0.117",
|
||||
"zigpy-deconz==0.23.2",
|
||||
"zigpy==0.64.1",
|
||||
"zigpy-xbee==0.20.1",
|
||||
"zigpy-zigate==0.12.0",
|
||||
"zigpy-znp==0.12.1",
|
||||
"zigpy-zigate==0.12.1",
|
||||
"zigpy-znp==0.12.2",
|
||||
"universal-silabs-flasher==0.0.20",
|
||||
"pyserial-asyncio-fast==0.11"
|
||||
],
|
||||
|
||||
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 7
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
PATCH_VERSION: Final = "0b6"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
||||
@@ -158,26 +158,29 @@ class ConfigSource(enum.StrEnum):
|
||||
YAML = "yaml"
|
||||
|
||||
|
||||
class EventStateChangedData(TypedDict):
|
||||
class EventStateEventData(TypedDict):
|
||||
"""Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data."""
|
||||
|
||||
entity_id: str
|
||||
new_state: State | None
|
||||
|
||||
|
||||
class EventStateChangedData(EventStateEventData):
|
||||
"""EVENT_STATE_CHANGED data.
|
||||
|
||||
A state changed event is fired when on state write when the state is changed.
|
||||
"""
|
||||
|
||||
entity_id: str
|
||||
old_state: State | None
|
||||
new_state: State | None
|
||||
|
||||
|
||||
class EventStateReportedData(TypedDict):
|
||||
class EventStateReportedData(EventStateEventData):
|
||||
"""EVENT_STATE_REPORTED data.
|
||||
|
||||
A state reported event is fired when on state write when the state is unchanged.
|
||||
"""
|
||||
|
||||
entity_id: str
|
||||
old_last_reported: datetime.datetime
|
||||
new_state: State | None
|
||||
|
||||
|
||||
# SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead
|
||||
|
||||
@@ -297,6 +297,10 @@ SSDP = {
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine SE",
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine Pro Max",
|
||||
},
|
||||
],
|
||||
"unifiprotect": [
|
||||
{
|
||||
@@ -311,6 +315,10 @@ SSDP = {
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine SE",
|
||||
},
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
"modelDescription": "UniFi Dream Machine Pro Max",
|
||||
},
|
||||
],
|
||||
"upnp": [
|
||||
{
|
||||
|
||||
@@ -869,6 +869,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
)
|
||||
add_config_entry = config_entry
|
||||
|
||||
if not new_connections and not new_identifiers:
|
||||
raise HomeAssistantError(
|
||||
"A device must have at least one of identifiers or connections"
|
||||
)
|
||||
|
||||
if merge_connections is not UNDEFINED and new_connections is not UNDEFINED:
|
||||
raise HomeAssistantError(
|
||||
"Cannot define both merge_connections and new_connections"
|
||||
|
||||
@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar
|
||||
from homeassistant.const import (
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
EVENT_STATE_CHANGED,
|
||||
EVENT_STATE_REPORTED,
|
||||
MATCH_ALL,
|
||||
SUN_EVENT_SUNRISE,
|
||||
SUN_EVENT_SUNSET,
|
||||
@@ -26,6 +27,8 @@ from homeassistant.core import (
|
||||
Event,
|
||||
# Explicit reexport of 'EventStateChangedData' for backwards compatibility
|
||||
EventStateChangedData as EventStateChangedData, # noqa: PLC0414
|
||||
EventStateEventData,
|
||||
EventStateReportedData,
|
||||
HassJob,
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
@@ -57,6 +60,9 @@ from .typing import TemplateVarsType
|
||||
_TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey(
|
||||
"track_state_change_data"
|
||||
)
|
||||
_TRACK_STATE_REPORT_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey(
|
||||
"track_state_report_data"
|
||||
)
|
||||
_TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = (
|
||||
HassKey("track_state_added_domain_data")
|
||||
)
|
||||
@@ -84,6 +90,7 @@ RANDOM_MICROSECOND_MIN = 50000
|
||||
RANDOM_MICROSECOND_MAX = 500000
|
||||
|
||||
_TypedDictT = TypeVar("_TypedDictT", bound=Mapping[str, Any])
|
||||
_StateEventDataT = TypeVar("_StateEventDataT", bound=EventStateEventData)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
@@ -324,8 +331,8 @@ def async_track_state_change_event(
|
||||
@callback
|
||||
def _async_dispatch_entity_id_event(
|
||||
hass: HomeAssistant,
|
||||
callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
|
||||
event: Event[EventStateChangedData],
|
||||
callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]],
|
||||
event: Event[_StateEventDataT],
|
||||
) -> None:
|
||||
"""Dispatch to listeners."""
|
||||
if not (callbacks_list := callbacks.get(event.data["entity_id"])):
|
||||
@@ -342,10 +349,10 @@ def _async_dispatch_entity_id_event(
|
||||
|
||||
|
||||
@callback
|
||||
def _async_state_change_filter(
|
||||
def _async_state_filter(
|
||||
hass: HomeAssistant,
|
||||
callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
|
||||
event_data: EventStateChangedData,
|
||||
callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]],
|
||||
event_data: _StateEventDataT,
|
||||
) -> bool:
|
||||
"""Filter state changes by entity_id."""
|
||||
return event_data["entity_id"] in callbacks
|
||||
@@ -355,7 +362,7 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker(
|
||||
key=_TRACK_STATE_CHANGE_DATA,
|
||||
event_type=EVENT_STATE_CHANGED,
|
||||
dispatcher_callable=_async_dispatch_entity_id_event,
|
||||
filter_callable=_async_state_change_filter,
|
||||
filter_callable=_async_state_filter,
|
||||
)
|
||||
|
||||
|
||||
@@ -372,6 +379,26 @@ def _async_track_state_change_event(
|
||||
)
|
||||
|
||||
|
||||
_KEYED_TRACK_STATE_REPORT = _KeyedEventTracker(
|
||||
key=_TRACK_STATE_REPORT_DATA,
|
||||
event_type=EVENT_STATE_REPORTED,
|
||||
dispatcher_callable=_async_dispatch_entity_id_event,
|
||||
filter_callable=_async_state_filter,
|
||||
)
|
||||
|
||||
|
||||
def async_track_state_report_event(
|
||||
hass: HomeAssistant,
|
||||
entity_ids: str | Iterable[str],
|
||||
action: Callable[[Event[EventStateReportedData]], Any],
|
||||
job_type: HassJobType | None = None,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Track EVENT_STATE_REPORTED by entity_id without lowercasing."""
|
||||
return _async_track_event(
|
||||
_KEYED_TRACK_STATE_REPORT, hass, entity_ids, action, job_type
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _remove_empty_listener() -> None:
|
||||
"""Remove a listener that does nothing."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user