forked from home-assistant/core
Compare commits
124 Commits
2024.4.0b1
...
2024.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04072cb3c1 | ||
|
|
05082fcceb | ||
|
|
8c0b44d6d5 | ||
|
|
9a342f87c0 | ||
|
|
d062ef357b | ||
|
|
265d04c593 | ||
|
|
fc9653581a | ||
|
|
7bea6eface | ||
|
|
19f3ef763d | ||
|
|
fa88975055 | ||
|
|
1322f38911 | ||
|
|
75127105b9 | ||
|
|
d6793a756f | ||
|
|
20e88255df | ||
|
|
2f9f1008a5 | ||
|
|
90bc21b7f6 | ||
|
|
b29eb317bd | ||
|
|
dcd9d987a7 | ||
|
|
e26ea40570 | ||
|
|
0a2d79f63e | ||
|
|
d1b1d6388f | ||
|
|
93569e3827 | ||
|
|
61a359e4d2 | ||
|
|
b1fb77cb4d | ||
|
|
95606135a6 | ||
|
|
47d9879c0c | ||
|
|
e3c111b1dd | ||
|
|
9937743863 | ||
|
|
ed3daed869 | ||
|
|
5d5dc24b33 | ||
|
|
c39d6f0730 | ||
|
|
87ffd5ac56 | ||
|
|
71877fdeda | ||
|
|
2434a22e4e | ||
|
|
618fa08ab2 | ||
|
|
96003e3562 | ||
|
|
411e55d059 | ||
|
|
58533f02af | ||
|
|
aa14793479 | ||
|
|
0191d3e41b | ||
|
|
319f76cdc8 | ||
|
|
530725bbfa | ||
|
|
d8ae7d6955 | ||
|
|
3d0bafbdc9 | ||
|
|
ef8e54877f | ||
|
|
a39e1a6428 | ||
|
|
450be67406 | ||
|
|
25289e0ca1 | ||
|
|
d983fa6da7 | ||
|
|
b61397656c | ||
|
|
590546a9a5 | ||
|
|
9ba4d26abd | ||
|
|
aa33da546d | ||
|
|
3845523a27 | ||
|
|
6a7fad0228 | ||
|
|
33f07ce035 | ||
|
|
4302c5c273 | ||
|
|
b2df1b1c03 | ||
|
|
0aa134459b | ||
|
|
0ca3700c16 | ||
|
|
35ff633d99 | ||
|
|
7a2f6ce430 | ||
|
|
7cb603a226 | ||
|
|
43562289e4 | ||
|
|
79fa7caa41 | ||
|
|
8bdb27c88b | ||
|
|
f676448f27 | ||
|
|
639c4a843b | ||
|
|
02dee34338 | ||
|
|
4e0290ce0e | ||
|
|
fa2f49693c | ||
|
|
2ce784105d | ||
|
|
85fb4a27a3 | ||
|
|
8cbedbe26b | ||
|
|
5bd52da13a | ||
|
|
d53848aae4 | ||
|
|
4e0d6f287e | ||
|
|
5af5f3694e | ||
|
|
b539b25682 | ||
|
|
ca31479d29 | ||
|
|
92dfec3c98 | ||
|
|
230c29edbe | ||
|
|
559fe65471 | ||
|
|
384d10a51d | ||
|
|
e5a620545c | ||
|
|
7b84e86f89 | ||
|
|
18b6de567d | ||
|
|
a6076a0d33 | ||
|
|
7164993562 | ||
|
|
bc21836e7e | ||
|
|
52612b10fd | ||
|
|
623d85ecaa | ||
|
|
43631d5944 | ||
|
|
112aab47fb | ||
|
|
ea13f102e0 | ||
|
|
bb33725e7f | ||
|
|
bd6890ab83 | ||
|
|
25c611ffc4 | ||
|
|
fc24b61859 | ||
|
|
71588b5c22 | ||
|
|
14dfb6a255 | ||
|
|
ef97255d9c | ||
|
|
e8afdd67d0 | ||
|
|
008e4413b5 | ||
|
|
c373d40e34 | ||
|
|
bdf51553ef | ||
|
|
f2edc15687 | ||
|
|
286a09d737 | ||
|
|
e8ee2fd25c | ||
|
|
11b8b01cde | ||
|
|
4f761c25d8 | ||
|
|
953ceb0d8d | ||
|
|
e53672250f | ||
|
|
84901f1983 | ||
|
|
e4d973e8a2 | ||
|
|
cdd7ce435a | ||
|
|
c7ce53cc49 | ||
|
|
db7d0a0ee9 | ||
|
|
906febadef | ||
|
|
bc740f95c9 | ||
|
|
bf4e527f44 | ||
|
|
35e582a240 | ||
|
|
65d25bd780 | ||
|
|
b8a2c14813 |
@@ -1249,6 +1249,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/sms/ @ocalvo
|
||||
/homeassistant/components/snapcast/ @luar123
|
||||
/tests/components/snapcast/ @luar123
|
||||
/homeassistant/components/snmp/ @nmaggioni
|
||||
/tests/components/snmp/ @nmaggioni
|
||||
/homeassistant/components/snooz/ @AustinBrunkhorst
|
||||
/tests/components/snooz/ @AustinBrunkhorst
|
||||
/homeassistant/components/solaredge/ @frenck
|
||||
|
||||
@@ -93,6 +93,11 @@ from .util.async_ import create_eager_task
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
from .util.package import async_get_user_site, is_virtual_env
|
||||
|
||||
with contextlib.suppress(ImportError):
|
||||
# Ensure anyio backend is imported to avoid it being imported in the event loop
|
||||
from anyio._backends import _asyncio # noqa: F401
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .runner import RuntimeConfig
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.4.6"]
|
||||
"requirements": ["aioairzone-cloud==0.4.7"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import logging
|
||||
from time import monotonic
|
||||
|
||||
from aiohttp import ClientError
|
||||
from yalexs.activity import Activity, ActivityType
|
||||
@@ -26,9 +27,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ACTIVITY_STREAM_FETCH_LIMIT = 10
|
||||
ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500
|
||||
|
||||
INITIAL_LOCK_RESYNC_TIME = 60
|
||||
|
||||
# If there is a storm of activity (ie lock, unlock, door open, door close, etc)
|
||||
# we want to debounce the updates so we don't hammer the activity api too much.
|
||||
ACTIVITY_DEBOUNCE_COOLDOWN = 3
|
||||
ACTIVITY_DEBOUNCE_COOLDOWN = 4
|
||||
|
||||
|
||||
@callback
|
||||
@@ -62,6 +65,7 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
self.pubnub = pubnub
|
||||
self._update_debounce: dict[str, Debouncer] = {}
|
||||
self._update_debounce_jobs: dict[str, HassJob] = {}
|
||||
self._start_time: float | None = None
|
||||
|
||||
@callback
|
||||
def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None:
|
||||
@@ -70,6 +74,7 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Token refresh check and catch up the activity stream."""
|
||||
self._start_time = monotonic()
|
||||
update_debounce = self._update_debounce
|
||||
update_debounce_jobs = self._update_debounce_jobs
|
||||
for house_id in self._house_ids:
|
||||
@@ -140,11 +145,25 @@ class ActivityStream(AugustSubscriberMixin):
|
||||
|
||||
debouncer = self._update_debounce[house_id]
|
||||
debouncer.async_schedule_call()
|
||||
|
||||
# Schedule two updates past the debounce time
|
||||
# to ensure we catch the case where the activity
|
||||
# api does not update right away and we need to poll
|
||||
# it again. Sometimes the lock operator or a doorbell
|
||||
# will not show up in the activity stream right away.
|
||||
# Only do additional polls if we are past
|
||||
# the initial lock resync time to avoid a storm
|
||||
# of activity at setup.
|
||||
if (
|
||||
not self._start_time
|
||||
or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Skipping additional updates due to ongoing initial lock resync time"
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug("Scheduling additional updates for house id %s", house_id)
|
||||
job = self._update_debounce_jobs[house_id]
|
||||
for step in (1, 2):
|
||||
future_updates.append(
|
||||
|
||||
@@ -40,7 +40,7 @@ ATTR_OPERATION_TAG = "tag"
|
||||
# Limit battery, online, and hardware updates to hourly
|
||||
# in order to reduce the number of api requests and
|
||||
# avoid hitting rate limits
|
||||
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1)
|
||||
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24)
|
||||
|
||||
# Activity needs to be checked more frequently as the
|
||||
# doorbell motion and rings are included here
|
||||
|
||||
@@ -49,9 +49,17 @@ class AugustSubscriberMixin:
|
||||
"""Call the refresh method."""
|
||||
self._hass.async_create_task(self._async_refresh(now), eager_start=True)
|
||||
|
||||
@callback
|
||||
def _async_cancel_update_interval(self, _: Event | None = None) -> None:
|
||||
"""Cancel the scheduled update."""
|
||||
if self._unsub_interval:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
|
||||
@callback
|
||||
def _async_setup_listeners(self) -> None:
|
||||
"""Create interval and stop listeners."""
|
||||
self._async_cancel_update_interval()
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self._hass,
|
||||
self._async_scheduled_refresh,
|
||||
@@ -59,17 +67,12 @@ class AugustSubscriberMixin:
|
||||
name="august refresh",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_cancel_update_interval(_: Event) -> None:
|
||||
self._stop_interval = None
|
||||
if self._unsub_interval:
|
||||
self._unsub_interval()
|
||||
|
||||
self._stop_interval = self._hass.bus.async_listen(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
_async_cancel_update_interval,
|
||||
run_immediately=True,
|
||||
)
|
||||
if not self._stop_interval:
|
||||
self._stop_interval = self._hass.bus.async_listen(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
self._async_cancel_update_interval,
|
||||
run_immediately=True,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_unsubscribe_device_id(
|
||||
@@ -82,13 +85,7 @@ class AugustSubscriberMixin:
|
||||
|
||||
if self._subscriptions:
|
||||
return
|
||||
|
||||
if self._unsub_interval:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
if self._stop_interval:
|
||||
self._stop_interval()
|
||||
self._stop_interval = None
|
||||
self._async_cancel_update_interval()
|
||||
|
||||
@callback
|
||||
def async_signal_device_id_update(self, device_id: str) -> None:
|
||||
|
||||
@@ -56,6 +56,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
||||
mjpeg_url=self.mjpeg_source,
|
||||
still_image_url=self.image_source,
|
||||
authentication=HTTP_DIGEST_AUTHENTICATION,
|
||||
verify_ssl=False,
|
||||
unique_id=f"{hub.unique_id}-camera",
|
||||
)
|
||||
|
||||
@@ -74,16 +75,18 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
||||
|
||||
Additionally used when device change IP address.
|
||||
"""
|
||||
proto = self.hub.config.protocol
|
||||
host = self.hub.config.host
|
||||
port = self.hub.config.port
|
||||
|
||||
image_options = self.generate_options(skip_stream_profile=True)
|
||||
self._still_image_url = (
|
||||
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
|
||||
f"/jpg/image.cgi{image_options}"
|
||||
f"{proto}://{host}:{port}/axis-cgi/jpg/image.cgi{image_options}"
|
||||
)
|
||||
|
||||
mjpeg_options = self.generate_options()
|
||||
self._mjpeg_url = (
|
||||
f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi"
|
||||
f"/mjpg/video.cgi{mjpeg_options}"
|
||||
f"{proto}://{host}:{port}/axis-cgi/mjpg/video.cgi{mjpeg_options}"
|
||||
)
|
||||
|
||||
stream_options = self.generate_options(add_video_codec_h264=True)
|
||||
@@ -95,10 +98,7 @@ class AxisCamera(AxisEntity, MjpegCamera):
|
||||
self.hub.additional_diagnostics["camera_sources"] = {
|
||||
"Image": self._still_image_url,
|
||||
"MJPEG": self._mjpeg_url,
|
||||
"Stream": (
|
||||
f"rtsp://user:pass@{self.hub.config.host}/axis-media"
|
||||
f"/media.amp{stream_options}"
|
||||
),
|
||||
"Stream": (f"rtsp://user:pass@{host}/axis-media/media.amp{stream_options}"),
|
||||
}
|
||||
|
||||
@property
|
||||
|
||||
@@ -168,16 +168,13 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
self, entry_data: Mapping[str, Any], keep_password: bool
|
||||
) -> ConfigFlowResult:
|
||||
"""Re-run configuration step."""
|
||||
protocol = entry_data.get(CONF_PROTOCOL, "http")
|
||||
password = entry_data[CONF_PASSWORD] if keep_password else ""
|
||||
self.discovery_schema = {
|
||||
vol.Required(
|
||||
CONF_PROTOCOL, default=entry_data.get(CONF_PROTOCOL, "http")
|
||||
): str,
|
||||
vol.Required(CONF_PROTOCOL, default=protocol): vol.In(PROTOCOL_CHOICES),
|
||||
vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str,
|
||||
vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str,
|
||||
vol.Required(
|
||||
CONF_PASSWORD,
|
||||
default=entry_data[CONF_PASSWORD] if keep_password else "",
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD, default=password): str,
|
||||
vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int,
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_PROTOCOL,
|
||||
CONF_TRIGGER_TIME,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
@@ -31,6 +32,7 @@ class AxisConfig:
|
||||
|
||||
entry: ConfigEntry
|
||||
|
||||
protocol: str
|
||||
host: str
|
||||
port: int
|
||||
username: str
|
||||
@@ -54,6 +56,7 @@ class AxisConfig:
|
||||
options = config_entry.options
|
||||
return cls(
|
||||
entry=config_entry,
|
||||
protocol=config.get(CONF_PROTOCOL, "http"),
|
||||
host=config[CONF_HOST],
|
||||
username=config[CONF_USERNAME],
|
||||
password=config[CONF_PASSWORD],
|
||||
|
||||
@@ -116,7 +116,7 @@ class AxisHub:
|
||||
if status.status.state == ClientState.ACTIVE:
|
||||
self.config.entry.async_on_unload(
|
||||
await mqtt.async_subscribe(
|
||||
hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message
|
||||
hass, f"{status.config.device_topic_prefix}/#", self.mqtt_message
|
||||
)
|
||||
)
|
||||
|
||||
@@ -124,7 +124,8 @@ class AxisHub:
|
||||
def mqtt_message(self, message: ReceiveMessage) -> None:
|
||||
"""Receive Axis MQTT message."""
|
||||
self.disconnect_from_stream()
|
||||
|
||||
if message.topic.endswith("event/connection"):
|
||||
return
|
||||
event = mqtt_json_to_event(message.payload)
|
||||
self.api.event.handler(event)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==59"],
|
||||
"requirements": ["axis==61"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["brother==4.0.2"],
|
||||
"requirements": ["brother==4.1.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_printer._tcp.local.",
|
||||
|
||||
@@ -58,6 +58,7 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
@@ -75,6 +76,7 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
raise intent.NoStatesMatchedError(
|
||||
name=entity_name,
|
||||
area=None,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["hass_nabucasa"],
|
||||
"requirements": ["hass-nabucasa==0.79.0"]
|
||||
"requirements": ["hass-nabucasa==0.78.0"]
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
intent,
|
||||
start,
|
||||
template,
|
||||
@@ -163,7 +164,12 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
self.hass.bus.async_listen(
|
||||
ar.EVENT_AREA_REGISTRY_UPDATED,
|
||||
self._async_handle_area_registry_changed,
|
||||
self._async_handle_area_floor_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
fr.EVENT_FLOOR_REGISTRY_UPDATED,
|
||||
self._async_handle_area_floor_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
@@ -696,10 +702,13 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
return lang_intents
|
||||
|
||||
@core.callback
|
||||
def _async_handle_area_registry_changed(
|
||||
self, event: core.Event[ar.EventAreaRegistryUpdatedData]
|
||||
def _async_handle_area_floor_registry_changed(
|
||||
self,
|
||||
event: core.Event[
|
||||
ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData
|
||||
],
|
||||
) -> None:
|
||||
"""Clear area area cache when the area registry has changed."""
|
||||
"""Clear area/floor list cache when the area registry has changed."""
|
||||
self._slot_lists = None
|
||||
|
||||
@core.callback
|
||||
@@ -773,6 +782,8 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
# Default name
|
||||
entity_names.append((state.name, state.name, context))
|
||||
|
||||
_LOGGER.debug("Exposed entities: %s", entity_names)
|
||||
|
||||
# Expose all areas.
|
||||
#
|
||||
# We pass in area id here with the expectation that no two areas will
|
||||
@@ -788,11 +799,25 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
area_names.append((alias, area.id))
|
||||
|
||||
_LOGGER.debug("Exposed entities: %s", entity_names)
|
||||
# Expose all floors.
|
||||
#
|
||||
# We pass in floor id here with the expectation that no two floors will
|
||||
# share the same name or alias.
|
||||
floors = fr.async_get(self.hass)
|
||||
floor_names = []
|
||||
for floor in floors.async_list_floors():
|
||||
floor_names.append((floor.name, floor.floor_id))
|
||||
if floor.aliases:
|
||||
for alias in floor.aliases:
|
||||
if not alias.strip():
|
||||
continue
|
||||
|
||||
floor_names.append((alias, floor.floor_id))
|
||||
|
||||
self._slot_lists = {
|
||||
"area": TextSlotList.from_tuples(area_names, allow_template=False),
|
||||
"name": TextSlotList.from_tuples(entity_names, allow_template=False),
|
||||
"floor": TextSlotList.from_tuples(floor_names, allow_template=False),
|
||||
}
|
||||
|
||||
return self._slot_lists
|
||||
@@ -953,6 +978,10 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str
|
||||
# area only
|
||||
return ErrorKey.NO_AREA, {"area": unmatched_area}
|
||||
|
||||
if unmatched_floor := unmatched_text.get("floor"):
|
||||
# floor only
|
||||
return ErrorKey.NO_FLOOR, {"floor": unmatched_floor}
|
||||
|
||||
# Area may still have matched
|
||||
matched_area: str | None = None
|
||||
if matched_area_entity := result.entities.get("area"):
|
||||
@@ -1000,6 +1029,13 @@ def _get_no_states_matched_response(
|
||||
"area": no_states_error.area,
|
||||
}
|
||||
|
||||
if no_states_error.floor:
|
||||
# domain in floor
|
||||
return ErrorKey.NO_DOMAIN_IN_FLOOR, {
|
||||
"domain": domain,
|
||||
"floor": no_states_error.floor,
|
||||
}
|
||||
|
||||
# domain only
|
||||
return ErrorKey.NO_DOMAIN, {"domain": domain}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.27"]
|
||||
"requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.3"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"],
|
||||
"requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["async-upnp-client==0.38.2"],
|
||||
"requirements": ["async-upnp-client==0.38.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
||||
@@ -11,7 +11,11 @@ import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
@@ -43,6 +47,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
hass.async_create_task(_async_import_config(hass, config))
|
||||
return True
|
||||
|
||||
|
||||
async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Import the Downloader component from the YAML file."""
|
||||
|
||||
import_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
@@ -51,28 +62,40 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
},
|
||||
)
|
||||
|
||||
translation_key = "deprecated_yaml"
|
||||
if (
|
||||
import_result["type"] == FlowResultType.ABORT
|
||||
and import_result["reason"] == "import_failed"
|
||||
and import_result["reason"] != "single_instance_allowed"
|
||||
):
|
||||
translation_key = "import_failed"
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.9.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Downloader",
|
||||
},
|
||||
)
|
||||
return True
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="directory_does_not_exist",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Downloader",
|
||||
"url": "/config/integrations/dashboard/add?domain=downloader",
|
||||
},
|
||||
)
|
||||
else:
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Downloader",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -83,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not os.path.isabs(download_path):
|
||||
download_path = hass.config.path(download_path)
|
||||
|
||||
if not os.path.isdir(download_path):
|
||||
if not await hass.async_add_executor_job(os.path.isdir, download_path):
|
||||
_LOGGER.error(
|
||||
"Download path %s does not exist. File Downloader not active", download_path
|
||||
)
|
||||
|
||||
@@ -46,19 +46,24 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by configuration file."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
return await self.async_step_user(user_input)
|
||||
try:
|
||||
await self._validate_input(user_input)
|
||||
except DirectoryDoesNotExist:
|
||||
return self.async_abort(reason="directory_does_not_exist")
|
||||
return self.async_create_entry(title=DEFAULT_NAME, data=user_input)
|
||||
|
||||
async def _validate_input(self, user_input: dict[str, Any]) -> None:
|
||||
"""Validate the user input if the directory exists."""
|
||||
if not os.path.isabs(user_input[CONF_DOWNLOAD_DIR]):
|
||||
download_path = self.hass.config.path(user_input[CONF_DOWNLOAD_DIR])
|
||||
download_path = user_input[CONF_DOWNLOAD_DIR]
|
||||
if not os.path.isabs(download_path):
|
||||
download_path = self.hass.config.path(download_path)
|
||||
|
||||
if not os.path.isdir(download_path):
|
||||
if not await self.hass.async_add_executor_job(os.path.isdir, download_path):
|
||||
_LOGGER.error(
|
||||
"Download path %s does not exist. File Downloader not active",
|
||||
download_path,
|
||||
|
||||
@@ -37,13 +37,9 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"title": "The {integration_title} YAML configuration is being removed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration is already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
},
|
||||
"import_failed": {
|
||||
"directory_does_not_exist": {
|
||||
"title": "The {integration_title} failed to import",
|
||||
"description": "The {integration_title} integration failed to import.\n\nPlease check the logs for more details."
|
||||
"description": "The {integration_title} integration failed to import because the configured directory does not exist.\n\nEnsure the directory exists and restart Home Assistant to try again or remove the {integration_title} configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"services": {
|
||||
"restart": "mdi:restart",
|
||||
"start": "mdi:start",
|
||||
"start": "mdi:play",
|
||||
"stop": "mdi:stop"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ async def async_setup_entry(
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
)
|
||||
for device in controller.fibaro_devices[platform]
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flexit_bacnet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["flexit_bacnet==2.1.0"]
|
||||
"requirements": ["flexit_bacnet==2.2.1"]
|
||||
}
|
||||
|
||||
@@ -311,6 +311,17 @@ class FritzBoxTools(
|
||||
)
|
||||
return unregister_entity_updates
|
||||
|
||||
def _entity_states_update(self) -> dict:
|
||||
"""Run registered entity update calls."""
|
||||
entity_states = {}
|
||||
for key in list(self._entity_update_functions):
|
||||
if (update_fn := self._entity_update_functions.get(key)) is not None:
|
||||
_LOGGER.debug("update entity %s", key)
|
||||
entity_states[key] = update_fn(
|
||||
self.fritz_status, self.data["entity_states"].get(key)
|
||||
)
|
||||
return entity_states
|
||||
|
||||
async def _async_update_data(self) -> UpdateCoordinatorDataType:
|
||||
"""Update FritzboxTools data."""
|
||||
entity_data: UpdateCoordinatorDataType = {
|
||||
@@ -319,15 +330,9 @@ class FritzBoxTools(
|
||||
}
|
||||
try:
|
||||
await self.async_scan_devices()
|
||||
for key in list(self._entity_update_functions):
|
||||
_LOGGER.debug("update entity %s", key)
|
||||
entity_data["entity_states"][
|
||||
key
|
||||
] = await self.hass.async_add_executor_job(
|
||||
self._entity_update_functions[key],
|
||||
self.fritz_status,
|
||||
self.data["entity_states"].get(key),
|
||||
)
|
||||
entity_data["entity_states"] = await self.hass.async_add_executor_job(
|
||||
self._entity_states_update
|
||||
)
|
||||
if self.has_call_deflections:
|
||||
entity_data[
|
||||
"call_deflections"
|
||||
|
||||
@@ -141,7 +141,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
|
||||
# update old and user-configured config entries
|
||||
for entry in self._async_current_entries():
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if entry.data[CONF_HOST] == host:
|
||||
if uuid and not entry.unique_id:
|
||||
self.hass.config_entries.async_update_entry(entry, unique_id=uuid)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240328.0"]
|
||||
"requirements": ["home-assistant-frontend==20240404.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/fyta",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["fyta_cli==0.3.3"]
|
||||
"requirements": ["fyta_cli==0.3.5"]
|
||||
}
|
||||
|
||||
@@ -46,35 +46,35 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [
|
||||
translation_key="plant_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=PLANT_STATUS_LIST,
|
||||
value_fn=lambda value: PLANT_STATUS[value],
|
||||
value_fn=PLANT_STATUS.get,
|
||||
),
|
||||
FytaSensorEntityDescription(
|
||||
key="temperature_status",
|
||||
translation_key="temperature_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=PLANT_STATUS_LIST,
|
||||
value_fn=lambda value: PLANT_STATUS[value],
|
||||
value_fn=PLANT_STATUS.get,
|
||||
),
|
||||
FytaSensorEntityDescription(
|
||||
key="light_status",
|
||||
translation_key="light_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=PLANT_STATUS_LIST,
|
||||
value_fn=lambda value: PLANT_STATUS[value],
|
||||
value_fn=PLANT_STATUS.get,
|
||||
),
|
||||
FytaSensorEntityDescription(
|
||||
key="moisture_status",
|
||||
translation_key="moisture_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=PLANT_STATUS_LIST,
|
||||
value_fn=lambda value: PLANT_STATUS[value],
|
||||
value_fn=PLANT_STATUS.get,
|
||||
),
|
||||
FytaSensorEntityDescription(
|
||||
key="salinity_status",
|
||||
translation_key="salinity_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=PLANT_STATUS_LIST,
|
||||
value_fn=lambda value: PLANT_STATUS[value],
|
||||
value_fn=PLANT_STATUS.get,
|
||||
),
|
||||
FytaSensorEntityDescription(
|
||||
key="temperature",
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.45", "babel==2.13.1"]
|
||||
"requirements": ["holidays==0.46", "babel==2.13.1"]
|
||||
}
|
||||
|
||||
@@ -113,7 +113,11 @@ class HMThermostat(HMDevice, ClimateEntity):
|
||||
@property
|
||||
def preset_modes(self):
|
||||
"""Return a list of available preset modes."""
|
||||
return [HM_PRESET_MAP[mode] for mode in self._hmdevice.ACTIONNODE]
|
||||
return [
|
||||
HM_PRESET_MAP[mode]
|
||||
for mode in self._hmdevice.ACTIONNODE
|
||||
if mode in HM_PRESET_MAP
|
||||
]
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
|
||||
@@ -119,4 +119,5 @@ class PowerviewShadeButton(ShadeEntity, ButtonEntity):
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
await self.entity_description.press_action(self._shade)
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
await self.entity_description.press_action(self._shade)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@@ -25,6 +26,10 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData])
|
||||
"""Initialize DataUpdateCoordinator to gather data for specific Powerview Hub."""
|
||||
self.shades = shades
|
||||
self.hub = hub
|
||||
# The hub tends to crash if there are multiple radio operations at the same time
|
||||
# but it seems to handle all other requests that do not use RF without issue
|
||||
# so we have a lock to prevent multiple radio operations at the same time
|
||||
self.radio_operation_lock = asyncio.Lock()
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
|
||||
@@ -67,7 +67,8 @@ async def async_setup_entry(
|
||||
|
||||
for shade in pv_entry.shade_data.values():
|
||||
_LOGGER.debug("Initial refresh of shade: %s", shade.name)
|
||||
await shade.refresh(suppress_timeout=True) # default 15 second timeout
|
||||
async with coordinator.radio_operation_lock:
|
||||
await shade.refresh(suppress_timeout=True) # default 15 second timeout
|
||||
|
||||
entities: list[ShadeEntity] = []
|
||||
for shade in pv_entry.shade_data.values():
|
||||
@@ -207,7 +208,8 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
||||
async def _async_execute_move(self, move: ShadePosition) -> None:
|
||||
"""Execute a move that can affect multiple positions."""
|
||||
_LOGGER.debug("Move request %s: %s", self.name, move)
|
||||
response = await self._shade.move(move)
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
response = await self._shade.move(move)
|
||||
_LOGGER.debug("Move response %s: %s", self.name, response)
|
||||
|
||||
# Process the response from the hub (including new positions)
|
||||
@@ -318,7 +320,10 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
||||
# error if are already have one in flight
|
||||
return
|
||||
# suppress timeouts caused by hub nightly reboot
|
||||
await self._shade.refresh(suppress_timeout=True) # default 15 second timeout
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
await self._shade.refresh(
|
||||
suppress_timeout=True
|
||||
) # default 15 second timeout
|
||||
_LOGGER.debug("Process update %s: %s", self.name, self._shade.current_position)
|
||||
self._async_update_shade_data(self._shade.current_position)
|
||||
|
||||
|
||||
@@ -114,5 +114,6 @@ class PowerViewSelect(ShadeEntity, SelectEntity):
|
||||
"""Change the selected option."""
|
||||
await self.entity_description.select_fn(self._shade, option)
|
||||
# force update data to ensure new info is in coordinator
|
||||
await self._shade.refresh()
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
await self._shade.refresh(suppress_timeout=True)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -153,5 +153,6 @@ class PowerViewSensor(ShadeEntity, SensorEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Refresh sensor entity."""
|
||||
await self.entity_description.update_fn(self._shade)
|
||||
async with self.coordinator.radio_operation_lock:
|
||||
await self.entity_description.update_fn(self._shade)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"services": {
|
||||
"select_next": "mdi:skip",
|
||||
"select_next": "mdi:skip-next",
|
||||
"select_option": "mdi:check",
|
||||
"select_previous": "mdi:skip-previous",
|
||||
"select_first": "mdi:skip-backward",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==2.12.2",
|
||||
"xknxproject==3.7.0",
|
||||
"xknxproject==3.7.1",
|
||||
"knx-frontend==2024.1.20.105944"
|
||||
],
|
||||
"single_config_entry": true
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"requirements": ["pylitterbot==2023.4.9"]
|
||||
"requirements": ["pylitterbot==2023.4.11"]
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ LITTER_BOX_STATUS_STATE_MAP = {
|
||||
LitterBoxStatus.CLEAN_CYCLE: STATE_CLEANING,
|
||||
LitterBoxStatus.EMPTY_CYCLE: STATE_CLEANING,
|
||||
LitterBoxStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED,
|
||||
LitterBoxStatus.CAT_DETECTED: STATE_DOCKED,
|
||||
LitterBoxStatus.CAT_SENSOR_TIMING: STATE_DOCKED,
|
||||
LitterBoxStatus.DRAWER_FULL_1: STATE_DOCKED,
|
||||
LitterBoxStatus.DRAWER_FULL_2: STATE_DOCKED,
|
||||
|
||||
@@ -179,7 +179,7 @@ async def _get_dashboard_info(hass, url_path):
|
||||
"views": views,
|
||||
}
|
||||
|
||||
if config is None:
|
||||
if config is None or "views" not in config:
|
||||
return data
|
||||
|
||||
for idx, view in enumerate(config["views"]):
|
||||
|
||||
@@ -141,7 +141,7 @@ class LutronLight(LutronDevice, LightEntity):
|
||||
else:
|
||||
brightness = self._prev_brightness
|
||||
self._prev_brightness = brightness
|
||||
args = {"new_level": brightness}
|
||||
args = {"new_level": to_lutron_level(brightness)}
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
args["fade_time_seconds"] = kwargs[ATTR_TRANSITION]
|
||||
self._lutron_device.set_level(**args)
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"unjoin": "mdi:ungroup",
|
||||
"volume_down": "mdi:volume-minus",
|
||||
"volume_mute": "mdi:volume-mute",
|
||||
"volume_set": "mdi:volume",
|
||||
"volume_set": "mdi:volume-medium",
|
||||
"volume_up": "mdi:volume-plus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Motionblinds BLE integration."""
|
||||
"""Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -34,9 +34,9 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Motionblinds BLE integration."""
|
||||
"""Set up Motionblinds Bluetooth integration."""
|
||||
|
||||
_LOGGER.debug("Setting up Motionblinds BLE integration")
|
||||
_LOGGER.debug("Setting up Motionblinds Bluetooth integration")
|
||||
|
||||
# The correct time is needed for encryption
|
||||
_LOGGER.debug("Setting timezone for encryption: %s", hass.config.time_zone)
|
||||
@@ -46,7 +46,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Motionblinds BLE device from a config entry."""
|
||||
"""Set up Motionblinds Bluetooth device from a config entry."""
|
||||
|
||||
_LOGGER.debug("(%s) Setting up device", entry.data[CONF_MAC_CODE])
|
||||
|
||||
@@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Motionblinds BLE device from a config entry."""
|
||||
"""Unload Motionblinds Bluetooth device from a config entry."""
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Button entities for the Motionblinds BLE integration."""
|
||||
"""Button entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Config flow for Motionblinds BLE integration."""
|
||||
"""Config flow for Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_MAC_CODE): str})
|
||||
|
||||
|
||||
class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Motionblinds BLE."""
|
||||
"""Handle a config flow for Motionblinds Bluetooth."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a ConfigFlow."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Constants for the Motionblinds BLE integration."""
|
||||
"""Constants for the Motionblinds Bluetooth integration."""
|
||||
|
||||
ATTR_CONNECT = "connect"
|
||||
ATTR_DISCONNECT = "disconnect"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Cover entities for the Motionblinds BLE integration."""
|
||||
"""Cover entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Base entities for the Motionblinds BLE integration."""
|
||||
"""Base entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MotionblindsBLEEntity(Entity):
|
||||
"""Base class for Motionblinds BLE entities."""
|
||||
"""Base class for Motionblinds Bluetooth entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "motionblinds_ble",
|
||||
"name": "Motionblinds BLE",
|
||||
"name": "Motionblinds Bluetooth",
|
||||
"bluetooth": [
|
||||
{
|
||||
"local_name": "MOTION_*",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Select entities for the Motionblinds BLE integration."""
|
||||
"""Select entities for the Motionblinds Bluetooth integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from http import HTTPStatus
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from myuplink import MyUplinkAPI, get_manufacturer, get_system_name
|
||||
from myuplink import MyUplinkAPI, get_manufacturer, get_model, get_system_name
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
@@ -92,7 +92,7 @@ def create_devices(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=get_system_name(system),
|
||||
manufacturer=get_manufacturer(device),
|
||||
model=device.productName,
|
||||
model=get_model(device),
|
||||
sw_version=device.firmwareCurrent,
|
||||
serial_number=device.product_serial_number,
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/myuplink",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["myuplink==0.5.0"]
|
||||
"requirements": ["myuplink==0.6.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nobo_hub",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pynobo==1.8.0"]
|
||||
"requirements": ["pynobo==1.8.1"]
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
(CONF_REFRESH_TOKEN, client.refresh_token),
|
||||
(CONF_USER_UUID, client.user_uuid),
|
||||
):
|
||||
if entry.data[key] == value:
|
||||
if entry.data.get(key) == value:
|
||||
continue
|
||||
entry_updates["data"][key] = value
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.4.1"]
|
||||
"requirements": ["opower==0.4.3"]
|
||||
}
|
||||
|
||||
@@ -105,6 +105,22 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
|
||||
)
|
||||
== 1,
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.CORE_HEATING_STATUS,
|
||||
name="Heating status",
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE,
|
||||
name="Absence mode",
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.MODBUSLINK_DHW_BOOST_MODE,
|
||||
name="Boost mode",
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
),
|
||||
]
|
||||
|
||||
SUPPORTED_STATES = {
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import cast
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import HomeAssistantOverkizData
|
||||
@@ -27,15 +28,16 @@ async def async_setup_entry(
|
||||
"""Set up the Overkiz climate from a config entry."""
|
||||
data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
# Match devices based on the widget.
|
||||
entities_based_on_widget: list[Entity] = [
|
||||
WIDGET_TO_CLIMATE_ENTITY[device.widget](device.device_url, data.coordinator)
|
||||
for device in data.platforms[Platform.CLIMATE]
|
||||
if device.widget in WIDGET_TO_CLIMATE_ENTITY
|
||||
)
|
||||
]
|
||||
|
||||
# Match devices based on the widget and controllableName
|
||||
# This is for example used for Atlantic APC, where devices with different functionality share the same uiClass and widget.
|
||||
async_add_entities(
|
||||
# Match devices based on the widget and controllableName.
|
||||
# ie Atlantic APC
|
||||
entities_based_on_widget_and_controllable: list[Entity] = [
|
||||
WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget][
|
||||
cast(Controllable, device.controllable_name)
|
||||
](device.device_url, data.coordinator)
|
||||
@@ -43,14 +45,21 @@ async def async_setup_entry(
|
||||
if device.widget in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY
|
||||
and device.controllable_name
|
||||
in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget]
|
||||
)
|
||||
]
|
||||
|
||||
# Hitachi Air To Air Heat Pumps
|
||||
async_add_entities(
|
||||
# Match devices based on the widget and protocol.
|
||||
# #ie Hitachi Air To Air Heat Pumps
|
||||
entities_based_on_widget_and_protocol: list[Entity] = [
|
||||
WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget][device.protocol](
|
||||
device.device_url, data.coordinator
|
||||
)
|
||||
for device in data.platforms[Platform.CLIMATE]
|
||||
if device.widget in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY
|
||||
and device.protocol in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget]
|
||||
]
|
||||
|
||||
async_add_entities(
|
||||
entities_based_on_widget
|
||||
+ entities_based_on_widget_and_controllable
|
||||
+ entities_based_on_widget_and_protocol
|
||||
)
|
||||
|
||||
@@ -159,7 +159,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity):
|
||||
await self.async_set_heating_mode(PRESET_MODES_TO_OVERKIZ[preset_mode])
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
heating_mode = cast(
|
||||
str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE)
|
||||
@@ -179,7 +179,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity):
|
||||
return OVERKIZ_TO_PRESET_MODES[heating_mode]
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return hvac target temperature."""
|
||||
current_heating_profile = self.current_heating_profile
|
||||
if current_heating_profile in OVERKIZ_TEMPERATURE_STATE_BY_PROFILE:
|
||||
|
||||
@@ -3,16 +3,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import sleep
|
||||
from functools import cached_property
|
||||
from typing import Any, cast
|
||||
|
||||
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
|
||||
|
||||
from homeassistant.components.climate import PRESET_NONE, HVACMode
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
PRESET_NONE,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES
|
||||
|
||||
from ..coordinator import OverkizDataUpdateCoordinator
|
||||
from ..executor import OverkizExecutor
|
||||
from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone
|
||||
from .atlantic_pass_apc_zone_control import OVERKIZ_TO_HVAC_MODE
|
||||
|
||||
PRESET_SCHEDULE = "schedule"
|
||||
PRESET_MANUAL = "manual"
|
||||
@@ -24,32 +32,127 @@ OVERKIZ_MODE_TO_PRESET_MODES: dict[str, str] = {
|
||||
|
||||
PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_MODE_TO_PRESET_MODES.items()}
|
||||
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 20
|
||||
# Maps the HVAC current ZoneControl system operating mode.
|
||||
OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = {
|
||||
OverkizCommandParam.COOLING: HVACAction.COOLING,
|
||||
OverkizCommandParam.DRYING: HVACAction.DRYING,
|
||||
OverkizCommandParam.HEATING: HVACAction.HEATING,
|
||||
# There is no known way to differentiate OFF from Idle.
|
||||
OverkizCommandParam.STOP: HVACAction.OFF,
|
||||
}
|
||||
|
||||
HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE: dict[HVACAction, OverkizState] = {
|
||||
HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_PROFILE,
|
||||
HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_PROFILE,
|
||||
}
|
||||
|
||||
HVAC_ACTION_TO_OVERKIZ_MODE_STATE: dict[HVACAction, OverkizState] = {
|
||||
HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_MODE,
|
||||
HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_MODE,
|
||||
}
|
||||
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 1
|
||||
|
||||
SUPPORTED_FEATURES: ClimateEntityFeature = (
|
||||
ClimateEntityFeature.PRESET_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE: dict[
|
||||
OverkizCommandParam, tuple[HVACMode, ClimateEntityFeature]
|
||||
] = {
|
||||
OverkizCommandParam.COOLING: (
|
||||
HVACMode.COOL,
|
||||
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||
),
|
||||
OverkizCommandParam.HEATING: (
|
||||
HVACMode.HEAT,
|
||||
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE,
|
||||
),
|
||||
OverkizCommandParam.HEATING_AND_COOLING: (
|
||||
HVACMode.HEAT_COOL,
|
||||
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# Those device depends on a main probe that choose the operating mode (heating, cooling, ...)
|
||||
# Those device depends on a main probe that choose the operating mode (heating, cooling, ...).
|
||||
class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
"""Representation of Atlantic Pass APC Heating And Cooling Zone Control."""
|
||||
|
||||
_attr_target_temperature_step = PRECISION_HALVES
|
||||
|
||||
def __init__(
|
||||
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Init method."""
|
||||
super().__init__(device_url, coordinator)
|
||||
|
||||
# There is less supported functions, because they depend on the ZoneControl.
|
||||
if not self.is_using_derogated_temperature_fallback:
|
||||
# Modes are not configurable, they will follow current HVAC Mode of Zone Control.
|
||||
self._attr_hvac_modes = []
|
||||
# When using derogated temperature, we fallback to legacy behavior.
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return
|
||||
|
||||
# Those are available and tested presets on Shogun.
|
||||
self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
|
||||
self._attr_hvac_modes = []
|
||||
self._attr_supported_features = ClimateEntityFeature(0)
|
||||
|
||||
# Modes depends on device capabilities.
|
||||
if (thermal_configuration := self.thermal_configuration) is not None:
|
||||
(
|
||||
device_hvac_mode,
|
||||
climate_entity_feature,
|
||||
) = thermal_configuration
|
||||
self._attr_hvac_modes = [device_hvac_mode, HVACMode.OFF]
|
||||
self._attr_supported_features = climate_entity_feature
|
||||
|
||||
# Those are available and tested presets on Shogun.
|
||||
self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
|
||||
|
||||
# Those APC Heating and Cooling probes depends on the zone control device (main probe).
|
||||
# Only the base device (#1) can be used to get/set some states.
|
||||
# Like to retrieve and set the current operating mode (heating, cooling, drying, off).
|
||||
self.zone_control_device = self.executor.linked_device(
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX
|
||||
|
||||
self.zone_control_executor: OverkizExecutor | None = None
|
||||
|
||||
if (
|
||||
zone_control_device := self.executor.linked_device(
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX
|
||||
)
|
||||
) is not None:
|
||||
self.zone_control_executor = OverkizExecutor(
|
||||
zone_control_device.device_url,
|
||||
coordinator,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def thermal_configuration(self) -> tuple[HVACMode, ClimateEntityFeature] | None:
|
||||
"""Retrieve thermal configuration for this devices."""
|
||||
|
||||
if (
|
||||
(
|
||||
state_thermal_configuration := cast(
|
||||
OverkizCommandParam | None,
|
||||
self.executor.select_state(OverkizState.CORE_THERMAL_CONFIGURATION),
|
||||
)
|
||||
)
|
||||
is not None
|
||||
and state_thermal_configuration
|
||||
in OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE
|
||||
):
|
||||
return OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE[
|
||||
state_thermal_configuration
|
||||
]
|
||||
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def device_hvac_mode(self) -> HVACMode | None:
|
||||
"""ZoneControlZone device has a single possible mode."""
|
||||
|
||||
return (
|
||||
None
|
||||
if self.thermal_configuration is None
|
||||
else self.thermal_configuration[0]
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -61,21 +164,37 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
)
|
||||
|
||||
@property
|
||||
def zone_control_hvac_mode(self) -> HVACMode:
|
||||
def zone_control_hvac_action(self) -> HVACAction:
|
||||
"""Return hvac operation ie. heat, cool, dry, off mode."""
|
||||
|
||||
if (
|
||||
self.zone_control_device is not None
|
||||
and (
|
||||
state := self.zone_control_device.states[
|
||||
if self.zone_control_executor is not None and (
|
||||
(
|
||||
state := self.zone_control_executor.select_state(
|
||||
OverkizState.IO_PASS_APC_OPERATING_MODE
|
||||
]
|
||||
)
|
||||
)
|
||||
is not None
|
||||
and (value := state.value_as_str) is not None
|
||||
):
|
||||
return OVERKIZ_TO_HVAC_MODE[value]
|
||||
return HVACMode.OFF
|
||||
return OVERKIZ_TO_HVAC_ACTION[cast(str, state)]
|
||||
|
||||
return HVACAction.OFF
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return the current running hvac operation."""
|
||||
|
||||
# When ZoneControl action is heating/cooling but Zone is stopped, means the zone is idle.
|
||||
if (
|
||||
hvac_action := self.zone_control_hvac_action
|
||||
) in HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE and cast(
|
||||
str,
|
||||
self.executor.select_state(
|
||||
HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE[hvac_action]
|
||||
),
|
||||
) == OverkizCommandParam.STOP:
|
||||
return HVACAction.IDLE
|
||||
|
||||
return hvac_action
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
@@ -84,30 +203,32 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return super().hvac_mode
|
||||
|
||||
zone_control_hvac_mode = self.zone_control_hvac_mode
|
||||
if (device_hvac_mode := self.device_hvac_mode) is None:
|
||||
return HVACMode.OFF
|
||||
|
||||
# Should be same, because either thermostat or this integration change both.
|
||||
on_off_state = cast(
|
||||
cooling_is_off = cast(
|
||||
str,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_COOLING_ON_OFF
|
||||
if zone_control_hvac_mode == HVACMode.COOL
|
||||
else OverkizState.CORE_HEATING_ON_OFF
|
||||
),
|
||||
)
|
||||
self.executor.select_state(OverkizState.CORE_COOLING_ON_OFF),
|
||||
) in (OverkizCommandParam.OFF, None)
|
||||
|
||||
heating_is_off = cast(
|
||||
str,
|
||||
self.executor.select_state(OverkizState.CORE_HEATING_ON_OFF),
|
||||
) in (OverkizCommandParam.OFF, None)
|
||||
|
||||
# Device is Stopped, it means the air flux is flowing but its venting door is closed.
|
||||
if on_off_state == OverkizCommandParam.OFF:
|
||||
hvac_mode = HVACMode.OFF
|
||||
else:
|
||||
hvac_mode = zone_control_hvac_mode
|
||||
if (
|
||||
(device_hvac_mode == HVACMode.COOL and cooling_is_off)
|
||||
or (device_hvac_mode == HVACMode.HEAT and heating_is_off)
|
||||
or (
|
||||
device_hvac_mode == HVACMode.HEAT_COOL
|
||||
and cooling_is_off
|
||||
and heating_is_off
|
||||
)
|
||||
):
|
||||
return HVACMode.OFF
|
||||
|
||||
# It helps keep it consistent with the Zone Control, within the interface.
|
||||
if self._attr_hvac_modes != [zone_control_hvac_mode, HVACMode.OFF]:
|
||||
self._attr_hvac_modes = [zone_control_hvac_mode, HVACMode.OFF]
|
||||
self.async_write_ha_state()
|
||||
|
||||
return hvac_mode
|
||||
return device_hvac_mode
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
@@ -118,46 +239,49 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
# They are mainly managed by the Zone Control device
|
||||
# However, it make sense to map the OFF Mode to the Overkiz STOP Preset
|
||||
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_ON_OFF,
|
||||
OverkizCommandParam.OFF,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_ON_OFF,
|
||||
OverkizCommandParam.OFF,
|
||||
)
|
||||
else:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_ON_OFF,
|
||||
OverkizCommandParam.ON,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_ON_OFF,
|
||||
OverkizCommandParam.ON,
|
||||
)
|
||||
on_off_target_command_param = (
|
||||
OverkizCommandParam.OFF
|
||||
if hvac_mode == HVACMode.OFF
|
||||
else OverkizCommandParam.ON
|
||||
)
|
||||
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_ON_OFF,
|
||||
on_off_target_command_param,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_ON_OFF,
|
||||
on_off_target_command_param,
|
||||
)
|
||||
|
||||
await self.async_refresh_modes()
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode, e.g., schedule, manual."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return super().preset_mode
|
||||
|
||||
mode = OVERKIZ_MODE_TO_PRESET_MODES[
|
||||
cast(
|
||||
str,
|
||||
self.executor.select_state(
|
||||
OverkizState.IO_PASS_APC_COOLING_MODE
|
||||
if self.zone_control_hvac_mode == HVACMode.COOL
|
||||
else OverkizState.IO_PASS_APC_HEATING_MODE
|
||||
),
|
||||
if (
|
||||
self.zone_control_hvac_action in HVAC_ACTION_TO_OVERKIZ_MODE_STATE
|
||||
and (
|
||||
mode_state := HVAC_ACTION_TO_OVERKIZ_MODE_STATE[
|
||||
self.zone_control_hvac_action
|
||||
]
|
||||
)
|
||||
]
|
||||
and (
|
||||
(
|
||||
mode := OVERKIZ_MODE_TO_PRESET_MODES[
|
||||
cast(str, self.executor.select_state(mode_state))
|
||||
]
|
||||
)
|
||||
is not None
|
||||
)
|
||||
):
|
||||
return mode
|
||||
|
||||
return mode if mode is not None else PRESET_NONE
|
||||
return PRESET_NONE
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
@@ -178,13 +302,18 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
await self.async_refresh_modes()
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return hvac target temperature."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return super().target_temperature
|
||||
|
||||
if self.zone_control_hvac_mode == HVACMode.COOL:
|
||||
device_hvac_mode = self.device_hvac_mode
|
||||
|
||||
if device_hvac_mode == HVACMode.HEAT_COOL:
|
||||
return None
|
||||
|
||||
if device_hvac_mode == HVACMode.COOL:
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
@@ -192,7 +321,7 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
),
|
||||
)
|
||||
|
||||
if self.zone_control_hvac_mode == HVACMode.HEAT:
|
||||
if device_hvac_mode == HVACMode.HEAT:
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
@@ -204,32 +333,73 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE)
|
||||
)
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the highbound target temperature we try to reach (cooling)."""
|
||||
|
||||
if self.device_hvac_mode != HVACMode.HEAT_COOL:
|
||||
return None
|
||||
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(OverkizState.CORE_COOLING_TARGET_TEMPERATURE),
|
||||
)
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the lowbound target temperature we try to reach (heating)."""
|
||||
|
||||
if self.device_hvac_mode != HVACMode.HEAT_COOL:
|
||||
return None
|
||||
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(OverkizState.CORE_HEATING_TARGET_TEMPERATURE),
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new temperature."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return await super().async_set_temperature(**kwargs)
|
||||
|
||||
temperature = kwargs[ATTR_TEMPERATURE]
|
||||
target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
hvac_mode = self.hvac_mode
|
||||
|
||||
if hvac_mode == HVACMode.HEAT_COOL:
|
||||
if target_temp_low is not None:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_TARGET_TEMPERATURE,
|
||||
target_temp_low,
|
||||
)
|
||||
|
||||
if target_temp_high is not None:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_TARGET_TEMPERATURE,
|
||||
target_temp_high,
|
||||
)
|
||||
|
||||
elif target_temperature is not None:
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_TARGET_TEMPERATURE,
|
||||
target_temperature,
|
||||
)
|
||||
|
||||
elif hvac_mode == HVACMode.COOL:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_TARGET_TEMPERATURE,
|
||||
target_temperature,
|
||||
)
|
||||
|
||||
# Change both (heating/cooling) temperature is a good way to have consistency
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_TARGET_TEMPERATURE,
|
||||
temperature,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_COOLING_TARGET_TEMPERATURE,
|
||||
temperature,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DEROGATION_ON_OFF_STATE,
|
||||
OverkizCommandParam.OFF,
|
||||
OverkizCommandParam.ON,
|
||||
)
|
||||
|
||||
# Target temperature may take up to 1 minute to get refreshed.
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.REFRESH_TARGET_TEMPERATURE
|
||||
)
|
||||
await self.async_refresh_modes()
|
||||
|
||||
async def async_refresh_modes(self) -> None:
|
||||
"""Refresh the device modes to have new states."""
|
||||
@@ -256,3 +426,51 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.REFRESH_TARGET_TEMPERATURE
|
||||
)
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return Minimum Temperature for AC of this group."""
|
||||
|
||||
device_hvac_mode = self.device_hvac_mode
|
||||
|
||||
if device_hvac_mode in (HVACMode.HEAT, HVACMode.HEAT_COOL):
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MINIMUM_HEATING_TARGET_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
if device_hvac_mode == HVACMode.COOL:
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MINIMUM_COOLING_TARGET_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return Max Temperature for AC of this group."""
|
||||
|
||||
device_hvac_mode = self.device_hvac_mode
|
||||
|
||||
if device_hvac_mode == HVACMode.HEAT:
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MAXIMUM_HEATING_TARGET_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
if device_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL):
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MAXIMUM_COOLING_TARGET_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
return super().max_temp
|
||||
|
||||
@@ -298,6 +298,11 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
|
||||
OverkizState.OVP_FAN_SPEED,
|
||||
OverkizCommandParam.AUTO,
|
||||
)
|
||||
# Sanitize fan mode: Overkiz is sometimes providing a state that
|
||||
# cannot be used as a command. Convert it to HA space and back to Overkiz
|
||||
if fan_mode not in FAN_MODES_TO_OVERKIZ.values():
|
||||
fan_mode = FAN_MODES_TO_OVERKIZ[OVERKIZ_TO_FAN_MODES[fan_mode]]
|
||||
|
||||
hvac_mode = self._control_backfill(
|
||||
hvac_mode,
|
||||
OverkizState.OVP_MODE_CHANGE,
|
||||
@@ -357,5 +362,5 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity):
|
||||
]
|
||||
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.GLOBAL_CONTROL, command_data
|
||||
OverkizCommand.GLOBAL_CONTROL, *command_data
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.13.8"],
|
||||
"requirements": ["pyoverkiz==1.13.9"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
||||
@@ -97,6 +97,28 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [
|
||||
max_value_state_name=OverkizState.CORE_MAXIMAL_SHOWER_MANUAL_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
OverkizNumberDescription(
|
||||
key=OverkizState.CORE_TARGET_DWH_TEMPERATURE,
|
||||
name="Target temperature",
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
command=OverkizCommand.SET_TARGET_DHW_TEMPERATURE,
|
||||
native_min_value=50,
|
||||
native_max_value=65,
|
||||
min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE,
|
||||
max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
OverkizNumberDescription(
|
||||
key=OverkizState.CORE_WATER_TARGET_TEMPERATURE,
|
||||
name="Water target temperature",
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
command=OverkizCommand.SET_WATER_TARGET_TEMPERATURE,
|
||||
native_min_value=50,
|
||||
native_max_value=65,
|
||||
min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE,
|
||||
max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
# SomfyHeatingTemperatureInterface
|
||||
OverkizNumberDescription(
|
||||
key=OverkizState.CORE_ECO_ROOM_TEMPERATURE,
|
||||
|
||||
@@ -399,6 +399,20 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_BOTTOM_TANK_WATER_TEMPERATURE,
|
||||
name="Bottom tank water temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONTROL_WATER_TARGET_TEMPERATURE,
|
||||
name="Control water target temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
# Cover
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_TARGET_CLOSURE,
|
||||
|
||||
@@ -42,7 +42,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
INVALID_TOKEN_MESSAGE,
|
||||
PLATFORMS,
|
||||
PLATFORMS_COMPLETED,
|
||||
PLEX_SERVER_CONFIG,
|
||||
PLEX_UPDATE_LIBRARY_SIGNAL,
|
||||
PLEX_UPDATE_PLATFORMS_SIGNAL,
|
||||
@@ -94,18 +93,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
gdm.scan(scan_for_clients=True)
|
||||
|
||||
debouncer = Debouncer[None](
|
||||
hass,
|
||||
_LOGGER,
|
||||
cooldown=10,
|
||||
immediate=True,
|
||||
function=gdm_scan,
|
||||
hass, _LOGGER, cooldown=10, immediate=True, function=gdm_scan, background=True
|
||||
).async_call
|
||||
|
||||
hass_data = PlexData(
|
||||
servers={},
|
||||
dispatchers={},
|
||||
websockets={},
|
||||
platforms_completed={},
|
||||
gdm_scanner=gdm,
|
||||
gdm_debouncer=debouncer,
|
||||
)
|
||||
@@ -180,7 +174,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
server_id = plex_server.machine_identifier
|
||||
hass_data = get_plex_data(hass)
|
||||
hass_data[SERVERS][server_id] = plex_server
|
||||
hass_data[PLATFORMS_COMPLETED][server_id] = set()
|
||||
|
||||
entry.add_update_listener(async_options_updated)
|
||||
|
||||
@@ -233,11 +226,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
hass_data[WEBSOCKETS][server_id] = websocket
|
||||
|
||||
def start_websocket_session(platform):
|
||||
hass_data[PLATFORMS_COMPLETED][server_id].add(platform)
|
||||
if hass_data[PLATFORMS_COMPLETED][server_id] == PLATFORMS:
|
||||
hass.loop.create_task(websocket.listen())
|
||||
|
||||
def close_websocket_session(_):
|
||||
websocket.close()
|
||||
|
||||
@@ -248,8 +236,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
for platform in PLATFORMS:
|
||||
start_websocket_session(platform)
|
||||
entry.async_create_background_task(
|
||||
hass, websocket.listen(), f"plex websocket listener {entry.entry_id}"
|
||||
)
|
||||
|
||||
async_cleanup_plex_devices(hass, entry)
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ GDM_SCANNER: Final = "gdm_scanner"
|
||||
PLATFORMS = frozenset(
|
||||
[Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.UPDATE]
|
||||
)
|
||||
PLATFORMS_COMPLETED: Final = "platforms_completed"
|
||||
PLAYER_SOURCE = "player_source"
|
||||
SERVERS: Final = "servers"
|
||||
WEBSOCKETS: Final = "websockets"
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, TypedDict
|
||||
from plexapi.gdm import GDM
|
||||
from plexwebsocket import PlexWebsocket
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
|
||||
from .const import DOMAIN, SERVERS
|
||||
@@ -23,7 +22,6 @@ class PlexData(TypedDict):
|
||||
servers: dict[str, PlexServer]
|
||||
dispatchers: dict[str, list[CALLBACK_TYPE]]
|
||||
websockets: dict[str, PlexWebsocket]
|
||||
platforms_completed: dict[str, set[Platform]]
|
||||
gdm_scanner: GDM
|
||||
gdm_debouncer: Callable[[], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["plexapi", "plexwebsocket"],
|
||||
"requirements": [
|
||||
"PlexAPI==4.15.10",
|
||||
"PlexAPI==4.15.11",
|
||||
"plexauth==0.0.6",
|
||||
"plexwebsocket==0.0.14"
|
||||
],
|
||||
|
||||
@@ -97,6 +97,7 @@ class PlexServer:
|
||||
cooldown=DEBOUNCE_TIMEOUT,
|
||||
immediate=True,
|
||||
function=self._async_update_platforms,
|
||||
background=True,
|
||||
).async_call
|
||||
self.thumbnail_cache = {}
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ class PrometheusMetrics:
|
||||
self, entity_id: str, friendly_name: str | None = None
|
||||
) -> None:
|
||||
"""Remove labelsets matching the given entity id from all metrics."""
|
||||
for metric in self._metrics.values():
|
||||
for metric in list(self._metrics.values()):
|
||||
for sample in cast(list[prometheus_client.Metric], metric.collect())[
|
||||
0
|
||||
].samples:
|
||||
|
||||
@@ -715,6 +715,7 @@ class Statistics(Base, StatisticsBase):
|
||||
"start_ts",
|
||||
unique=True,
|
||||
),
|
||||
_DEFAULT_TABLE_ARGS,
|
||||
)
|
||||
__tablename__ = TABLE_STATISTICS
|
||||
|
||||
@@ -732,6 +733,7 @@ class StatisticsShortTerm(Base, StatisticsBase):
|
||||
"start_ts",
|
||||
unique=True,
|
||||
),
|
||||
_DEFAULT_TABLE_ARGS,
|
||||
)
|
||||
__tablename__ = TABLE_STATISTICS_SHORT_TERM
|
||||
|
||||
@@ -760,7 +762,10 @@ class StatisticsMeta(Base):
|
||||
class RecorderRuns(Base):
|
||||
"""Representation of recorder run."""
|
||||
|
||||
__table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),)
|
||||
__table_args__ = (
|
||||
Index("ix_recorder_runs_start_end", "start", "end"),
|
||||
_DEFAULT_TABLE_ARGS,
|
||||
)
|
||||
__tablename__ = TABLE_RECORDER_RUNS
|
||||
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||
@@ -789,6 +794,7 @@ class MigrationChanges(Base):
|
||||
"""Representation of migration changes."""
|
||||
|
||||
__tablename__ = TABLE_MIGRATION_CHANGES
|
||||
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||
|
||||
migration_id: Mapped[str] = mapped_column(String(255), primary_key=True)
|
||||
version: Mapped[int] = mapped_column(SmallInteger)
|
||||
@@ -798,6 +804,8 @@ class SchemaChanges(Base):
|
||||
"""Representation of schema version changes."""
|
||||
|
||||
__tablename__ = TABLE_SCHEMA_CHANGES
|
||||
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||
|
||||
change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||
schema_version: Mapped[int | None] = mapped_column(Integer)
|
||||
changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow)
|
||||
@@ -816,6 +824,8 @@ class StatisticsRuns(Base):
|
||||
"""Representation of statistics run."""
|
||||
|
||||
__tablename__ = TABLE_STATISTICS_RUNS
|
||||
__table_args__ = (_DEFAULT_TABLE_ARGS,)
|
||||
|
||||
run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True)
|
||||
start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True)
|
||||
|
||||
|
||||
@@ -125,16 +125,16 @@ class RenaultVehicleProxy:
|
||||
coordinator = self.coordinators[key]
|
||||
if coordinator.not_supported:
|
||||
# Remove endpoint as it is not supported for this vehicle.
|
||||
LOGGER.info(
|
||||
"Ignoring endpoint %s as it is not supported for this vehicle: %s",
|
||||
LOGGER.warning(
|
||||
"Ignoring endpoint %s as it is not supported: %s",
|
||||
coordinator.name,
|
||||
coordinator.last_exception,
|
||||
)
|
||||
del self.coordinators[key]
|
||||
elif coordinator.access_denied:
|
||||
# Remove endpoint as it is denied for this vehicle.
|
||||
LOGGER.info(
|
||||
"Ignoring endpoint %s as it is denied for this vehicle: %s",
|
||||
LOGGER.warning(
|
||||
"Ignoring endpoint %s as it is denied: %s",
|
||||
coordinator.name,
|
||||
coordinator.last_exception,
|
||||
)
|
||||
|
||||
@@ -46,7 +46,6 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
"""Initialize ReolinkVODMediaSource."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
self.data: dict[str, ReolinkData] = hass.data[DOMAIN]
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
@@ -57,7 +56,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
_, config_entry_id, channel_str, stream_res, filename = identifier
|
||||
channel = int(channel_str)
|
||||
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
vod_type = VodRequestType.RTMP
|
||||
if host.api.is_nvr:
|
||||
@@ -130,7 +130,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
continue
|
||||
channels: list[str] = []
|
||||
host = self.data[config_entry.entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry.entry_id].host
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_reg, config_entry.entry_id
|
||||
)
|
||||
@@ -187,7 +188,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
self, config_entry_id: str, channel: int
|
||||
) -> BrowseMediaSource:
|
||||
"""Allow the user to select the high or low playback resolution, (low loads faster)."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
main_enc = await host.api.get_encoding(channel, "main")
|
||||
if main_enc == "h265":
|
||||
@@ -236,7 +238,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
self, config_entry_id: str, channel: int, stream: str
|
||||
) -> BrowseMediaSource:
|
||||
"""Return all days on which recordings are available for a reolink camera."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
# We want today of the camera, not necessarily today of the server
|
||||
now = host.api.time() or await host.api.async_get_time()
|
||||
@@ -288,7 +291,8 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
day: int,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return all recording files on a specific day of a Reolink camera."""
|
||||
host = self.data[config_entry_id].host
|
||||
data: dict[str, ReolinkData] = self.hass.data[DOMAIN]
|
||||
host = data[config_entry_id].host
|
||||
|
||||
start = dt.datetime(year, month, day, hour=0, minute=0, second=0)
|
||||
end = dt.datetime(year, month, day, hour=23, minute=59, second=59)
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ring",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ring_doorbell"],
|
||||
"requirements": ["ring-doorbell[listen]==0.8.7"]
|
||||
"requirements": ["ring-doorbell[listen]==0.8.9"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/romy",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["romy==0.0.7"],
|
||||
"requirements": ["romy==0.0.10"],
|
||||
"zeroconf": ["_aicu-http._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/roomba",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["paho_mqtt", "roombapy"],
|
||||
"requirements": ["roombapy==1.6.13"],
|
||||
"requirements": ["roombapy==1.8.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_amzn-alexa._tcp.local.",
|
||||
|
||||
@@ -54,7 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=""): cv.string,
|
||||
vol.Optional(CONF_NAME, default="Rova"): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=["bio"]): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||
cv.ensure_list, [vol.In(["bio", "paper", "plastic", "residual"])]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==2.6.0",
|
||||
"wakeonlan==2.1.0",
|
||||
"async-upnp-client==0.38.2"
|
||||
"async-upnp-client==0.38.3"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -234,3 +234,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = (
|
||||
)
|
||||
|
||||
CONF_GEN = "gen"
|
||||
|
||||
SHELLY_PLUS_RGBW_CHANNELS = 4
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.light import (
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_RGBW_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
LightEntityFeature,
|
||||
@@ -34,12 +35,14 @@ from .const import (
|
||||
RGBW_MODELS,
|
||||
RPC_MIN_TRANSITION_TIME_SEC,
|
||||
SHBLB_1_RGB_EFFECTS,
|
||||
SHELLY_PLUS_RGBW_CHANNELS,
|
||||
STANDARD_RGB_EFFECTS,
|
||||
)
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data
|
||||
from .entity import ShellyBlockEntity, ShellyRpcEntity
|
||||
from .utils import (
|
||||
async_remove_shelly_entity,
|
||||
async_remove_shelly_rpc_entities,
|
||||
brightness_to_percentage,
|
||||
get_device_entry_gen,
|
||||
get_rpc_key_ids,
|
||||
@@ -118,14 +121,28 @@ def async_setup_rpc_entry(
|
||||
return
|
||||
|
||||
if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"):
|
||||
# Light mode remove RGB & RGBW entities, add light entities
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, ["rgb:0", "rgbw:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids)
|
||||
return
|
||||
|
||||
light_keys = [f"light:{i}" for i in range(SHELLY_PLUS_RGBW_CHANNELS)]
|
||||
|
||||
if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"):
|
||||
# RGB mode remove light & RGBW entities, add RGB entity
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgbw:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids)
|
||||
return
|
||||
|
||||
if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"):
|
||||
# RGBW mode remove light & RGB entities, add RGBW entity
|
||||
async_remove_shelly_rpc_entities(
|
||||
hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgb:0"]
|
||||
)
|
||||
async_add_entities(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids)
|
||||
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
) -> None:
|
||||
"""Initialize update entity."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
self._ota_in_progress: bool = False
|
||||
self._ota_in_progress: bool | int = False
|
||||
self._attr_release_url = get_release_url(
|
||||
coordinator.device.gen, coordinator.model, description.beta
|
||||
)
|
||||
@@ -237,14 +237,13 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
@callback
|
||||
def _ota_progress_callback(self, event: dict[str, Any]) -> None:
|
||||
"""Handle device OTA progress."""
|
||||
if self._ota_in_progress:
|
||||
if self.in_progress is not False:
|
||||
event_type = event["event"]
|
||||
if event_type == OTA_BEGIN:
|
||||
self._attr_in_progress = 0
|
||||
self._ota_in_progress = 0
|
||||
elif event_type == OTA_PROGRESS:
|
||||
self._attr_in_progress = event["progress_percent"]
|
||||
self._ota_in_progress = event["progress_percent"]
|
||||
elif event_type in (OTA_ERROR, OTA_SUCCESS):
|
||||
self._attr_in_progress = False
|
||||
self._ota_in_progress = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -262,6 +261,11 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
|
||||
return self.installed_version
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool | int:
|
||||
"""Update installation in progress."""
|
||||
return self._ota_in_progress
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
@@ -292,7 +296,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
await self.coordinator.async_shutdown_device_and_start_reauth()
|
||||
else:
|
||||
self._ota_in_progress = True
|
||||
LOGGER.debug("OTA update call successful")
|
||||
LOGGER.info("OTA update call for %s successful", self.coordinator.name)
|
||||
|
||||
|
||||
class RpcSleepingUpdateEntity(
|
||||
|
||||
@@ -488,3 +488,15 @@ async def async_shutdown_device(device: BlockDevice | RpcDevice) -> None:
|
||||
await device.shutdown()
|
||||
if isinstance(device, BlockDevice):
|
||||
device.shutdown()
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove_shelly_rpc_entities(
|
||||
hass: HomeAssistant, domain: str, mac: str, keys: list[str]
|
||||
) -> None:
|
||||
"""Remove RPC based Shelly entity."""
|
||||
entity_reg = er_async_get(hass)
|
||||
for key in keys:
|
||||
if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"):
|
||||
LOGGER.debug("Removing entity: %s", entity_id)
|
||||
entity_reg.async_remove(entity_id)
|
||||
|
||||
@@ -45,7 +45,7 @@ class SnapcastConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
except OSError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await client.stop()
|
||||
client.stop()
|
||||
return self.async_create_entry(title=DEFAULT_TITLE, data=user_input)
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=SNAPCAST_SCHEMA, errors=errors
|
||||
|
||||
@@ -5,8 +5,19 @@ from __future__ import annotations
|
||||
import binascii
|
||||
import logging
|
||||
|
||||
from pysnmp.entity import config as cfg
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
from pysnmp.error import PySnmpError
|
||||
from pysnmp.hlapi.asyncio import (
|
||||
CommunityData,
|
||||
ContextData,
|
||||
ObjectIdentity,
|
||||
ObjectType,
|
||||
SnmpEngine,
|
||||
Udp6TransportTarget,
|
||||
UdpTransportTarget,
|
||||
UsmUserData,
|
||||
bulkWalkCmd,
|
||||
isEndOfMib,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
@@ -24,7 +35,13 @@ from .const import (
|
||||
CONF_BASEOID,
|
||||
CONF_COMMUNITY,
|
||||
CONF_PRIV_KEY,
|
||||
DEFAULT_AUTH_PROTOCOL,
|
||||
DEFAULT_COMMUNITY,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_PRIV_PROTOCOL,
|
||||
DEFAULT_TIMEOUT,
|
||||
DEFAULT_VERSION,
|
||||
SNMP_VERSIONS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -40,9 +57,12 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def get_scanner(hass: HomeAssistant, config: ConfigType) -> SnmpScanner | None:
|
||||
async def async_get_scanner(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> SnmpScanner | None:
|
||||
"""Validate the configuration and return an SNMP scanner."""
|
||||
scanner = SnmpScanner(config[DOMAIN])
|
||||
await scanner.async_init()
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
@@ -51,39 +71,75 @@ class SnmpScanner(DeviceScanner):
|
||||
"""Queries any SNMP capable Access Point for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
"""Initialize the scanner and test the target device."""
|
||||
host = config[CONF_HOST]
|
||||
community = config[CONF_COMMUNITY]
|
||||
baseoid = config[CONF_BASEOID]
|
||||
authkey = config.get(CONF_AUTH_KEY)
|
||||
authproto = DEFAULT_AUTH_PROTOCOL
|
||||
privkey = config.get(CONF_PRIV_KEY)
|
||||
privproto = DEFAULT_PRIV_PROTOCOL
|
||||
|
||||
self.snmp = cmdgen.CommandGenerator()
|
||||
try:
|
||||
# Try IPv4 first.
|
||||
target = UdpTransportTarget((host, DEFAULT_PORT), timeout=DEFAULT_TIMEOUT)
|
||||
except PySnmpError:
|
||||
# Then try IPv6.
|
||||
try:
|
||||
target = Udp6TransportTarget(
|
||||
(host, DEFAULT_PORT), timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
except PySnmpError as err:
|
||||
_LOGGER.error("Invalid SNMP host: %s", err)
|
||||
return
|
||||
|
||||
self.host = cmdgen.UdpTransportTarget((config[CONF_HOST], 161))
|
||||
if CONF_AUTH_KEY not in config or CONF_PRIV_KEY not in config:
|
||||
self.auth = cmdgen.CommunityData(config[CONF_COMMUNITY])
|
||||
if authkey is not None or privkey is not None:
|
||||
if not authkey:
|
||||
authproto = "none"
|
||||
if not privkey:
|
||||
privproto = "none"
|
||||
|
||||
request_args = [
|
||||
SnmpEngine(),
|
||||
UsmUserData(
|
||||
community,
|
||||
authKey=authkey or None,
|
||||
privKey=privkey or None,
|
||||
authProtocol=authproto,
|
||||
privProtocol=privproto,
|
||||
),
|
||||
target,
|
||||
ContextData(),
|
||||
]
|
||||
else:
|
||||
self.auth = cmdgen.UsmUserData(
|
||||
config[CONF_COMMUNITY],
|
||||
config[CONF_AUTH_KEY],
|
||||
config[CONF_PRIV_KEY],
|
||||
authProtocol=cfg.usmHMACSHAAuthProtocol,
|
||||
privProtocol=cfg.usmAesCfb128Protocol,
|
||||
)
|
||||
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
|
||||
self.last_results = []
|
||||
request_args = [
|
||||
SnmpEngine(),
|
||||
CommunityData(community, mpModel=SNMP_VERSIONS[DEFAULT_VERSION]),
|
||||
target,
|
||||
ContextData(),
|
||||
]
|
||||
|
||||
# Test the router is accessible
|
||||
data = self.get_snmp_data()
|
||||
self.request_args = request_args
|
||||
self.baseoid = baseoid
|
||||
self.last_results = []
|
||||
self.success_init = False
|
||||
|
||||
async def async_init(self):
|
||||
"""Make a one-off read to check if the target device is reachable and readable."""
|
||||
data = await self.async_get_snmp_data()
|
||||
self.success_init = data is not None
|
||||
|
||||
def scan_devices(self):
|
||||
async def async_scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
await self._async_update_info()
|
||||
return [client["mac"] for client in self.last_results if client.get("mac")]
|
||||
|
||||
def get_device_name(self, device):
|
||||
async def async_get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
# We have no names
|
||||
return None
|
||||
|
||||
def _update_info(self):
|
||||
async def _async_update_info(self):
|
||||
"""Ensure the information from the device is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
@@ -91,38 +147,42 @@ class SnmpScanner(DeviceScanner):
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
if not (data := self.get_snmp_data()):
|
||||
if not (data := await self.async_get_snmp_data()):
|
||||
return False
|
||||
|
||||
self.last_results = data
|
||||
return True
|
||||
|
||||
def get_snmp_data(self):
|
||||
async def async_get_snmp_data(self):
|
||||
"""Fetch MAC addresses from access point via SNMP."""
|
||||
devices = []
|
||||
|
||||
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
|
||||
self.auth, self.host, self.baseoid
|
||||
walker = bulkWalkCmd(
|
||||
*self.request_args,
|
||||
0,
|
||||
50,
|
||||
ObjectType(ObjectIdentity(self.baseoid)),
|
||||
lexicographicMode=False,
|
||||
)
|
||||
async for errindication, errstatus, errindex, res in walker:
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
return
|
||||
if errstatus:
|
||||
_LOGGER.error(
|
||||
"SNMP error: %s at %s",
|
||||
errstatus.prettyPrint(),
|
||||
errindex and res[int(errindex) - 1][0] or "?",
|
||||
)
|
||||
return
|
||||
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
return
|
||||
if errstatus:
|
||||
_LOGGER.error(
|
||||
"SNMP error: %s at %s",
|
||||
errstatus.prettyPrint(),
|
||||
errindex and restable[int(errindex) - 1][0] or "?",
|
||||
)
|
||||
return
|
||||
|
||||
for resrow in restable:
|
||||
for _, val in resrow:
|
||||
try:
|
||||
mac = binascii.hexlify(val.asOctets()).decode("utf-8")
|
||||
except AttributeError:
|
||||
continue
|
||||
_LOGGER.debug("Found MAC address: %s", mac)
|
||||
mac = ":".join([mac[i : i + 2] for i in range(0, len(mac), 2)])
|
||||
devices.append({"mac": mac})
|
||||
for _oid, value in res:
|
||||
if not isEndOfMib(res):
|
||||
try:
|
||||
mac = binascii.hexlify(value.asOctets()).decode("utf-8")
|
||||
except AttributeError:
|
||||
continue
|
||||
_LOGGER.debug("Found MAC address: %s", mac)
|
||||
mac = ":".join([mac[i : i + 2] for i in range(0, len(mac), 2)])
|
||||
devices.append({"mac": mac})
|
||||
return devices
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "snmp",
|
||||
"name": "SNMP",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@nmaggioni"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/snmp",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyasn1", "pysmi", "pysnmp"],
|
||||
|
||||
@@ -270,7 +270,7 @@ class SnmpData:
|
||||
"SNMP OID %s received type=%s and data %s",
|
||||
self._baseoid,
|
||||
type(value),
|
||||
bytes(value),
|
||||
value,
|
||||
)
|
||||
if isinstance(value, NoSuchObject):
|
||||
_LOGGER.error(
|
||||
|
||||
@@ -7,6 +7,7 @@ from contextlib import suppress
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import cast
|
||||
import urllib.parse
|
||||
|
||||
from soco.data_structures import DidlObject
|
||||
from soco.ms_data_structures import MusicServiceItem
|
||||
@@ -60,12 +61,14 @@ def get_thumbnail_url_full(
|
||||
media_content_id,
|
||||
media_content_type,
|
||||
)
|
||||
return getattr(item, "album_art_uri", None)
|
||||
return urllib.parse.unquote(getattr(item, "album_art_uri", ""))
|
||||
|
||||
return get_browse_image_url(
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
media_image_id,
|
||||
return urllib.parse.unquote(
|
||||
get_browse_image_url(
|
||||
media_content_type,
|
||||
media_content_id,
|
||||
media_image_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -166,6 +169,7 @@ def build_item_response(
|
||||
payload["idstring"] = "A:ALBUMARTIST/" + "/".join(
|
||||
payload["idstring"].split("/")[2:]
|
||||
)
|
||||
payload["idstring"] = urllib.parse.unquote(payload["idstring"])
|
||||
|
||||
try:
|
||||
search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]]
|
||||
@@ -201,7 +205,7 @@ def build_item_response(
|
||||
|
||||
if not title:
|
||||
try:
|
||||
title = payload["idstring"].split("/")[1]
|
||||
title = urllib.parse.unquote(payload["idstring"].split("/")[1])
|
||||
except IndexError:
|
||||
title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
|
||||
|
||||
@@ -493,10 +497,24 @@ def get_media(
|
||||
"""Fetch media/album."""
|
||||
search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type)
|
||||
|
||||
if search_type == "playlists":
|
||||
# Format is S:TITLE or S:ITEM_ID
|
||||
splits = item_id.split(":")
|
||||
title = splits[1] if len(splits) > 1 else None
|
||||
playlist = next(
|
||||
(
|
||||
p
|
||||
for p in media_library.get_playlists()
|
||||
if (item_id == p.item_id or title == p.title)
|
||||
),
|
||||
None,
|
||||
)
|
||||
return playlist
|
||||
|
||||
if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM:
|
||||
item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:])
|
||||
|
||||
search_term = item_id.split("/")[-1]
|
||||
search_term = urllib.parse.unquote(item_id.split("/")[-1])
|
||||
matches = media_library.get_music_library_information(
|
||||
search_type, search_term=search_term, full_album_art_uri=True
|
||||
)
|
||||
|
||||
@@ -626,13 +626,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
soco.play_uri(media_id, force_radio=is_radio)
|
||||
elif media_type == MediaType.PLAYLIST:
|
||||
if media_id.startswith("S:"):
|
||||
item = media_browser.get_media(self.media.library, media_id, media_type)
|
||||
soco.play_uri(item.get_uri())
|
||||
return
|
||||
try:
|
||||
playlist = media_browser.get_media(
|
||||
self.media.library, media_id, media_type
|
||||
)
|
||||
else:
|
||||
playlists = soco.get_sonos_playlists(complete_result=True)
|
||||
playlist = next(p for p in playlists if p.title == media_id)
|
||||
except StopIteration:
|
||||
playlist = next((p for p in playlists if p.title == media_id), None)
|
||||
if not playlist:
|
||||
_LOGGER.error('Could not find a Sonos playlist named "%s"', media_id)
|
||||
else:
|
||||
soco.clear_queue()
|
||||
|
||||
@@ -25,10 +25,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
partial(speedtest.Speedtest, secure=True)
|
||||
)
|
||||
coordinator = SpeedTestDataCoordinator(hass, config_entry, api)
|
||||
await hass.async_add_executor_job(coordinator.update_servers)
|
||||
except speedtest.SpeedtestException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
hass.data[DOMAIN] = coordinator
|
||||
|
||||
async def _async_finish_startup(hass: HomeAssistant) -> None:
|
||||
"""Run this only when HA has finished its startup."""
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@@ -36,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
# Don't start a speedtest during startup
|
||||
async_at_started(hass, _async_finish_startup)
|
||||
|
||||
hass.data[DOMAIN] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["async-upnp-client==0.38.2"]
|
||||
"requirements": ["async-upnp-client==0.38.3"]
|
||||
}
|
||||
|
||||
@@ -58,14 +58,14 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
|
||||
async def _async_update_data(self) -> StarlinkData:
|
||||
async with asyncio.timeout(4):
|
||||
try:
|
||||
status, location, sleep = await asyncio.gather(
|
||||
self.hass.async_add_executor_job(status_data, self.channel_context),
|
||||
self.hass.async_add_executor_job(
|
||||
location_data, self.channel_context
|
||||
),
|
||||
self.hass.async_add_executor_job(
|
||||
get_sleep_config, self.channel_context
|
||||
),
|
||||
status = await self.hass.async_add_executor_job(
|
||||
status_data, self.channel_context
|
||||
)
|
||||
location = await self.hass.async_add_executor_job(
|
||||
location_data, self.channel_context
|
||||
)
|
||||
sleep = await self.hass.async_add_executor_job(
|
||||
get_sleep_config, self.channel_context
|
||||
)
|
||||
return StarlinkData(location, sleep, *status)
|
||||
except GrpcError as exc:
|
||||
|
||||
@@ -10,6 +10,7 @@ import math
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -62,14 +63,22 @@ class StarlinkTimeEntity(StarlinkEntity, TimeEntity):
|
||||
def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time:
|
||||
hour = math.floor(utc_minutes / 60)
|
||||
minute = utc_minutes % 60
|
||||
utc = datetime.now(UTC).replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
try:
|
||||
utc = datetime.now(UTC).replace(
|
||||
hour=hour, minute=minute, second=0, microsecond=0
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HomeAssistantError from exc
|
||||
return utc.astimezone(timezone).time()
|
||||
|
||||
|
||||
def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int:
|
||||
zoned_time = datetime.now(timezone).replace(
|
||||
hour=t.hour, minute=t.minute, second=0, microsecond=0
|
||||
)
|
||||
try:
|
||||
zoned_time = datetime.now(timezone).replace(
|
||||
hour=t.hour, minute=t.minute, second=0, microsecond=0
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HomeAssistantError from exc
|
||||
utc_time = zoned_time.astimezone(UTC).time()
|
||||
return (utc_time.hour * 60) + utc_time.minute
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if (
|
||||
SynoSurveillanceStation.INFO_API_KEY in available_apis
|
||||
and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis
|
||||
and api.surveillance_station is not None
|
||||
):
|
||||
coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api)
|
||||
await coordinator_switches.async_config_entry_first_refresh()
|
||||
|
||||
@@ -116,7 +116,7 @@ class SynoDSMSecurityBinarySensor(SynoDSMBinarySensor):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return bool(self._api.security)
|
||||
return bool(self._api.security) and super().available
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
|
||||
@@ -108,7 +108,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the availability of the camera."""
|
||||
return self.camera_data.is_enabled and self.coordinator.last_update_success
|
||||
return self.camera_data.is_enabled and super().available
|
||||
|
||||
@property
|
||||
def is_recording(self) -> bool:
|
||||
|
||||
@@ -286,18 +286,7 @@ class SynoApi:
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update function for updating API information."""
|
||||
try:
|
||||
await self._update()
|
||||
except SYNOLOGY_CONNECTION_EXCEPTIONS as err:
|
||||
LOGGER.debug(
|
||||
"Connection error during update of '%s' with exception: %s",
|
||||
self._entry.unique_id,
|
||||
err,
|
||||
)
|
||||
LOGGER.warning(
|
||||
"Connection error during update, fallback by reloading the entry"
|
||||
)
|
||||
await self._hass.config_entries.async_reload(self._entry.entry_id)
|
||||
await self._update()
|
||||
|
||||
async def _update(self) -> None:
|
||||
"""Update function for updating API information."""
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reboot": "mdi:reboot",
|
||||
"reboot": "mdi:restart",
|
||||
"shutdown": "mdi:power"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ class SynoDSMUtilSensor(SynoDSMSensor):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return bool(self._api.utilisation)
|
||||
return bool(self._api.utilisation) and super().available
|
||||
|
||||
|
||||
class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor):
|
||||
|
||||
@@ -98,7 +98,7 @@ class SynoDSMSurveillanceHomeModeToggle(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return bool(self._api.surveillance_station)
|
||||
return bool(self._api.surveillance_station) and super().available
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
|
||||
@@ -59,7 +59,7 @@ class SynoDSMUpdateEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return bool(self._api.upgrade)
|
||||
return bool(self._api.upgrade) and super().available
|
||||
|
||||
@property
|
||||
def installed_version(self) -> str | None:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user