forked from home-assistant/core
Compare commits
113 Commits
2022.6.0b2
...
2022.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d31d94532 | ||
|
|
4678466560 | ||
|
|
a886c6110d | ||
|
|
33f282af46 | ||
|
|
2f3232f087 | ||
|
|
54ff6ddd41 | ||
|
|
eef79e2912 | ||
|
|
93aad108a7 | ||
|
|
792ebbb600 | ||
|
|
c47774e273 | ||
|
|
22bdeab1e7 | ||
|
|
ca05cde6ba | ||
|
|
1e59ce19f5 | ||
|
|
7bdada7898 | ||
|
|
06a2fe94d3 | ||
|
|
854b0dbb2d | ||
|
|
bd8424d184 | ||
|
|
b50e3d5ce7 | ||
|
|
39c6a57c35 | ||
|
|
c0482bdbfd | ||
|
|
d9a41d10ff | ||
|
|
b401f16583 | ||
|
|
373634cc50 | ||
|
|
10fb3035d6 | ||
|
|
f3136c811c | ||
|
|
085eee88c9 | ||
|
|
6a3b74adf6 | ||
|
|
5c512ad5cb | ||
|
|
65cb82765b | ||
|
|
9f8fe7fca6 | ||
|
|
73536c07d7 | ||
|
|
e0ca5bafda | ||
|
|
ddc8c0a3b7 | ||
|
|
ff687a8248 | ||
|
|
0d9330c39e | ||
|
|
69e8f5bb98 | ||
|
|
6a8a97b57c | ||
|
|
f5e0363117 | ||
|
|
f1bcfedf84 | ||
|
|
0e985284c9 | ||
|
|
12e6f143a4 | ||
|
|
2b77db2597 | ||
|
|
a4297c0411 | ||
|
|
54b94c4826 | ||
|
|
b28b204b86 | ||
|
|
8558ea2f9a | ||
|
|
01b3da1554 | ||
|
|
880590da64 | ||
|
|
b74bd1aa0a | ||
|
|
caa79d8462 | ||
|
|
9295cc4df9 | ||
|
|
24e148ab8e | ||
|
|
3d75b0776f | ||
|
|
39da7a93ec | ||
|
|
bf47d86d30 | ||
|
|
2f3359f376 | ||
|
|
1139136365 | ||
|
|
9e723f9b6d | ||
|
|
9bd2e3ad7c | ||
|
|
384cb44d15 | ||
|
|
1274448de1 | ||
|
|
354149e43c | ||
|
|
17a3c62821 | ||
|
|
668f56f103 | ||
|
|
0db9863746 | ||
|
|
e60dc1b503 | ||
|
|
8606447848 | ||
|
|
de0c672cc2 | ||
|
|
c3acdcb2c8 | ||
|
|
9effb78a7f | ||
|
|
647df29a00 | ||
|
|
a54a5b2d20 | ||
|
|
f4d280b59d | ||
|
|
d268c828ee | ||
|
|
82ed6869d0 | ||
|
|
6b3a284135 | ||
|
|
ca8c750a5a | ||
|
|
7c2f73ddba | ||
|
|
1b2cb4eab7 | ||
|
|
4bf5132a06 | ||
|
|
6e06b6c9ed | ||
|
|
103f324c52 | ||
|
|
48d36e49f0 | ||
|
|
a4e2d31a19 | ||
|
|
15bdfb2a45 | ||
|
|
b842c76fbd | ||
|
|
a98528c93f | ||
|
|
a202ffe4c1 | ||
|
|
77e4c86c07 | ||
|
|
72a79736a6 | ||
|
|
2809592e71 | ||
|
|
da7446bf52 | ||
|
|
2942986a7b | ||
|
|
67ef3229fd | ||
|
|
952433d16e | ||
|
|
6f01c13845 | ||
|
|
f8b7527bf0 | ||
|
|
f039aac31c | ||
|
|
c62692dff1 | ||
|
|
4b524c0776 | ||
|
|
f41b2fa2cf | ||
|
|
ce4825c9e2 | ||
|
|
6bf6a0f7bc | ||
|
|
f33517ef2c | ||
|
|
da62e2cc23 | ||
|
|
b360f0280b | ||
|
|
50eaf2f475 | ||
|
|
bd222a1fe0 | ||
|
|
3a06b5f320 | ||
|
|
c45dc49270 | ||
|
|
301f7647d1 | ||
|
|
79340f85d2 | ||
|
|
afcc8679dd |
@@ -491,7 +491,6 @@ omit =
|
||||
homeassistant/components/homematic/*
|
||||
homeassistant/components/home_plus_control/api.py
|
||||
homeassistant/components/home_plus_control/switch.py
|
||||
homeassistant/components/homewizard/diagnostics.py
|
||||
homeassistant/components/homeworks/*
|
||||
homeassistant/components/honeywell/__init__.py
|
||||
homeassistant/components/honeywell/climate.py
|
||||
@@ -966,6 +965,7 @@ omit =
|
||||
homeassistant/components/rainmachine/model.py
|
||||
homeassistant/components/rainmachine/sensor.py
|
||||
homeassistant/components/rainmachine/switch.py
|
||||
homeassistant/components/rainmachine/util.py
|
||||
homeassistant/components/raspyrfm/*
|
||||
homeassistant/components/recollect_waste/__init__.py
|
||||
homeassistant/components/recollect_waste/sensor.py
|
||||
|
||||
@@ -26,7 +26,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up BAF fan auto comfort."""
|
||||
data: BAFData = hass.data[DOMAIN][entry.entry_id]
|
||||
if data.device.has_fan:
|
||||
if data.device.has_fan and data.device.has_auto_comfort:
|
||||
async_add_entities(
|
||||
[BAFAutoComfort(data.device, f"{data.device.name} Auto Comfort")]
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Big Ass Fans",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/baf",
|
||||
"requirements": ["aiobafi6==0.3.0"],
|
||||
"requirements": ["aiobafi6==0.5.0"],
|
||||
"codeowners": ["@bdraco", "@jfroy"],
|
||||
"iot_class": "local_push",
|
||||
"zeroconf": [
|
||||
|
||||
@@ -36,27 +36,7 @@ class BAFNumberDescription(NumberEntityDescription, BAFNumberDescriptionMixin):
|
||||
"""Class describing BAF sensor entities."""
|
||||
|
||||
|
||||
FAN_NUMBER_DESCRIPTIONS = (
|
||||
BAFNumberDescription(
|
||||
key="return_to_auto_timeout",
|
||||
name="Return to Auto Timeout",
|
||||
min_value=ONE_MIN_SECS,
|
||||
max_value=HALF_DAY_SECS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
unit_of_measurement=TIME_SECONDS,
|
||||
value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout),
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
BAFNumberDescription(
|
||||
key="motion_sense_timeout",
|
||||
name="Motion Sense Timeout",
|
||||
min_value=ONE_MIN_SECS,
|
||||
max_value=ONE_DAY_SECS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
unit_of_measurement=TIME_SECONDS,
|
||||
value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout),
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
AUTO_COMFORT_NUMBER_DESCRIPTIONS = (
|
||||
BAFNumberDescription(
|
||||
key="comfort_min_speed",
|
||||
name="Auto Comfort Minimum Speed",
|
||||
@@ -86,6 +66,29 @@ FAN_NUMBER_DESCRIPTIONS = (
|
||||
),
|
||||
)
|
||||
|
||||
FAN_NUMBER_DESCRIPTIONS = (
|
||||
BAFNumberDescription(
|
||||
key="return_to_auto_timeout",
|
||||
name="Return to Auto Timeout",
|
||||
min_value=ONE_MIN_SECS,
|
||||
max_value=HALF_DAY_SECS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
unit_of_measurement=TIME_SECONDS,
|
||||
value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout),
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
BAFNumberDescription(
|
||||
key="motion_sense_timeout",
|
||||
name="Motion Sense Timeout",
|
||||
min_value=ONE_MIN_SECS,
|
||||
max_value=ONE_DAY_SECS,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
unit_of_measurement=TIME_SECONDS,
|
||||
value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout),
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
)
|
||||
|
||||
LIGHT_NUMBER_DESCRIPTIONS = (
|
||||
BAFNumberDescription(
|
||||
key="light_return_to_auto_timeout",
|
||||
@@ -125,6 +128,8 @@ async def async_setup_entry(
|
||||
descriptions.extend(FAN_NUMBER_DESCRIPTIONS)
|
||||
if device.has_light:
|
||||
descriptions.extend(LIGHT_NUMBER_DESCRIPTIONS)
|
||||
if device.has_auto_comfort:
|
||||
descriptions.extend(AUTO_COMFORT_NUMBER_DESCRIPTIONS)
|
||||
async_add_entities(BAFNumber(device, description) for description in descriptions)
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class BAFSensorDescription(
|
||||
"""Class describing BAF sensor entities."""
|
||||
|
||||
|
||||
BASE_SENSORS = (
|
||||
AUTO_COMFORT_SENSORS = (
|
||||
BAFSensorDescription(
|
||||
key="temperature",
|
||||
name="Temperature",
|
||||
@@ -103,10 +103,12 @@ async def async_setup_entry(
|
||||
"""Set up BAF fan sensors."""
|
||||
data: BAFData = hass.data[DOMAIN][entry.entry_id]
|
||||
device = data.device
|
||||
sensors_descriptions = list(BASE_SENSORS)
|
||||
sensors_descriptions: list[BAFSensorDescription] = []
|
||||
for description in DEFINED_ONLY_SENSORS:
|
||||
if getattr(device, description.key):
|
||||
sensors_descriptions.append(description)
|
||||
if device.has_auto_comfort:
|
||||
sensors_descriptions.extend(AUTO_COMFORT_SENSORS)
|
||||
if device.has_fan:
|
||||
sensors_descriptions.extend(FAN_SENSORS)
|
||||
async_add_entities(
|
||||
|
||||
@@ -48,13 +48,16 @@ BASE_SWITCHES = [
|
||||
),
|
||||
]
|
||||
|
||||
FAN_SWITCHES = [
|
||||
AUTO_COMFORT_SWITCHES = [
|
||||
BAFSwitchDescription(
|
||||
key="comfort_heat_assist_enable",
|
||||
name="Auto Comfort Heat Assist",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda device: cast(Optional[bool], device.comfort_heat_assist_enable),
|
||||
),
|
||||
]
|
||||
|
||||
FAN_SWITCHES = [
|
||||
BAFSwitchDescription(
|
||||
key="fan_beep_enable",
|
||||
name="Beep",
|
||||
@@ -120,6 +123,8 @@ async def async_setup_entry(
|
||||
descriptions.extend(FAN_SWITCHES)
|
||||
if device.has_light:
|
||||
descriptions.extend(LIGHT_SWITCHES)
|
||||
if device.has_auto_comfort:
|
||||
descriptions.extend(AUTO_COMFORT_SWITCHES)
|
||||
async_add_entities(BAFSwitch(device, description) for description in descriptions)
|
||||
|
||||
|
||||
|
||||
@@ -24,10 +24,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.components.media_player.browse_media import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.components.media_player.const import (
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
MEDIA_TYPE_MUSIC,
|
||||
)
|
||||
from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_HOST,
|
||||
@@ -1023,11 +1020,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
return await self.send_bluesound_command(f"Play?seek={float(position)}")
|
||||
|
||||
async def async_play_media(self, media_type, media_id, **kwargs):
|
||||
"""
|
||||
Send the play_media command to the media player.
|
||||
|
||||
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
|
||||
"""
|
||||
"""Send the play_media command to the media player."""
|
||||
if self.is_grouped and not self.is_master:
|
||||
return
|
||||
|
||||
@@ -1041,9 +1034,6 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
url = f"Play?url={media_id}"
|
||||
|
||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||
return await self.send_bluesound_command(url)
|
||||
|
||||
return await self.send_bluesound_command(url)
|
||||
|
||||
async def async_volume_up(self):
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
|
||||
from bimmer_connected.account import MyBMWAccount
|
||||
from bimmer_connected.api.regions import get_region_from_name
|
||||
from bimmer_connected.vehicle.models import GPSPosition
|
||||
from bimmer_connected.models import GPSPosition
|
||||
from httpx import HTTPError, TimeoutException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -32,6 +32,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
entry.data[CONF_PASSWORD],
|
||||
get_region_from_name(entry.data[CONF_REGION]),
|
||||
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
|
||||
use_metric_units=hass.config.units.is_metric,
|
||||
)
|
||||
self.read_only = entry.options[CONF_READ_ONLY]
|
||||
self._entry = entry
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"requirements": ["bimmer_connected==0.9.0"],
|
||||
"requirements": ["bimmer_connected==0.9.4"],
|
||||
"codeowners": ["@gerard33", "@rikroe"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -6,8 +6,8 @@ from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from bimmer_connected.models import ValueWithUnit
|
||||
from bimmer_connected.vehicle import MyBMWVehicle
|
||||
from bimmer_connected.vehicle.models import ValueWithUnit
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for WebDav Calendar."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import re
|
||||
@@ -143,15 +142,13 @@ class WebDavCalendarEntity(CalendarEntity):
|
||||
def update(self):
|
||||
"""Update event data."""
|
||||
self.data.update()
|
||||
event = copy.deepcopy(self.data.event)
|
||||
if event is None:
|
||||
self._event = event
|
||||
return
|
||||
(summary, offset) = extract_offset(event.summary, OFFSET)
|
||||
event.summary = summary
|
||||
self._event = event
|
||||
self._event = self.data.event
|
||||
self._attr_extra_state_attributes = {
|
||||
"offset_reached": is_offset_reached(event.start_datetime_local, offset)
|
||||
"offset_reached": is_offset_reached(
|
||||
self._event.start_datetime_local, self.data.offset
|
||||
)
|
||||
if self._event
|
||||
else False
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +162,7 @@ class WebDavCalendarData:
|
||||
self.include_all_day = include_all_day
|
||||
self.search = search
|
||||
self.event = None
|
||||
self.offset = None
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
@@ -264,13 +262,15 @@ class WebDavCalendarData:
|
||||
return
|
||||
|
||||
# Populate the entity attributes with the event values
|
||||
(summary, offset) = extract_offset(vevent.summary.value, OFFSET)
|
||||
self.event = CalendarEvent(
|
||||
summary=vevent.summary.value,
|
||||
summary=summary,
|
||||
start=vevent.dtstart.value,
|
||||
end=self.get_end_date(vevent),
|
||||
location=self.get_attr_value(vevent, "location"),
|
||||
description=self.get_attr_value(vevent, "description"),
|
||||
)
|
||||
self.offset = offset
|
||||
|
||||
@staticmethod
|
||||
def is_matching(vevent, search):
|
||||
|
||||
@@ -266,10 +266,8 @@ async def parse_m3u(hass, url):
|
||||
hls_content_types = (
|
||||
# https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10
|
||||
"application/vnd.apple.mpegurl",
|
||||
# Some sites serve these as the informal HLS m3u type.
|
||||
"application/x-mpegurl",
|
||||
"audio/mpegurl",
|
||||
"audio/x-mpegurl",
|
||||
# Additional informal types used by Mozilla gecko not included as they
|
||||
# don't reliably indicate HLS streams
|
||||
)
|
||||
m3u_data = await _fetch_playlist(hass, url, hls_content_types)
|
||||
m3u_lines = m3u_data.splitlines()
|
||||
@@ -292,6 +290,9 @@ async def parse_m3u(hass, url):
|
||||
elif line.startswith("#EXT-X-VERSION:"):
|
||||
# HLS stream, supported by cast devices
|
||||
raise PlaylistSupported("HLS")
|
||||
elif line.startswith("#EXT-X-STREAM-INF:"):
|
||||
# HLS stream, supported by cast devices
|
||||
raise PlaylistSupported("HLS")
|
||||
elif line.startswith("#"):
|
||||
# Ignore other extensions
|
||||
continue
|
||||
|
||||
@@ -39,7 +39,6 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
self._cur_entity_prefs = self._prefs.google_entity_configs
|
||||
self._cur_default_expose = self._prefs.google_default_expose
|
||||
self._sync_entities_lock = asyncio.Lock()
|
||||
self._sync_on_started = False
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
@@ -195,6 +194,8 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
):
|
||||
await async_setup_component(self.hass, GOOGLE_DOMAIN, {})
|
||||
|
||||
sync_entities = False
|
||||
|
||||
if self.should_report_state != self.is_reporting_state:
|
||||
if self.should_report_state:
|
||||
self.async_enable_report_state()
|
||||
@@ -203,7 +204,7 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
|
||||
# State reporting is reported as a property on entities.
|
||||
# So when we change it, we need to sync all entities.
|
||||
await self.async_sync_entities_all()
|
||||
sync_entities = True
|
||||
|
||||
# If entity prefs are the same or we have filter in config.yaml,
|
||||
# don't sync.
|
||||
@@ -215,12 +216,16 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
|
||||
if self.enabled and not self.is_local_sdk_active:
|
||||
self.async_enable_local_sdk()
|
||||
sync_entities = True
|
||||
elif not self.enabled and self.is_local_sdk_active:
|
||||
self.async_disable_local_sdk()
|
||||
|
||||
self._cur_entity_prefs = prefs.google_entity_configs
|
||||
self._cur_default_expose = prefs.google_default_expose
|
||||
|
||||
if sync_entities and self.hass.is_running:
|
||||
await self.async_sync_entities_all()
|
||||
|
||||
@callback
|
||||
def _handle_entity_registry_updated(self, event: Event) -> None:
|
||||
"""Handle when entity registry updated."""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
import logging
|
||||
import re
|
||||
from types import MappingProxyType
|
||||
@@ -481,7 +482,10 @@ class ElkEntity(Entity):
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the default attributes of the element."""
|
||||
return {**self._element.as_dict(), **self.initial_attrs()}
|
||||
dict_as_str = {}
|
||||
for key, val in self._element.as_dict().items():
|
||||
dict_as_str[key] = val.value if isinstance(val, Enum) else val
|
||||
return {**dict_as_str, **self.initial_attrs()}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -46,6 +46,8 @@ class FibaroCover(FibaroDevice, CoverEntity):
|
||||
self._attr_supported_features = (
|
||||
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||
)
|
||||
if "stop" in self.fibaro_device.actions:
|
||||
self._attr_supported_features |= CoverEntityFeature.STOP
|
||||
|
||||
@staticmethod
|
||||
def bound(position):
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
from bleak import BleakScanner
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
from fjaraskupan import DEVICE_NAME, Device, State, device_filter
|
||||
from fjaraskupan import Device, State, device_filter
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
@@ -90,7 +90,7 @@ class EntryState:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Fjäråskupan from a config entry."""
|
||||
|
||||
scanner = BleakScanner(filters={"Pattern": DEVICE_NAME, "DuplicateData": True})
|
||||
scanner = BleakScanner(filters={"DuplicateData": True})
|
||||
|
||||
state = EntryState(scanner, {})
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
@@ -7,7 +7,7 @@ import async_timeout
|
||||
from bleak import BleakScanner
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
from fjaraskupan import DEVICE_NAME, device_filter
|
||||
from fjaraskupan import device_filter
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_flow import register_discovery_flow
|
||||
@@ -28,7 +28,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
|
||||
|
||||
async with BleakScanner(
|
||||
detection_callback=detection,
|
||||
filters={"Pattern": DEVICE_NAME, "DuplicateData": True},
|
||||
filters={"DuplicateData": True},
|
||||
):
|
||||
try:
|
||||
async with async_timeout.timeout(CONST_WAIT_TIME):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20220526.0"],
|
||||
"requirements": ["home-assistant-frontend==20220601.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -199,18 +199,24 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
_LOGGER.warning(
|
||||
"Configuration of Google Calendar in YAML in configuration.yaml is "
|
||||
"is deprecated and will be removed in a future release; Your existing "
|
||||
"OAuth Application Credentials and other settings have been imported "
|
||||
"OAuth Application Credentials and access settings have been imported "
|
||||
"into the UI automatically and can be safely removed from your "
|
||||
"configuration.yaml file"
|
||||
)
|
||||
|
||||
if conf.get(CONF_TRACK_NEW) is False:
|
||||
# The track_new as False would previously result in new entries
|
||||
# in google_calendars.yaml with track set to Fasle which is
|
||||
# handled at calendar entity creation time.
|
||||
_LOGGER.warning(
|
||||
"You must manually set the integration System Options in the "
|
||||
"UI to disable newly discovered entities going forward"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Google from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
async_upgrade_entry(hass, entry)
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
@@ -233,10 +239,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except aiohttp.ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
access = FeatureAccess[entry.options[CONF_CALENDAR_ACCESS]]
|
||||
token_scopes = session.token.get("scope", [])
|
||||
if access.scope not in token_scopes:
|
||||
_LOGGER.debug("Scope '%s' not in scopes '%s'", access.scope, token_scopes)
|
||||
if not async_entry_has_scopes(hass, entry):
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Required scopes are not available, reauth required"
|
||||
)
|
||||
@@ -247,37 +250,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await async_setup_services(hass, calendar_service)
|
||||
# Only expose the add event service if we have the correct permissions
|
||||
if access is FeatureAccess.read_write:
|
||||
if get_feature_access(hass, entry) is FeatureAccess.read_write:
|
||||
await async_setup_add_event_service(hass, calendar_service)
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
# Reload entry when options are updated
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def async_upgrade_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Upgrade the config entry if needed."""
|
||||
if DATA_CONFIG not in hass.data[DOMAIN] and entry.options:
|
||||
return
|
||||
|
||||
options = (
|
||||
entry.options
|
||||
if entry.options
|
||||
else {
|
||||
CONF_CALENDAR_ACCESS: get_feature_access(hass).name,
|
||||
}
|
||||
)
|
||||
disable_new_entities = (
|
||||
not hass.data[DOMAIN].get(DATA_CONFIG, {}).get(CONF_TRACK_NEW, True)
|
||||
)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options=options,
|
||||
pref_disable_new_entities=disable_new_entities,
|
||||
)
|
||||
def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Verify that the config entry desired scope is present in the oauth token."""
|
||||
access = get_feature_access(hass, entry)
|
||||
token_scopes = entry.data.get("token", {}).get("scope", [])
|
||||
return access.scope in token_scopes
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -286,8 +273,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Reload the config entry when it changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
"""Reload config entry if the access options change."""
|
||||
if not async_entry_has_scopes(hass, entry):
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_setup_services(
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from collections.abc import Awaitable, Callable
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, cast
|
||||
|
||||
import aiohttp
|
||||
@@ -19,6 +18,7 @@ from oauth2client.client import (
|
||||
)
|
||||
|
||||
from homeassistant.components.application_credentials import AuthImplementation
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
@@ -49,12 +49,16 @@ class DeviceAuth(AuthImplementation):
|
||||
async def async_resolve_external_data(self, external_data: Any) -> dict:
|
||||
"""Resolve a Google API Credentials object to Home Assistant token."""
|
||||
creds: Credentials = external_data[DEVICE_AUTH_CREDS]
|
||||
delta = creds.token_expiry.replace(tzinfo=datetime.timezone.utc) - dt.utcnow()
|
||||
_LOGGER.debug(
|
||||
"Token expires at %s (in %s)", creds.token_expiry, delta.total_seconds()
|
||||
)
|
||||
return {
|
||||
"access_token": creds.access_token,
|
||||
"refresh_token": creds.refresh_token,
|
||||
"scope": " ".join(creds.scopes),
|
||||
"token_type": "Bearer",
|
||||
"expires_in": creds.token_expiry.timestamp() - time.time(),
|
||||
"expires_in": delta.total_seconds(),
|
||||
}
|
||||
|
||||
|
||||
@@ -127,8 +131,17 @@ class DeviceFlow:
|
||||
)
|
||||
|
||||
|
||||
def get_feature_access(hass: HomeAssistant) -> FeatureAccess:
|
||||
def get_feature_access(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry | None = None
|
||||
) -> FeatureAccess:
|
||||
"""Return the desired calendar feature access."""
|
||||
if (
|
||||
config_entry
|
||||
and config_entry.options
|
||||
and CONF_CALENDAR_ACCESS in config_entry.options
|
||||
):
|
||||
return FeatureAccess[config_entry.options[CONF_CALENDAR_ACCESS]]
|
||||
|
||||
# This may be called during config entry setup without integration setup running when there
|
||||
# is no google entry in configuration.yaml
|
||||
return cast(
|
||||
|
||||
@@ -213,6 +213,9 @@ class AbstractConfig(ABC):
|
||||
|
||||
async def async_sync_entities_all(self):
|
||||
"""Sync all entities to Google for all registered agents."""
|
||||
if not self._store.agent_user_ids:
|
||||
return 204
|
||||
|
||||
res = await gather(
|
||||
*(
|
||||
self.async_sync_entities(agent_user_id)
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing_extensions import ParamSpec
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerEnqueue,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
)
|
||||
@@ -73,6 +74,14 @@ CONTROL_TO_SUPPORT = {
|
||||
heos_const.CONTROL_PLAY_NEXT: MediaPlayerEntityFeature.NEXT_TRACK,
|
||||
}
|
||||
|
||||
HA_HEOS_ENQUEUE_MAP = {
|
||||
None: heos_const.ADD_QUEUE_REPLACE_AND_PLAY,
|
||||
MediaPlayerEnqueue.ADD: heos_const.ADD_QUEUE_ADD_TO_END,
|
||||
MediaPlayerEnqueue.REPLACE: heos_const.ADD_QUEUE_REPLACE_AND_PLAY,
|
||||
MediaPlayerEnqueue.NEXT: heos_const.ADD_QUEUE_PLAY_NEXT,
|
||||
MediaPlayerEnqueue.PLAY: heos_const.ADD_QUEUE_PLAY_NOW,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -224,11 +233,8 @@ class HeosMediaPlayer(MediaPlayerEntity):
|
||||
playlist = next((p for p in playlists if p.name == media_id), None)
|
||||
if not playlist:
|
||||
raise ValueError(f"Invalid playlist '{media_id}'")
|
||||
add_queue_option = (
|
||||
heos_const.ADD_QUEUE_ADD_TO_END
|
||||
if kwargs.get(ATTR_MEDIA_ENQUEUE)
|
||||
else heos_const.ADD_QUEUE_REPLACE_AND_PLAY
|
||||
)
|
||||
add_queue_option = HA_HEOS_ENQUEUE_MAP.get(kwargs.get(ATTR_MEDIA_ENQUEUE))
|
||||
|
||||
await self._player.add_to_queue(playlist, add_queue_option)
|
||||
return
|
||||
|
||||
|
||||
@@ -67,13 +67,15 @@ class HistoryStats:
|
||||
current_period_end_timestamp = floored_timestamp(current_period_end)
|
||||
previous_period_start_timestamp = floored_timestamp(previous_period_start)
|
||||
previous_period_end_timestamp = floored_timestamp(previous_period_end)
|
||||
now_timestamp = floored_timestamp(datetime.datetime.now())
|
||||
utc_now = dt_util.utcnow()
|
||||
now_timestamp = floored_timestamp(utc_now)
|
||||
|
||||
if now_timestamp < current_period_start_timestamp:
|
||||
if current_period_start > utc_now:
|
||||
# History cannot tell the future
|
||||
self._history_current_period = []
|
||||
self._previous_run_before_start = True
|
||||
|
||||
self._state = HistoryStatsState(None, None, self._period)
|
||||
return self._state
|
||||
#
|
||||
# We avoid querying the database if the below did NOT happen:
|
||||
#
|
||||
@@ -82,7 +84,7 @@ class HistoryStats:
|
||||
# - The period shrank in size
|
||||
# - The previous period ended before now
|
||||
#
|
||||
elif (
|
||||
if (
|
||||
not self._previous_run_before_start
|
||||
and current_period_start_timestamp == previous_period_start_timestamp
|
||||
and (
|
||||
@@ -117,10 +119,6 @@ class HistoryStats:
|
||||
)
|
||||
self._previous_run_before_start = False
|
||||
|
||||
if not self._history_current_period:
|
||||
self._state = HistoryStatsState(None, None, self._period)
|
||||
return self._state
|
||||
|
||||
hours_matched, match_count = self._async_compute_hours_and_changes(
|
||||
now_timestamp,
|
||||
current_period_start_timestamp,
|
||||
|
||||
@@ -75,9 +75,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Hive from a config entry."""
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
hive = Hive(websession)
|
||||
web_session = aiohttp_client.async_get_clientsession(hass)
|
||||
hive_config = dict(entry.data)
|
||||
hive = Hive(web_session)
|
||||
|
||||
hive_config["options"] = {}
|
||||
hive_config["options"].update(
|
||||
|
||||
@@ -102,7 +102,9 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
raise UnknownHiveError
|
||||
|
||||
# Setup the config entry
|
||||
await self.hive_auth.device_registration("Home Assistant")
|
||||
self.data["tokens"] = self.tokens
|
||||
self.data["device_data"] = await self.hive_auth.getDeviceData()
|
||||
if self.context["source"] == config_entries.SOURCE_REAUTH:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.entry, title=self.data["username"], data=self.data
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Hive",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hive",
|
||||
"requirements": ["pyhiveapi==0.4.2"],
|
||||
"requirements": ["pyhiveapi==0.5.5"],
|
||||
"codeowners": ["@Rendili", "@KJonline"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["apyhiveapi"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Diagnostics support for P1 Monitor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
@@ -21,10 +22,10 @@ async def async_get_config_entry_diagnostics(
|
||||
coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
meter_data = {
|
||||
"device": coordinator.api.device.todict(),
|
||||
"data": coordinator.api.data.todict(),
|
||||
"state": coordinator.api.state.todict()
|
||||
if coordinator.api.state is not None
|
||||
"device": asdict(coordinator.data["device"]),
|
||||
"data": asdict(coordinator.data["data"]),
|
||||
"state": asdict(coordinator.data["state"])
|
||||
if coordinator.data["state"] is not None
|
||||
else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, IALARMXR_TO_HASS
|
||||
from .const import DOMAIN
|
||||
from .utils import async_get_ialarmxr_mac
|
||||
|
||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL]
|
||||
@@ -74,7 +74,7 @@ class IAlarmXRDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
def __init__(self, hass: HomeAssistant, ialarmxr: IAlarmXR, mac: str) -> None:
|
||||
"""Initialize global iAlarm data updater."""
|
||||
self.ialarmxr: IAlarmXR = ialarmxr
|
||||
self.state: str | None = None
|
||||
self.state: int | None = None
|
||||
self.host: str = ialarmxr.host
|
||||
self.mac: str = mac
|
||||
|
||||
@@ -90,7 +90,7 @@ class IAlarmXRDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
status: int = self.ialarmxr.get_status()
|
||||
_LOGGER.debug("iAlarmXR status: %s", status)
|
||||
|
||||
self.state = IALARMXR_TO_HASS.get(status)
|
||||
self.state = status
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from iAlarmXR."""
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
"""Interfaces with iAlarmXR control panels."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pyialarmxr import IAlarmXR
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
@@ -15,6 +23,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from . import IAlarmXRDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
|
||||
IALARMXR_TO_HASS = {
|
||||
IAlarmXR.ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
|
||||
IAlarmXR.ARMED_STAY: STATE_ALARM_ARMED_HOME,
|
||||
IAlarmXR.DISARMED: STATE_ALARM_DISARMED,
|
||||
IAlarmXR.TRIGGERED: STATE_ALARM_TRIGGERED,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
@@ -24,7 +39,9 @@ async def async_setup_entry(
|
||||
async_add_entities([IAlarmXRPanel(coordinator)])
|
||||
|
||||
|
||||
class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity):
|
||||
class IAlarmXRPanel(
|
||||
CoordinatorEntity[IAlarmXRDataUpdateCoordinator], AlarmControlPanelEntity
|
||||
):
|
||||
"""Representation of an iAlarmXR device."""
|
||||
|
||||
_attr_supported_features = (
|
||||
@@ -37,7 +54,6 @@ class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity):
|
||||
def __init__(self, coordinator: IAlarmXRDataUpdateCoordinator) -> None:
|
||||
"""Initialize the alarm panel."""
|
||||
super().__init__(coordinator)
|
||||
self.coordinator: IAlarmXRDataUpdateCoordinator = coordinator
|
||||
self._attr_unique_id = coordinator.mac
|
||||
self._attr_device_info = DeviceInfo(
|
||||
manufacturer="Antifurto365 - Meian",
|
||||
@@ -48,7 +64,7 @@ class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity):
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the device."""
|
||||
return self.coordinator.state
|
||||
return IALARMXR_TO_HASS.get(self.coordinator.state)
|
||||
|
||||
def alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -72,13 +72,13 @@ class IAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"IAlarmXRGenericException with message: [ %s ]",
|
||||
ialarmxr_exception.message,
|
||||
)
|
||||
errors["base"] = "unknown"
|
||||
errors["base"] = "cannot_connect"
|
||||
except IAlarmXRSocketTimeoutException as ialarmxr_socket_timeout_exception:
|
||||
_LOGGER.debug(
|
||||
"IAlarmXRSocketTimeoutException with message: [ %s ]",
|
||||
ialarmxr_socket_timeout_exception.message,
|
||||
)
|
||||
errors["base"] = "unknown"
|
||||
errors["base"] = "timeout"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
"""Constants for the iAlarmXR integration."""
|
||||
from pyialarmxr import IAlarmXR
|
||||
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
|
||||
DOMAIN = "ialarm_xr"
|
||||
|
||||
IALARMXR_TO_HASS = {
|
||||
IAlarmXR.ARMED_AWAY: STATE_ALARM_ARMED_AWAY,
|
||||
IAlarmXR.ARMED_STAY: STATE_ALARM_ARMED_HOME,
|
||||
IAlarmXR.DISARMED: STATE_ALARM_DISARMED,
|
||||
IAlarmXR.TRIGGERED: STATE_ALARM_TRIGGERED,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"domain": "ialarm_xr",
|
||||
"name": "Antifurto365 iAlarmXR",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ialarmxr",
|
||||
"requirements": ["pyialarmxr==1.0.13"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/ialarm_xr",
|
||||
"requirements": ["pyialarmxr-homeassistant==1.0.18"],
|
||||
"codeowners": ["@bigmoby"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"timeout": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"timeout": "Timeout establishing connection",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
|
||||
@@ -154,17 +154,26 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
self._method = integration_method
|
||||
|
||||
self._attr_name = name if name is not None else f"{source_entity} integral"
|
||||
self._unit_template = (
|
||||
f"{'' if unit_prefix is None else unit_prefix}{{}}{unit_time}"
|
||||
)
|
||||
self._unit_template = f"{'' if unit_prefix is None else unit_prefix}{{}}"
|
||||
self._unit_of_measurement = None
|
||||
self._unit_prefix = UNIT_PREFIXES[unit_prefix]
|
||||
self._unit_time = UNIT_TIME[unit_time]
|
||||
self._unit_time_str = unit_time
|
||||
self._attr_state_class = SensorStateClass.TOTAL
|
||||
self._attr_icon = "mdi:chart-histogram"
|
||||
self._attr_should_poll = False
|
||||
self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity}
|
||||
|
||||
def _unit(self, source_unit: str) -> str:
|
||||
"""Derive unit from the source sensor, SI prefix and time unit."""
|
||||
unit_time = self._unit_time_str
|
||||
if source_unit.endswith(f"/{unit_time}"):
|
||||
integral_unit = source_unit[0 : (-(1 + len(unit_time)))]
|
||||
else:
|
||||
integral_unit = f"{source_unit}{unit_time}"
|
||||
|
||||
return self._unit_template.format(integral_unit)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -203,7 +212,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
update_state = False
|
||||
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if unit is not None:
|
||||
new_unit_of_measurement = self._unit_template.format(unit)
|
||||
new_unit_of_measurement = self._unit(unit)
|
||||
if self._unit_of_measurement != new_unit_of_measurement:
|
||||
self._unit_of_measurement = new_unit_of_measurement
|
||||
update_state = True
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
from pyisy.constants import (
|
||||
CMD_CLIMATE_FAN_SETTING,
|
||||
CMD_CLIMATE_MODE,
|
||||
ISY_VALUE_UNKNOWN,
|
||||
PROP_HEAT_COOL_STATE,
|
||||
PROP_HUMIDITY,
|
||||
PROP_SETPOINT_COOL,
|
||||
@@ -116,6 +117,8 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
|
||||
"""Return the current humidity."""
|
||||
if not (humidity := self._node.aux_properties.get(PROP_HUMIDITY)):
|
||||
return None
|
||||
if humidity.value == ISY_VALUE_UNKNOWN:
|
||||
return None
|
||||
return int(humidity.value)
|
||||
|
||||
@property
|
||||
|
||||
@@ -636,11 +636,6 @@ class KodiEntity(MediaPlayerEntity):
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return not self._connect_error
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
_LOGGER.debug("Firing event to turn on device")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "LCN",
|
||||
"config_flow": false,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"requirements": ["pypck==0.7.14"],
|
||||
"requirements": ["pypck==0.7.15"],
|
||||
"codeowners": ["@alengwenus"],
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"]
|
||||
|
||||
@@ -7,7 +7,10 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import frontend
|
||||
from homeassistant.components.recorder.const import DOMAIN as RECORDER_DOMAIN
|
||||
from homeassistant.components.recorder.filters import (
|
||||
extract_include_exclude_filter_conf,
|
||||
merge_include_exclude_filters,
|
||||
sqlalchemy_filter_from_include_exclude_conf,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@@ -115,9 +118,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass, "logbook", "logbook", "hass:format-list-bulleted-type"
|
||||
)
|
||||
|
||||
if conf := config.get(DOMAIN, {}):
|
||||
filters = sqlalchemy_filter_from_include_exclude_conf(conf)
|
||||
entities_filter = convert_include_exclude_filter(conf)
|
||||
recorder_conf = config.get(RECORDER_DOMAIN, {})
|
||||
logbook_conf = config.get(DOMAIN, {})
|
||||
recorder_filter = extract_include_exclude_filter_conf(recorder_conf)
|
||||
logbook_filter = extract_include_exclude_filter_conf(logbook_conf)
|
||||
merged_filter = merge_include_exclude_filters(recorder_filter, logbook_filter)
|
||||
|
||||
possible_merged_entities_filter = convert_include_exclude_filter(merged_filter)
|
||||
if not possible_merged_entities_filter.empty_filter:
|
||||
filters = sqlalchemy_filter_from_include_exclude_conf(merged_filter)
|
||||
entities_filter = possible_merged_entities_filter
|
||||
else:
|
||||
filters = None
|
||||
entities_filter = None
|
||||
|
||||
@@ -2,9 +2,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED
|
||||
from homeassistant.components.counter import DOMAIN as COUNTER_DOMAIN
|
||||
from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN
|
||||
from homeassistant.components.script import EVENT_SCRIPT_STARTED
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY
|
||||
|
||||
# Domains that are always continuous
|
||||
ALWAYS_CONTINUOUS_DOMAINS = {COUNTER_DOMAIN, PROXIMITY_DOMAIN}
|
||||
|
||||
# Domains that are continuous if there is a UOM set on the entity
|
||||
CONDITIONALLY_CONTINUOUS_DOMAINS = {SENSOR_DOMAIN}
|
||||
|
||||
ATTR_MESSAGE = "message"
|
||||
|
||||
DOMAIN = "logbook"
|
||||
@@ -30,13 +39,11 @@ LOGBOOK_ENTRY_NAME = "name"
|
||||
LOGBOOK_ENTRY_STATE = "state"
|
||||
LOGBOOK_ENTRY_WHEN = "when"
|
||||
|
||||
ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE}
|
||||
ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY = {
|
||||
EVENT_LOGBOOK_ENTRY,
|
||||
EVENT_AUTOMATION_TRIGGERED,
|
||||
EVENT_SCRIPT_STARTED,
|
||||
}
|
||||
# Automation events that can affect an entity_id or device_id
|
||||
AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED}
|
||||
|
||||
# Events that are built-in to the logbook or core
|
||||
BUILT_IN_EVENTS = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE}
|
||||
|
||||
LOGBOOK_FILTERS = "logbook_filters"
|
||||
LOGBOOK_ENTITIES_FILTER = "entities_filter"
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
from homeassistant.components.sensor import ATTR_STATE_CLASS
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_DOMAIN,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
EVENT_LOGBOOK_ENTRY,
|
||||
@@ -19,15 +20,13 @@ from homeassistant.core import (
|
||||
State,
|
||||
callback,
|
||||
is_callback,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.entityfilter import EntityFilter
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
|
||||
from .const import (
|
||||
ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED,
|
||||
DOMAIN,
|
||||
ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY,
|
||||
)
|
||||
from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN
|
||||
from .models import LazyEventPartialState
|
||||
|
||||
|
||||
@@ -41,6 +40,25 @@ def async_filter_entities(hass: HomeAssistant, entity_ids: list[str]) -> list[st
|
||||
]
|
||||
|
||||
|
||||
@callback
|
||||
def _async_config_entries_for_ids(
|
||||
hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None
|
||||
) -> set[str]:
|
||||
"""Find the config entry ids for a set of entities or devices."""
|
||||
config_entry_ids: set[str] = set()
|
||||
if entity_ids:
|
||||
eng_reg = er.async_get(hass)
|
||||
for entity_id in entity_ids:
|
||||
if (entry := eng_reg.async_get(entity_id)) and entry.config_entry_id:
|
||||
config_entry_ids.add(entry.config_entry_id)
|
||||
if device_ids:
|
||||
dev_reg = dr.async_get(hass)
|
||||
for device_id in device_ids:
|
||||
if (device := dev_reg.async_get(device_id)) and device.config_entries:
|
||||
config_entry_ids |= device.config_entries
|
||||
return config_entry_ids
|
||||
|
||||
|
||||
def async_determine_event_types(
|
||||
hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None
|
||||
) -> tuple[str, ...]:
|
||||
@@ -49,42 +67,91 @@ def async_determine_event_types(
|
||||
str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]]
|
||||
] = hass.data.get(DOMAIN, {})
|
||||
if not entity_ids and not device_ids:
|
||||
return (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events)
|
||||
config_entry_ids: set[str] = set()
|
||||
intrested_event_types: set[str] = set()
|
||||
return (*BUILT_IN_EVENTS, *external_events)
|
||||
|
||||
interested_domains: set[str] = set()
|
||||
for entry_id in _async_config_entries_for_ids(hass, entity_ids, device_ids):
|
||||
if entry := hass.config_entries.async_get_entry(entry_id):
|
||||
interested_domains.add(entry.domain)
|
||||
|
||||
#
|
||||
# automations and scripts can refer to entities or devices
|
||||
# but they do not have a config entry so we need
|
||||
# to add them since we have historically included
|
||||
# them when matching only on entities
|
||||
#
|
||||
intrested_event_types: set[str] = {
|
||||
external_event
|
||||
for external_event, domain_call in external_events.items()
|
||||
if domain_call[0] in interested_domains
|
||||
} | AUTOMATION_EVENTS
|
||||
if entity_ids:
|
||||
#
|
||||
# Home Assistant doesn't allow firing events from
|
||||
# entities so we have a limited list to check
|
||||
#
|
||||
# automations and scripts can refer to entities
|
||||
# but they do not have a config entry so we need
|
||||
# to add them.
|
||||
#
|
||||
# We also allow entity_ids to be recorded via
|
||||
# manual logbook entries.
|
||||
#
|
||||
intrested_event_types |= ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY
|
||||
# We also allow entity_ids to be recorded via manual logbook entries.
|
||||
intrested_event_types.add(EVENT_LOGBOOK_ENTRY)
|
||||
|
||||
if device_ids:
|
||||
dev_reg = dr.async_get(hass)
|
||||
for device_id in device_ids:
|
||||
if (device := dev_reg.async_get(device_id)) and device.config_entries:
|
||||
config_entry_ids |= device.config_entries
|
||||
interested_domains: set[str] = set()
|
||||
for entry_id in config_entry_ids:
|
||||
if entry := hass.config_entries.async_get_entry(entry_id):
|
||||
interested_domains.add(entry.domain)
|
||||
for external_event, domain_call in external_events.items():
|
||||
if domain_call[0] in interested_domains:
|
||||
intrested_event_types.add(external_event)
|
||||
return tuple(intrested_event_types)
|
||||
|
||||
return tuple(
|
||||
event_type
|
||||
for event_type in (EVENT_LOGBOOK_ENTRY, *external_events)
|
||||
if event_type in intrested_event_types
|
||||
)
|
||||
|
||||
@callback
|
||||
def extract_attr(source: dict[str, Any], attr: str) -> list[str]:
|
||||
"""Extract an attribute as a list or string."""
|
||||
if (value := source.get(attr)) is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return str(value).split(",")
|
||||
|
||||
|
||||
@callback
|
||||
def event_forwarder_filtered(
|
||||
target: Callable[[Event], None],
|
||||
entities_filter: EntityFilter | None,
|
||||
entity_ids: list[str] | None,
|
||||
device_ids: list[str] | None,
|
||||
) -> Callable[[Event], None]:
|
||||
"""Make a callable to filter events."""
|
||||
if not entities_filter and not entity_ids and not device_ids:
|
||||
# No filter
|
||||
# - Script Trace (context ids)
|
||||
# - Automation Trace (context ids)
|
||||
return target
|
||||
|
||||
if entities_filter:
|
||||
# We have an entity filter:
|
||||
# - Logbook panel
|
||||
|
||||
@callback
|
||||
def _forward_events_filtered_by_entities_filter(event: Event) -> None:
|
||||
assert entities_filter is not None
|
||||
event_data = event.data
|
||||
entity_ids = extract_attr(event_data, ATTR_ENTITY_ID)
|
||||
if entity_ids and not any(
|
||||
entities_filter(entity_id) for entity_id in entity_ids
|
||||
):
|
||||
return
|
||||
domain = event_data.get(ATTR_DOMAIN)
|
||||
if domain and not entities_filter(f"{domain}._"):
|
||||
return
|
||||
target(event)
|
||||
|
||||
return _forward_events_filtered_by_entities_filter
|
||||
|
||||
# We are filtering on entity_ids and/or device_ids:
|
||||
# - Areas
|
||||
# - Devices
|
||||
# - Logbook Card
|
||||
entity_ids_set = set(entity_ids) if entity_ids else set()
|
||||
device_ids_set = set(device_ids) if device_ids else set()
|
||||
|
||||
@callback
|
||||
def _forward_events_filtered_by_device_entity_ids(event: Event) -> None:
|
||||
event_data = event.data
|
||||
if entity_ids_set.intersection(
|
||||
extract_attr(event_data, ATTR_ENTITY_ID)
|
||||
) or device_ids_set.intersection(extract_attr(event_data, ATTR_DEVICE_ID)):
|
||||
target(event)
|
||||
|
||||
return _forward_events_filtered_by_device_entity_ids
|
||||
|
||||
|
||||
@callback
|
||||
@@ -93,6 +160,7 @@ def async_subscribe_events(
|
||||
subscriptions: list[CALLBACK_TYPE],
|
||||
target: Callable[[Event], None],
|
||||
event_types: tuple[str, ...],
|
||||
entities_filter: EntityFilter | None,
|
||||
entity_ids: list[str] | None,
|
||||
device_ids: list[str] | None,
|
||||
) -> None:
|
||||
@@ -103,34 +171,30 @@ def async_subscribe_events(
|
||||
"""
|
||||
ent_reg = er.async_get(hass)
|
||||
assert is_callback(target), "target must be a callback"
|
||||
event_forwarder = target
|
||||
|
||||
if entity_ids or device_ids:
|
||||
entity_ids_set = set(entity_ids) if entity_ids else set()
|
||||
device_ids_set = set(device_ids) if device_ids else set()
|
||||
|
||||
@callback
|
||||
def _forward_events_filtered(event: Event) -> None:
|
||||
event_data = event.data
|
||||
if (
|
||||
entity_ids_set and event_data.get(ATTR_ENTITY_ID) in entity_ids_set
|
||||
) or (device_ids_set and event_data.get(ATTR_DEVICE_ID) in device_ids_set):
|
||||
target(event)
|
||||
|
||||
event_forwarder = _forward_events_filtered
|
||||
|
||||
event_forwarder = event_forwarder_filtered(
|
||||
target, entities_filter, entity_ids, device_ids
|
||||
)
|
||||
for event_type in event_types:
|
||||
subscriptions.append(
|
||||
hass.bus.async_listen(event_type, event_forwarder, run_immediately=True)
|
||||
)
|
||||
|
||||
if device_ids and not entity_ids:
|
||||
# No entities to subscribe to but we are filtering
|
||||
# on device ids so we do not want to get any state
|
||||
# changed events
|
||||
return
|
||||
|
||||
@callback
|
||||
def _forward_state_events_filtered(event: Event) -> None:
|
||||
if event.data.get("old_state") is None or event.data.get("new_state") is None:
|
||||
return
|
||||
state: State = event.data["new_state"]
|
||||
if not _is_state_filtered(ent_reg, state):
|
||||
target(event)
|
||||
if _is_state_filtered(ent_reg, state) or (
|
||||
entities_filter and not entities_filter(state.entity_id)
|
||||
):
|
||||
return
|
||||
target(event)
|
||||
|
||||
if entity_ids:
|
||||
subscriptions.append(
|
||||
@@ -172,7 +236,8 @@ def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool:
|
||||
we only get significant changes (state.last_changed != state.last_updated)
|
||||
"""
|
||||
return bool(
|
||||
state.last_changed != state.last_updated
|
||||
split_entity_id(state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS
|
||||
or state.last_changed != state.last_updated
|
||||
or ATTR_UNIT_OF_MEASUREMENT in state.attributes
|
||||
or is_sensor_continuous(ent_reg, state.entity_id)
|
||||
)
|
||||
@@ -187,7 +252,8 @@ def _is_entity_id_filtered(
|
||||
from the database when a list of entities is requested.
|
||||
"""
|
||||
return bool(
|
||||
(state := hass.states.get(entity_id))
|
||||
split_entity_id(entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS
|
||||
or (state := hass.states.get(entity_id))
|
||||
and (ATTR_UNIT_OF_MEASUREMENT in state.attributes)
|
||||
or is_sensor_continuous(ent_reg, entity_id)
|
||||
)
|
||||
|
||||
@@ -5,8 +5,6 @@ from collections.abc import Callable, Generator
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime as dt
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.engine.row import Row
|
||||
@@ -30,7 +28,6 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entityfilter import EntityFilter
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import (
|
||||
@@ -46,7 +43,6 @@ from .const import (
|
||||
CONTEXT_STATE,
|
||||
CONTEXT_USER_ID,
|
||||
DOMAIN,
|
||||
LOGBOOK_ENTITIES_FILTER,
|
||||
LOGBOOK_ENTRY_DOMAIN,
|
||||
LOGBOOK_ENTRY_ENTITY_ID,
|
||||
LOGBOOK_ENTRY_ICON,
|
||||
@@ -62,11 +58,6 @@ from .models import EventAsRow, LazyEventPartialState, async_event_to_row
|
||||
from .queries import statement_for_request
|
||||
from .queries.common import PSUEDO_EVENT_STATE_CHANGED
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"')
|
||||
DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"')
|
||||
|
||||
|
||||
@dataclass
|
||||
class LogbookRun:
|
||||
@@ -106,10 +97,6 @@ class EventProcessor:
|
||||
self.device_ids = device_ids
|
||||
self.context_id = context_id
|
||||
self.filters: Filters | None = hass.data[LOGBOOK_FILTERS]
|
||||
if self.limited_select:
|
||||
self.entities_filter: EntityFilter | Callable[[str], bool] | None = None
|
||||
else:
|
||||
self.entities_filter = hass.data[LOGBOOK_ENTITIES_FILTER]
|
||||
format_time = (
|
||||
_row_time_fired_timestamp if timestamp else _row_time_fired_isoformat
|
||||
)
|
||||
@@ -183,7 +170,6 @@ class EventProcessor:
|
||||
return list(
|
||||
_humanify(
|
||||
row_generator,
|
||||
self.entities_filter,
|
||||
self.ent_reg,
|
||||
self.logbook_run,
|
||||
self.context_augmenter,
|
||||
@@ -193,7 +179,6 @@ class EventProcessor:
|
||||
|
||||
def _humanify(
|
||||
rows: Generator[Row | EventAsRow, None, None],
|
||||
entities_filter: EntityFilter | Callable[[str], bool] | None,
|
||||
ent_reg: er.EntityRegistry,
|
||||
logbook_run: LogbookRun,
|
||||
context_augmenter: ContextAugmenter,
|
||||
@@ -208,29 +193,13 @@ def _humanify(
|
||||
include_entity_name = logbook_run.include_entity_name
|
||||
format_time = logbook_run.format_time
|
||||
|
||||
def _keep_row(row: EventAsRow) -> bool:
|
||||
"""Check if the entity_filter rejects a row."""
|
||||
assert entities_filter is not None
|
||||
if entity_id := row.entity_id:
|
||||
return entities_filter(entity_id)
|
||||
if entity_id := row.data.get(ATTR_ENTITY_ID):
|
||||
return entities_filter(entity_id)
|
||||
if domain := row.data.get(ATTR_DOMAIN):
|
||||
return entities_filter(f"{domain}._")
|
||||
return True
|
||||
|
||||
# Process rows
|
||||
for row in rows:
|
||||
context_id = context_lookup.memorize(row)
|
||||
if row.context_only:
|
||||
continue
|
||||
event_type = row.event_type
|
||||
if event_type == EVENT_CALL_SERVICE or (
|
||||
entities_filter
|
||||
# We literally mean is EventAsRow not a subclass of EventAsRow
|
||||
and type(row) is EventAsRow # pylint: disable=unidiomatic-typecheck
|
||||
and not _keep_row(row)
|
||||
):
|
||||
if event_type == EVENT_CALL_SERVICE:
|
||||
continue
|
||||
if event_type is PSUEDO_EVENT_STATE_CHANGED:
|
||||
entity_id = row.entity_id
|
||||
@@ -407,7 +376,8 @@ class ContextAugmenter:
|
||||
def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool:
|
||||
"""Check of rows match by using the same method as Events __hash__."""
|
||||
if (
|
||||
(state_id := row.state_id) is not None
|
||||
row is other_row
|
||||
or (state_id := row.state_id) is not None
|
||||
and state_id == other_row.state_id
|
||||
or (event_id := row.event_id) is not None
|
||||
and event_id == other_row.event_id
|
||||
@@ -416,12 +386,6 @@ def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _row_event_data_extract(row: Row | EventAsRow, extractor: re.Pattern) -> str | None:
|
||||
"""Extract from event_data row."""
|
||||
result = extractor.search(row.shared_data or row.event_data or "")
|
||||
return result.group(1) if result else None
|
||||
|
||||
|
||||
def _row_time_fired_isoformat(row: Row | EventAsRow) -> str:
|
||||
"""Convert the row timed_fired to isoformat."""
|
||||
return process_timestamp_to_utc_isoformat(row.time_fired or dt_util.utcnow())
|
||||
|
||||
@@ -10,25 +10,31 @@ from sqlalchemy.sql.elements import ClauseList
|
||||
from sqlalchemy.sql.expression import literal
|
||||
from sqlalchemy.sql.selectable import Select
|
||||
|
||||
from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN
|
||||
from homeassistant.components.recorder.filters import like_domain_matchers
|
||||
from homeassistant.components.recorder.models import (
|
||||
EVENTS_CONTEXT_ID_INDEX,
|
||||
OLD_FORMAT_ATTRS_JSON,
|
||||
OLD_STATE,
|
||||
SHARED_ATTRS_JSON,
|
||||
STATES_CONTEXT_ID_INDEX,
|
||||
EventData,
|
||||
Events,
|
||||
StateAttributes,
|
||||
States,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
|
||||
CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN}
|
||||
CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS]
|
||||
from ..const import ALWAYS_CONTINUOUS_DOMAINS, CONDITIONALLY_CONTINUOUS_DOMAINS
|
||||
|
||||
# Domains that are continuous if there is a UOM set on the entity
|
||||
CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers(
|
||||
CONDITIONALLY_CONTINUOUS_DOMAINS
|
||||
)
|
||||
# Domains that are always continuous
|
||||
ALWAYS_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers(ALWAYS_CONTINUOUS_DOMAINS)
|
||||
|
||||
UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":'
|
||||
UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%"
|
||||
|
||||
|
||||
PSUEDO_EVENT_STATE_CHANGED = None
|
||||
# Since we don't store event_types and None
|
||||
# and we don't store state_changed in events
|
||||
@@ -121,9 +127,7 @@ def select_events_context_only() -> Select:
|
||||
By marking them as context_only we know they are only for
|
||||
linking context ids and we can avoid processing them.
|
||||
"""
|
||||
return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY).outerjoin(
|
||||
EventData, (Events.data_id == EventData.data_id)
|
||||
)
|
||||
return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY)
|
||||
|
||||
|
||||
def select_states_context_only() -> Select:
|
||||
@@ -220,29 +224,44 @@ def _missing_state_matcher() -> sqlalchemy.and_:
|
||||
def _not_continuous_entity_matcher() -> sqlalchemy.or_:
|
||||
"""Match non continuous entities."""
|
||||
return sqlalchemy.or_(
|
||||
_not_continuous_domain_matcher(),
|
||||
# First exclude domains that may be continuous
|
||||
_not_possible_continuous_domain_matcher(),
|
||||
# But let in the entities in the possible continuous domains
|
||||
# that are not actually continuous sensors because they lack a UOM
|
||||
sqlalchemy.and_(
|
||||
_continuous_domain_matcher, _not_uom_attributes_matcher()
|
||||
_conditionally_continuous_domain_matcher, _not_uom_attributes_matcher()
|
||||
).self_group(),
|
||||
)
|
||||
|
||||
|
||||
def _not_continuous_domain_matcher() -> sqlalchemy.and_:
|
||||
"""Match not continuous domains."""
|
||||
def _not_possible_continuous_domain_matcher() -> sqlalchemy.and_:
|
||||
"""Match not continuous domains.
|
||||
|
||||
This matches domain that are always considered continuous
|
||||
and domains that are conditionally (if they have a UOM)
|
||||
continuous domains.
|
||||
"""
|
||||
return sqlalchemy.and_(
|
||||
*[
|
||||
~States.entity_id.like(entity_domain)
|
||||
for entity_domain in CONTINUOUS_ENTITY_ID_LIKE
|
||||
for entity_domain in (
|
||||
*ALWAYS_CONTINUOUS_ENTITY_ID_LIKE,
|
||||
*CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE,
|
||||
)
|
||||
],
|
||||
).self_group()
|
||||
|
||||
|
||||
def _continuous_domain_matcher() -> sqlalchemy.or_:
|
||||
"""Match continuous domains."""
|
||||
def _conditionally_continuous_domain_matcher() -> sqlalchemy.or_:
|
||||
"""Match conditionally continuous domains.
|
||||
|
||||
This matches domain that are only considered
|
||||
continuous if a UOM is set.
|
||||
"""
|
||||
return sqlalchemy.or_(
|
||||
*[
|
||||
States.entity_id.like(entity_domain)
|
||||
for entity_domain in CONTINUOUS_ENTITY_ID_LIKE
|
||||
for entity_domain in CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE
|
||||
],
|
||||
).self_group()
|
||||
|
||||
@@ -252,3 +271,17 @@ def _not_uom_attributes_matcher() -> ClauseList:
|
||||
return ~StateAttributes.shared_attrs.like(
|
||||
UNIT_OF_MEASUREMENT_JSON_LIKE
|
||||
) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE)
|
||||
|
||||
|
||||
def apply_states_context_hints(query: Query) -> Query:
|
||||
"""Force mysql to use the right index on large context_id selects."""
|
||||
return query.with_hint(
|
||||
States, f"FORCE INDEX ({STATES_CONTEXT_ID_INDEX})", dialect_name="mysql"
|
||||
)
|
||||
|
||||
|
||||
def apply_events_context_hints(query: Query) -> Query:
|
||||
"""Force mysql to use the right index on large context_id selects."""
|
||||
return query.with_hint(
|
||||
Events, f"FORCE INDEX ({EVENTS_CONTEXT_ID_INDEX})", dialect_name="mysql"
|
||||
)
|
||||
|
||||
@@ -4,15 +4,22 @@ from __future__ import annotations
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime as dt
|
||||
|
||||
from sqlalchemy import lambda_stmt, select, union_all
|
||||
from sqlalchemy import lambda_stmt, select
|
||||
from sqlalchemy.orm import Query
|
||||
from sqlalchemy.sql.elements import ClauseList
|
||||
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
||||
from sqlalchemy.sql.selectable import CTE, CompoundSelect
|
||||
|
||||
from homeassistant.components.recorder.models import DEVICE_ID_IN_EVENT, Events, States
|
||||
from homeassistant.components.recorder.models import (
|
||||
DEVICE_ID_IN_EVENT,
|
||||
EventData,
|
||||
Events,
|
||||
States,
|
||||
)
|
||||
|
||||
from .common import (
|
||||
apply_events_context_hints,
|
||||
apply_states_context_hints,
|
||||
select_events_context_id_subquery,
|
||||
select_events_context_only,
|
||||
select_events_without_states,
|
||||
@@ -27,13 +34,10 @@ def _select_device_id_context_ids_sub_query(
|
||||
json_quotable_device_ids: list[str],
|
||||
) -> CompoundSelect:
|
||||
"""Generate a subquery to find context ids for multiple devices."""
|
||||
return select(
|
||||
union_all(
|
||||
select_events_context_id_subquery(start_day, end_day, event_types).where(
|
||||
apply_event_device_id_matchers(json_quotable_device_ids)
|
||||
),
|
||||
).c.context_id
|
||||
inner = select_events_context_id_subquery(start_day, end_day, event_types).where(
|
||||
apply_event_device_id_matchers(json_quotable_device_ids)
|
||||
)
|
||||
return select(inner.c.context_id).group_by(inner.c.context_id)
|
||||
|
||||
|
||||
def _apply_devices_context_union(
|
||||
@@ -51,8 +55,16 @@ def _apply_devices_context_union(
|
||||
json_quotable_device_ids,
|
||||
).cte()
|
||||
return query.union_all(
|
||||
select_events_context_only().where(Events.context_id.in_(devices_cte.select())),
|
||||
select_states_context_only().where(States.context_id.in_(devices_cte.select())),
|
||||
apply_events_context_hints(
|
||||
select_events_context_only()
|
||||
.select_from(devices_cte)
|
||||
.outerjoin(Events, devices_cte.c.context_id == Events.context_id)
|
||||
).outerjoin(EventData, (Events.data_id == EventData.data_id)),
|
||||
apply_states_context_hints(
|
||||
select_states_context_only()
|
||||
.select_from(devices_cte)
|
||||
.outerjoin(States, devices_cte.c.context_id == States.context_id)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,11 +14,14 @@ from homeassistant.components.recorder.models import (
|
||||
ENTITY_ID_IN_EVENT,
|
||||
ENTITY_ID_LAST_UPDATED_INDEX,
|
||||
OLD_ENTITY_ID_IN_EVENT,
|
||||
EventData,
|
||||
Events,
|
||||
States,
|
||||
)
|
||||
|
||||
from .common import (
|
||||
apply_events_context_hints,
|
||||
apply_states_context_hints,
|
||||
apply_states_filters,
|
||||
select_events_context_id_subquery,
|
||||
select_events_context_only,
|
||||
@@ -36,16 +39,15 @@ def _select_entities_context_ids_sub_query(
|
||||
json_quotable_entity_ids: list[str],
|
||||
) -> CompoundSelect:
|
||||
"""Generate a subquery to find context ids for multiple entities."""
|
||||
return select(
|
||||
union_all(
|
||||
select_events_context_id_subquery(start_day, end_day, event_types).where(
|
||||
apply_event_entity_id_matchers(json_quotable_entity_ids)
|
||||
),
|
||||
apply_entities_hints(select(States.context_id))
|
||||
.filter((States.last_updated > start_day) & (States.last_updated < end_day))
|
||||
.where(States.entity_id.in_(entity_ids)),
|
||||
).c.context_id
|
||||
union = union_all(
|
||||
select_events_context_id_subquery(start_day, end_day, event_types).where(
|
||||
apply_event_entity_id_matchers(json_quotable_entity_ids)
|
||||
),
|
||||
apply_entities_hints(select(States.context_id))
|
||||
.filter((States.last_updated > start_day) & (States.last_updated < end_day))
|
||||
.where(States.entity_id.in_(entity_ids)),
|
||||
)
|
||||
return select(union.c.context_id).group_by(union.c.context_id)
|
||||
|
||||
|
||||
def _apply_entities_context_union(
|
||||
@@ -64,14 +66,23 @@ def _apply_entities_context_union(
|
||||
entity_ids,
|
||||
json_quotable_entity_ids,
|
||||
).cte()
|
||||
# We used to optimize this to exclude rows we already in the union with
|
||||
# a States.entity_id.not_in(entity_ids) but that made the
|
||||
# query much slower on MySQL, and since we already filter them away
|
||||
# in the python code anyways since they will have context_only
|
||||
# set on them the impact is minimal.
|
||||
return query.union_all(
|
||||
states_query_for_entity_ids(start_day, end_day, entity_ids),
|
||||
select_events_context_only().where(
|
||||
Events.context_id.in_(entities_cte.select())
|
||||
apply_events_context_hints(
|
||||
select_events_context_only()
|
||||
.select_from(entities_cte)
|
||||
.outerjoin(Events, entities_cte.c.context_id == Events.context_id)
|
||||
).outerjoin(EventData, (Events.data_id == EventData.data_id)),
|
||||
apply_states_context_hints(
|
||||
select_states_context_only()
|
||||
.select_from(entities_cte)
|
||||
.outerjoin(States, entities_cte.c.context_id == States.context_id)
|
||||
),
|
||||
select_states_context_only()
|
||||
.where(States.entity_id.not_in(entity_ids))
|
||||
.where(States.context_id.in_(entities_cte.select())),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,9 +10,11 @@ from sqlalchemy.orm import Query
|
||||
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
||||
from sqlalchemy.sql.selectable import CTE, CompoundSelect
|
||||
|
||||
from homeassistant.components.recorder.models import Events, States
|
||||
from homeassistant.components.recorder.models import EventData, Events, States
|
||||
|
||||
from .common import (
|
||||
apply_events_context_hints,
|
||||
apply_states_context_hints,
|
||||
select_events_context_id_subquery,
|
||||
select_events_context_only,
|
||||
select_events_without_states,
|
||||
@@ -35,18 +37,17 @@ def _select_entities_device_id_context_ids_sub_query(
|
||||
json_quotable_device_ids: list[str],
|
||||
) -> CompoundSelect:
|
||||
"""Generate a subquery to find context ids for multiple entities and multiple devices."""
|
||||
return select(
|
||||
union_all(
|
||||
select_events_context_id_subquery(start_day, end_day, event_types).where(
|
||||
_apply_event_entity_id_device_id_matchers(
|
||||
json_quotable_entity_ids, json_quotable_device_ids
|
||||
)
|
||||
),
|
||||
apply_entities_hints(select(States.context_id))
|
||||
.filter((States.last_updated > start_day) & (States.last_updated < end_day))
|
||||
.where(States.entity_id.in_(entity_ids)),
|
||||
).c.context_id
|
||||
union = union_all(
|
||||
select_events_context_id_subquery(start_day, end_day, event_types).where(
|
||||
_apply_event_entity_id_device_id_matchers(
|
||||
json_quotable_entity_ids, json_quotable_device_ids
|
||||
)
|
||||
),
|
||||
apply_entities_hints(select(States.context_id))
|
||||
.filter((States.last_updated > start_day) & (States.last_updated < end_day))
|
||||
.where(States.entity_id.in_(entity_ids)),
|
||||
)
|
||||
return select(union.c.context_id).group_by(union.c.context_id)
|
||||
|
||||
|
||||
def _apply_entities_devices_context_union(
|
||||
@@ -66,14 +67,23 @@ def _apply_entities_devices_context_union(
|
||||
json_quotable_entity_ids,
|
||||
json_quotable_device_ids,
|
||||
).cte()
|
||||
# We used to optimize this to exclude rows we already in the union with
|
||||
# a States.entity_id.not_in(entity_ids) but that made the
|
||||
# query much slower on MySQL, and since we already filter them away
|
||||
# in the python code anyways since they will have context_only
|
||||
# set on them the impact is minimal.
|
||||
return query.union_all(
|
||||
states_query_for_entity_ids(start_day, end_day, entity_ids),
|
||||
select_events_context_only().where(
|
||||
Events.context_id.in_(devices_entities_cte.select())
|
||||
apply_events_context_hints(
|
||||
select_events_context_only()
|
||||
.select_from(devices_entities_cte)
|
||||
.outerjoin(Events, devices_entities_cte.c.context_id == Events.context_id)
|
||||
).outerjoin(EventData, (Events.data_id == EventData.data_id)),
|
||||
apply_states_context_hints(
|
||||
select_states_context_only()
|
||||
.select_from(devices_entities_cte)
|
||||
.outerjoin(States, devices_entities_cte.c.context_id == States.context_id)
|
||||
),
|
||||
select_states_context_only()
|
||||
.where(States.entity_id.not_in(entity_ids))
|
||||
.where(States.context_id.in_(devices_entities_cte.select())),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ from homeassistant.components.websocket_api import messages
|
||||
from homeassistant.components.websocket_api.connection import ActiveConnection
|
||||
from homeassistant.components.websocket_api.const import JSON_DUMP
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.entityfilter import EntityFilter
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import LOGBOOK_ENTITIES_FILTER
|
||||
from .helpers import (
|
||||
async_determine_event_types,
|
||||
async_filter_entities,
|
||||
@@ -67,6 +69,23 @@ async def _async_wait_for_recorder_sync(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_send_empty_response(
|
||||
connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None
|
||||
) -> None:
|
||||
"""Send an empty response.
|
||||
|
||||
The current case for this is when they ask for entity_ids
|
||||
that will all be filtered away because they have UOMs or
|
||||
state_class.
|
||||
"""
|
||||
connection.send_result(msg_id)
|
||||
stream_end_time = end_time or dt_util.utcnow()
|
||||
empty_stream_message = _generate_stream_message([], start_time, stream_end_time)
|
||||
empty_response = messages.event_message(msg_id, empty_stream_message)
|
||||
connection.send_message(JSON_DUMP(empty_response))
|
||||
|
||||
|
||||
async def _async_send_historical_events(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
@@ -171,6 +190,17 @@ async def _async_get_ws_stream_events(
|
||||
)
|
||||
|
||||
|
||||
def _generate_stream_message(
|
||||
events: list[dict[str, Any]], start_day: dt, end_day: dt
|
||||
) -> dict[str, Any]:
|
||||
"""Generate a logbook stream message response."""
|
||||
return {
|
||||
"events": events,
|
||||
"start_time": dt_util.utc_to_timestamp(start_day),
|
||||
"end_time": dt_util.utc_to_timestamp(end_day),
|
||||
}
|
||||
|
||||
|
||||
def _ws_stream_get_events(
|
||||
msg_id: int,
|
||||
start_day: dt,
|
||||
@@ -184,11 +214,7 @@ def _ws_stream_get_events(
|
||||
last_time = None
|
||||
if events:
|
||||
last_time = dt_util.utc_from_timestamp(events[-1]["when"])
|
||||
message = {
|
||||
"events": events,
|
||||
"start_time": dt_util.utc_to_timestamp(start_day),
|
||||
"end_time": dt_util.utc_to_timestamp(end_day),
|
||||
}
|
||||
message = _generate_stream_message(events, start_day, end_day)
|
||||
if partial:
|
||||
# This is a hint to consumers of the api that
|
||||
# we are about to send a another block of historical
|
||||
@@ -275,6 +301,10 @@ async def ws_event_stream(
|
||||
entity_ids = msg.get("entity_ids")
|
||||
if entity_ids:
|
||||
entity_ids = async_filter_entities(hass, entity_ids)
|
||||
if not entity_ids:
|
||||
_async_send_empty_response(connection, msg_id, start_time, end_time)
|
||||
return
|
||||
|
||||
event_types = async_determine_event_types(hass, entity_ids, device_ids)
|
||||
event_processor = EventProcessor(
|
||||
hass,
|
||||
@@ -337,8 +367,18 @@ async def ws_event_stream(
|
||||
)
|
||||
_unsub()
|
||||
|
||||
entities_filter: EntityFilter | None = None
|
||||
if not event_processor.limited_select:
|
||||
entities_filter = hass.data[LOGBOOK_ENTITIES_FILTER]
|
||||
|
||||
async_subscribe_events(
|
||||
hass, subscriptions, _queue_or_cancel, event_types, entity_ids, device_ids
|
||||
hass,
|
||||
subscriptions,
|
||||
_queue_or_cancel,
|
||||
event_types,
|
||||
entities_filter,
|
||||
entity_ids,
|
||||
device_ids,
|
||||
)
|
||||
subscriptions_setup_complete_time = dt_util.utcnow()
|
||||
connection.subscriptions[msg_id] = _unsub
|
||||
@@ -356,7 +396,7 @@ async def ws_event_stream(
|
||||
)
|
||||
|
||||
await _async_wait_for_recorder_sync(hass)
|
||||
if not subscriptions:
|
||||
if msg_id not in connection.subscriptions:
|
||||
# Unsubscribe happened while waiting for recorder
|
||||
return
|
||||
|
||||
@@ -388,6 +428,8 @@ async def ws_event_stream(
|
||||
|
||||
if not subscriptions:
|
||||
# Unsubscribe happened while waiting for formatted events
|
||||
# or there are no supported entities (all UOM or state class)
|
||||
# or devices
|
||||
return
|
||||
|
||||
live_stream.task = asyncio.create_task(
|
||||
@@ -475,7 +517,7 @@ async def ws_get_events(
|
||||
)
|
||||
|
||||
connection.send_message(
|
||||
await hass.async_add_executor_job(
|
||||
await get_instance(hass).async_add_executor_job(
|
||||
_ws_formatted_get_events,
|
||||
msg["id"],
|
||||
start_time,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "LOOKin",
|
||||
"documentation": "https://www.home-assistant.io/integrations/lookin/",
|
||||
"codeowners": ["@ANMalko", "@bdraco"],
|
||||
"requirements": ["aiolookin==0.1.0"],
|
||||
"requirements": ["aiolookin==0.1.1"],
|
||||
"zeroconf": ["_lookin._tcp.local."],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -110,7 +110,7 @@ def _state_schema(state):
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(
|
||||
vol.All(
|
||||
mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||
mqtt.config.MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "manual_mqtt",
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
|
||||
@@ -76,6 +76,7 @@ from .const import ( # noqa: F401
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_ALBUM_ARTIST,
|
||||
ATTR_MEDIA_ALBUM_NAME,
|
||||
ATTR_MEDIA_ANNOUNCE,
|
||||
ATTR_MEDIA_ARTIST,
|
||||
ATTR_MEDIA_CHANNEL,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
@@ -147,6 +148,19 @@ ENTITY_IMAGE_CACHE = {CACHE_IMAGES: collections.OrderedDict(), CACHE_MAXSIZE: 16
|
||||
SCAN_INTERVAL = dt.timedelta(seconds=10)
|
||||
|
||||
|
||||
class MediaPlayerEnqueue(StrEnum):
|
||||
"""Enqueue types for playing media."""
|
||||
|
||||
# add given media item to end of the queue
|
||||
ADD = "add"
|
||||
# play the given media item next, keep queue
|
||||
NEXT = "next"
|
||||
# play the given media item now, keep queue
|
||||
PLAY = "play"
|
||||
# play the given media item now, clear queue
|
||||
REPLACE = "replace"
|
||||
|
||||
|
||||
class MediaPlayerDeviceClass(StrEnum):
|
||||
"""Device class for media players."""
|
||||
|
||||
@@ -169,7 +183,10 @@ DEVICE_CLASS_RECEIVER = MediaPlayerDeviceClass.RECEIVER.value
|
||||
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = {
|
||||
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
|
||||
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
|
||||
vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean,
|
||||
vol.Exclusive(ATTR_MEDIA_ENQUEUE, "enqueue_announce"): vol.Any(
|
||||
cv.boolean, vol.Coerce(MediaPlayerEnqueue)
|
||||
),
|
||||
vol.Exclusive(ATTR_MEDIA_ANNOUNCE, "enqueue_announce"): cv.boolean,
|
||||
vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict,
|
||||
}
|
||||
|
||||
@@ -350,10 +367,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"async_select_sound_mode",
|
||||
[MediaPlayerEntityFeature.SELECT_SOUND_MODE],
|
||||
)
|
||||
|
||||
# Remove in Home Assistant 2022.9
|
||||
def _rewrite_enqueue(value):
|
||||
"""Rewrite the enqueue value."""
|
||||
if ATTR_MEDIA_ENQUEUE not in value:
|
||||
pass
|
||||
elif value[ATTR_MEDIA_ENQUEUE] is True:
|
||||
value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.ADD
|
||||
_LOGGER.warning(
|
||||
"Playing media with enqueue set to True is deprecated. Use 'add' instead"
|
||||
)
|
||||
elif value[ATTR_MEDIA_ENQUEUE] is False:
|
||||
value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.PLAY
|
||||
_LOGGER.warning(
|
||||
"Playing media with enqueue set to False is deprecated. Use 'play' instead"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_PLAY_MEDIA,
|
||||
vol.All(
|
||||
cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA),
|
||||
_rewrite_enqueue,
|
||||
_rename_keys(
|
||||
media_type=ATTR_MEDIA_CONTENT_TYPE,
|
||||
media_id=ATTR_MEDIA_CONTENT_ID,
|
||||
|
||||
@@ -10,6 +10,7 @@ ATTR_ENTITY_PICTURE_LOCAL = "entity_picture_local"
|
||||
ATTR_GROUP_MEMBERS = "group_members"
|
||||
ATTR_INPUT_SOURCE = "source"
|
||||
ATTR_INPUT_SOURCE_LIST = "source_list"
|
||||
ATTR_MEDIA_ANNOUNCE = "announce"
|
||||
ATTR_MEDIA_ALBUM_ARTIST = "media_album_artist"
|
||||
ATTR_MEDIA_ALBUM_NAME = "media_album_name"
|
||||
ATTR_MEDIA_ARTIST = "media_artist"
|
||||
|
||||
@@ -27,7 +27,6 @@ from .const import (
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
ATTR_SOUND_MODE,
|
||||
@@ -118,7 +117,7 @@ async def _async_reproduce_states(
|
||||
if features & MediaPlayerEntityFeature.PLAY_MEDIA:
|
||||
await call_service(
|
||||
SERVICE_PLAY_MEDIA,
|
||||
[ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE],
|
||||
[ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID],
|
||||
)
|
||||
already_playing = True
|
||||
|
||||
|
||||
@@ -151,6 +151,29 @@ play_media:
|
||||
selector:
|
||||
text:
|
||||
|
||||
enqueue:
|
||||
name: Enqueue
|
||||
description: If the content should be played now or be added to the queue.
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: "Play now"
|
||||
value: "play"
|
||||
- label: "Play next"
|
||||
value: "next"
|
||||
- label: "Add to queue"
|
||||
value: "add"
|
||||
- label: "Play now and clear queue"
|
||||
value: "replace"
|
||||
announce:
|
||||
name: Announce
|
||||
description: If the media should be played as an announcement.
|
||||
required: false
|
||||
example: "true"
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
select_source:
|
||||
name: Select source
|
||||
description: Send the media player the command to change input source.
|
||||
|
||||
@@ -102,7 +102,8 @@ class MikrotikHubTracker(ScannerEntity):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the client."""
|
||||
return self.device.name
|
||||
# Stringify to ensure we return a string
|
||||
return str(self.device.name)
|
||||
|
||||
@property
|
||||
def hostname(self) -> str:
|
||||
|
||||
@@ -9,6 +9,7 @@ from homeassistant.components.cover import (
|
||||
ATTR_TILT_POSITION,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -64,6 +65,10 @@ TILT_DEVICE_MAP = {
|
||||
BlindType.VerticalBlindRight: CoverDeviceClass.BLIND,
|
||||
}
|
||||
|
||||
TILT_ONLY_DEVICE_MAP = {
|
||||
BlindType.WoodShutter: CoverDeviceClass.BLIND,
|
||||
}
|
||||
|
||||
TDBU_DEVICE_MAP = {
|
||||
BlindType.TopDownBottomUp: CoverDeviceClass.SHADE,
|
||||
}
|
||||
@@ -108,6 +113,16 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
elif blind.type in TILT_ONLY_DEVICE_MAP:
|
||||
entities.append(
|
||||
MotionTiltOnlyDevice(
|
||||
coordinator,
|
||||
blind,
|
||||
TILT_ONLY_DEVICE_MAP[blind.type],
|
||||
sw_version,
|
||||
)
|
||||
)
|
||||
|
||||
elif blind.type in TDBU_DEVICE_MAP:
|
||||
entities.append(
|
||||
MotionTDBUDevice(
|
||||
@@ -356,6 +371,49 @@ class MotionTiltDevice(MotionPositionDevice):
|
||||
await self.hass.async_add_executor_job(self._blind.Stop)
|
||||
|
||||
|
||||
class MotionTiltOnlyDevice(MotionTiltDevice):
|
||||
"""Representation of a Motion Blind Device."""
|
||||
|
||||
_restore_tilt = False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.STOP_TILT
|
||||
)
|
||||
|
||||
if self.current_cover_tilt_position is not None:
|
||||
supported_features |= CoverEntityFeature.SET_TILT_POSITION
|
||||
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed or not."""
|
||||
if self._blind.angle is None:
|
||||
return None
|
||||
return self._blind.angle == 0
|
||||
|
||||
async def async_set_absolute_position(self, **kwargs):
|
||||
"""Move the cover to a specific absolute position (see TDBU)."""
|
||||
angle = kwargs.get(ATTR_TILT_POSITION)
|
||||
if angle is not None:
|
||||
angle = angle * 180 / 100
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._blind.Set_angle,
|
||||
angle,
|
||||
)
|
||||
|
||||
|
||||
class MotionTDBUDevice(MotionPositionDevice):
|
||||
"""Representation of a Motion Top Down Bottom Up blind Device."""
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Motion Blinds",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"requirements": ["motionblinds==0.6.7"],
|
||||
"requirements": ["motionblinds==0.6.8"],
|
||||
"dependencies": ["network"],
|
||||
"dhcp": [
|
||||
{ "registered_devices": true },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
"""This platform enables the possibility to control a MQTT alarm."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
import re
|
||||
@@ -31,8 +30,8 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttCommandTemplate, MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
@@ -45,11 +44,13 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttCommandTemplate, MqttValueTemplate
|
||||
from .util import valid_publish_topic, valid_subscribe_topic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -85,7 +86,7 @@ DEFAULT_NAME = "MQTT Alarm"
|
||||
REMOTE_CODE = "REMOTE_CODE"
|
||||
REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT"
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
|
||||
@@ -94,7 +95,7 @@ PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
vol.Optional(
|
||||
CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE
|
||||
): cv.template,
|
||||
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
||||
@@ -107,8 +108,8 @@ PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_TRIGGER): cv.string,
|
||||
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
@@ -131,7 +132,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT alarm control panel configured under the alarm_control_panel key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, alarm.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
alarm.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -142,13 +147,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN)
|
||||
)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import functools
|
||||
import logging
|
||||
@@ -34,19 +33,20 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_RO_SCHEMA
|
||||
from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, PAYLOAD_NONE
|
||||
from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttAvailability,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttValueTemplate
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,7 +57,7 @@ DEFAULT_PAYLOAD_ON = "ON"
|
||||
DEFAULT_FORCE_UPDATE = False
|
||||
CONF_EXPIRE_AFTER = "expire_after"
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
||||
@@ -87,7 +87,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT binary sensor configured under the fan platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, binary_sensor.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
binary_sensor.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -98,12 +102,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
# setup for discovery
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT buttons."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -15,8 +14,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttCommandTemplate
|
||||
from .. import mqtt
|
||||
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
@@ -27,24 +25,26 @@ from .const import (
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttCommandTemplate
|
||||
from .util import valid_publish_topic
|
||||
|
||||
CONF_PAYLOAD_PRESS = "payload_press"
|
||||
DEFAULT_NAME = "MQTT Button"
|
||||
DEFAULT_PAYLOAD_PRESS = "PRESS"
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string,
|
||||
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
@@ -67,7 +67,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT button configured under the fan platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, button.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
button.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -78,12 +82,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT button through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
# setup for discovery
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Camera that loads a picture from an MQTT topic."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from base64 import b64decode
|
||||
import functools
|
||||
|
||||
@@ -17,17 +16,18 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import subscription
|
||||
from .. import mqtt
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC
|
||||
from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .util import valid_subscribe_topic
|
||||
|
||||
DEFAULT_NAME = "MQTT Camera"
|
||||
|
||||
@@ -40,10 +40,10 @@ MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Required(CONF_TOPIC): valid_subscribe_topic,
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
@@ -65,7 +65,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT camera configured under the camera platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, camera.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
camera.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -76,12 +80,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
# setup for discovery
|
||||
|
||||
659
homeassistant/components/mqtt/client.py
Normal file
659
homeassistant/components/mqtt/client.py
Normal file
@@ -0,0 +1,659 @@
|
||||
"""Support for MQTT message handling."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from functools import lru_cache, partial, wraps
|
||||
import inspect
|
||||
from itertools import groupby
|
||||
import logging
|
||||
from operator import attrgetter
|
||||
import ssl
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Union, cast
|
||||
import uuid
|
||||
|
||||
import attr
|
||||
import certifi
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_PROTOCOL,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
)
|
||||
from homeassistant.core import CoreState, HassJob, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
from homeassistant.util.logging import catch_log_exception
|
||||
|
||||
from .const import (
|
||||
ATTR_TOPIC,
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_BROKER,
|
||||
CONF_CERTIFICATE,
|
||||
CONF_CLIENT_CERT,
|
||||
CONF_CLIENT_KEY,
|
||||
CONF_KEEPALIVE,
|
||||
CONF_TLS_INSECURE,
|
||||
CONF_WILL_MESSAGE,
|
||||
DATA_MQTT,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_QOS,
|
||||
MQTT_CONNECTED,
|
||||
MQTT_DISCONNECTED,
|
||||
PROTOCOL_31,
|
||||
)
|
||||
from .discovery import LAST_DISCOVERY
|
||||
from .models import (
|
||||
AsyncMessageCallbackType,
|
||||
MessageCallbackType,
|
||||
PublishMessage,
|
||||
PublishPayloadType,
|
||||
ReceiveMessage,
|
||||
ReceivePayloadType,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Only import for paho-mqtt type checking here, imports are done locally
|
||||
# because integrations should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DISCOVERY_COOLDOWN = 2
|
||||
TIMEOUT_ACK = 10
|
||||
|
||||
SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None
|
||||
|
||||
|
||||
def publish(
|
||||
hass: HomeAssistant,
|
||||
topic: str,
|
||||
payload: PublishPayloadType,
|
||||
qos: int | None = 0,
|
||||
retain: bool | None = False,
|
||||
encoding: str | None = DEFAULT_ENCODING,
|
||||
) -> None:
|
||||
"""Publish message to a MQTT topic."""
|
||||
hass.add_job(async_publish, hass, topic, payload, qos, retain, encoding)
|
||||
|
||||
|
||||
async def async_publish(
|
||||
hass: HomeAssistant,
|
||||
topic: str,
|
||||
payload: PublishPayloadType,
|
||||
qos: int | None = 0,
|
||||
retain: bool | None = False,
|
||||
encoding: str | None = DEFAULT_ENCODING,
|
||||
) -> None:
|
||||
"""Publish message to a MQTT topic."""
|
||||
|
||||
outgoing_payload = payload
|
||||
if not isinstance(payload, bytes):
|
||||
if not encoding:
|
||||
_LOGGER.error(
|
||||
"Can't pass-through payload for publishing %s on %s with no encoding set, need 'bytes' got %s",
|
||||
payload,
|
||||
topic,
|
||||
type(payload),
|
||||
)
|
||||
return
|
||||
outgoing_payload = str(payload)
|
||||
if encoding != DEFAULT_ENCODING:
|
||||
# a string is encoded as utf-8 by default, other encoding requires bytes as payload
|
||||
try:
|
||||
outgoing_payload = outgoing_payload.encode(encoding)
|
||||
except (AttributeError, LookupError, UnicodeEncodeError):
|
||||
_LOGGER.error(
|
||||
"Can't encode payload for publishing %s on %s with encoding %s",
|
||||
payload,
|
||||
topic,
|
||||
encoding,
|
||||
)
|
||||
return
|
||||
|
||||
await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain)
|
||||
|
||||
|
||||
AsyncDeprecatedMessageCallbackType = Callable[
|
||||
[str, ReceivePayloadType, int], Awaitable[None]
|
||||
]
|
||||
DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None]
|
||||
|
||||
|
||||
def wrap_msg_callback(
|
||||
msg_callback: AsyncDeprecatedMessageCallbackType | DeprecatedMessageCallbackType,
|
||||
) -> AsyncMessageCallbackType | MessageCallbackType:
|
||||
"""Wrap an MQTT message callback to support deprecated signature."""
|
||||
# Check for partials to properly determine if coroutine function
|
||||
check_func = msg_callback
|
||||
while isinstance(check_func, partial):
|
||||
check_func = check_func.func
|
||||
|
||||
wrapper_func: AsyncMessageCallbackType | MessageCallbackType
|
||||
if asyncio.iscoroutinefunction(check_func):
|
||||
|
||||
@wraps(msg_callback)
|
||||
async def async_wrapper(msg: ReceiveMessage) -> None:
|
||||
"""Call with deprecated signature."""
|
||||
await cast(AsyncDeprecatedMessageCallbackType, msg_callback)(
|
||||
msg.topic, msg.payload, msg.qos
|
||||
)
|
||||
|
||||
wrapper_func = async_wrapper
|
||||
else:
|
||||
|
||||
@wraps(msg_callback)
|
||||
def wrapper(msg: ReceiveMessage) -> None:
|
||||
"""Call with deprecated signature."""
|
||||
msg_callback(msg.topic, msg.payload, msg.qos)
|
||||
|
||||
wrapper_func = wrapper
|
||||
return wrapper_func
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_subscribe(
|
||||
hass: HomeAssistant,
|
||||
topic: str,
|
||||
msg_callback: AsyncMessageCallbackType
|
||||
| MessageCallbackType
|
||||
| DeprecatedMessageCallbackType
|
||||
| AsyncDeprecatedMessageCallbackType,
|
||||
qos: int = DEFAULT_QOS,
|
||||
encoding: str | None = "utf-8",
|
||||
):
|
||||
"""Subscribe to an MQTT topic.
|
||||
|
||||
Call the return value to unsubscribe.
|
||||
"""
|
||||
# Count callback parameters which don't have a default value
|
||||
non_default = 0
|
||||
if msg_callback:
|
||||
non_default = sum(
|
||||
p.default == inspect.Parameter.empty
|
||||
for _, p in inspect.signature(msg_callback).parameters.items()
|
||||
)
|
||||
|
||||
wrapped_msg_callback = msg_callback
|
||||
# If we have 3 parameters with no default value, wrap the callback
|
||||
if non_default == 3:
|
||||
module = inspect.getmodule(msg_callback)
|
||||
_LOGGER.warning(
|
||||
"Signature of MQTT msg_callback '%s.%s' is deprecated",
|
||||
module.__name__ if module else "<unknown>",
|
||||
msg_callback.__name__,
|
||||
)
|
||||
wrapped_msg_callback = wrap_msg_callback(
|
||||
cast(DeprecatedMessageCallbackType, msg_callback)
|
||||
)
|
||||
|
||||
async_remove = await hass.data[DATA_MQTT].async_subscribe(
|
||||
topic,
|
||||
catch_log_exception(
|
||||
wrapped_msg_callback,
|
||||
lambda msg: (
|
||||
f"Exception in {msg_callback.__name__} when handling msg on "
|
||||
f"'{msg.topic}': '{msg.payload}'"
|
||||
),
|
||||
),
|
||||
qos,
|
||||
encoding,
|
||||
)
|
||||
return async_remove
|
||||
|
||||
|
||||
@bind_hass
|
||||
def subscribe(
|
||||
hass: HomeAssistant,
|
||||
topic: str,
|
||||
msg_callback: MessageCallbackType,
|
||||
qos: int = DEFAULT_QOS,
|
||||
encoding: str = "utf-8",
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe to an MQTT topic."""
|
||||
async_remove = asyncio.run_coroutine_threadsafe(
|
||||
async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop
|
||||
).result()
|
||||
|
||||
def remove():
|
||||
"""Remove listener convert."""
|
||||
run_callback_threadsafe(hass.loop, async_remove).result()
|
||||
|
||||
return remove
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class Subscription:
|
||||
"""Class to hold data about an active subscription."""
|
||||
|
||||
topic: str = attr.ib()
|
||||
matcher: Any = attr.ib()
|
||||
job: HassJob = attr.ib()
|
||||
qos: int = attr.ib(default=0)
|
||||
encoding: str | None = attr.ib(default="utf-8")
|
||||
|
||||
|
||||
class MqttClientSetup:
|
||||
"""Helper class to setup the paho mqtt client from config."""
|
||||
|
||||
def __init__(self, config: ConfigType) -> None:
|
||||
"""Initialize the MQTT client setup helper."""
|
||||
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
|
||||
if config[CONF_PROTOCOL] == PROTOCOL_31:
|
||||
proto = mqtt.MQTTv31
|
||||
else:
|
||||
proto = mqtt.MQTTv311
|
||||
|
||||
if (client_id := config.get(CONF_CLIENT_ID)) is None:
|
||||
# PAHO MQTT relies on the MQTT server to generate random client IDs.
|
||||
# However, that feature is not mandatory so we generate our own.
|
||||
client_id = mqtt.base62(uuid.uuid4().int, padding=22)
|
||||
self._client = mqtt.Client(client_id, protocol=proto)
|
||||
|
||||
# Enable logging
|
||||
self._client.enable_logger()
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
if username is not None:
|
||||
self._client.username_pw_set(username, password)
|
||||
|
||||
if (certificate := config.get(CONF_CERTIFICATE)) == "auto":
|
||||
certificate = certifi.where()
|
||||
|
||||
client_key = config.get(CONF_CLIENT_KEY)
|
||||
client_cert = config.get(CONF_CLIENT_CERT)
|
||||
tls_insecure = config.get(CONF_TLS_INSECURE)
|
||||
if certificate is not None:
|
||||
self._client.tls_set(
|
||||
certificate,
|
||||
certfile=client_cert,
|
||||
keyfile=client_key,
|
||||
tls_version=ssl.PROTOCOL_TLS,
|
||||
)
|
||||
|
||||
if tls_insecure is not None:
|
||||
self._client.tls_insecure_set(tls_insecure)
|
||||
|
||||
@property
|
||||
def client(self) -> mqtt.Client:
|
||||
"""Return the paho MQTT client."""
|
||||
return self._client
|
||||
|
||||
|
||||
class MQTT:
|
||||
"""Home Assistant MQTT client."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry,
|
||||
conf,
|
||||
) -> None:
|
||||
"""Initialize Home Assistant MQTT client."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.conf = conf
|
||||
self.subscriptions: list[Subscription] = []
|
||||
self.connected = False
|
||||
self._ha_started = asyncio.Event()
|
||||
self._last_subscribe = time.time()
|
||||
self._mqttc: mqtt.Client = None
|
||||
self._paho_lock = asyncio.Lock()
|
||||
|
||||
self._pending_operations: dict[str, asyncio.Event] = {}
|
||||
|
||||
if self.hass.state == CoreState.running:
|
||||
self._ha_started.set()
|
||||
else:
|
||||
|
||||
@callback
|
||||
def ha_started(_):
|
||||
self._ha_started.set()
|
||||
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started)
|
||||
|
||||
self.init_client()
|
||||
|
||||
def init_client(self):
|
||||
"""Initialize paho client."""
|
||||
self._mqttc = MqttClientSetup(self.conf).client
|
||||
self._mqttc.on_connect = self._mqtt_on_connect
|
||||
self._mqttc.on_disconnect = self._mqtt_on_disconnect
|
||||
self._mqttc.on_message = self._mqtt_on_message
|
||||
self._mqttc.on_publish = self._mqtt_on_callback
|
||||
self._mqttc.on_subscribe = self._mqtt_on_callback
|
||||
self._mqttc.on_unsubscribe = self._mqtt_on_callback
|
||||
|
||||
if (
|
||||
CONF_WILL_MESSAGE in self.conf
|
||||
and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE]
|
||||
):
|
||||
will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE])
|
||||
else:
|
||||
will_message = None
|
||||
|
||||
if will_message is not None:
|
||||
self._mqttc.will_set(
|
||||
topic=will_message.topic,
|
||||
payload=will_message.payload,
|
||||
qos=will_message.qos,
|
||||
retain=will_message.retain,
|
||||
)
|
||||
|
||||
async def async_publish(
|
||||
self, topic: str, payload: PublishPayloadType, qos: int, retain: bool
|
||||
) -> None:
|
||||
"""Publish a MQTT message."""
|
||||
async with self._paho_lock:
|
||||
msg_info = await self.hass.async_add_executor_job(
|
||||
self._mqttc.publish, topic, payload, qos, retain
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Transmitting message on %s: '%s', mid: %s",
|
||||
topic,
|
||||
payload,
|
||||
msg_info.mid,
|
||||
)
|
||||
_raise_on_error(msg_info.rc)
|
||||
await self._wait_for_mid(msg_info.mid)
|
||||
|
||||
async def async_connect(self) -> None:
|
||||
"""Connect to the host. Does not process messages yet."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
result: int | None = None
|
||||
try:
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self._mqttc.connect,
|
||||
self.conf[CONF_BROKER],
|
||||
self.conf[CONF_PORT],
|
||||
self.conf[CONF_KEEPALIVE],
|
||||
)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Failed to connect to MQTT server due to exception: %s", err)
|
||||
|
||||
if result is not None and result != 0:
|
||||
_LOGGER.error(
|
||||
"Failed to connect to MQTT server: %s", mqtt.error_string(result)
|
||||
)
|
||||
|
||||
self._mqttc.loop_start()
|
||||
|
||||
async def async_disconnect(self):
|
||||
"""Stop the MQTT client."""
|
||||
|
||||
def stop():
|
||||
"""Stop the MQTT client."""
|
||||
# Do not disconnect, we want the broker to always publish will
|
||||
self._mqttc.loop_stop()
|
||||
|
||||
await self.hass.async_add_executor_job(stop)
|
||||
|
||||
async def async_subscribe(
|
||||
self,
|
||||
topic: str,
|
||||
msg_callback: MessageCallbackType,
|
||||
qos: int,
|
||||
encoding: str | None = None,
|
||||
) -> Callable[[], None]:
|
||||
"""Set up a subscription to a topic with the provided qos.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not isinstance(topic, str):
|
||||
raise HomeAssistantError("Topic needs to be a string!")
|
||||
|
||||
subscription = Subscription(
|
||||
topic, _matcher_for_topic(topic), HassJob(msg_callback), qos, encoding
|
||||
)
|
||||
self.subscriptions.append(subscription)
|
||||
self._matching_subscriptions.cache_clear()
|
||||
|
||||
# Only subscribe if currently connected.
|
||||
if self.connected:
|
||||
self._last_subscribe = time.time()
|
||||
await self._async_perform_subscription(topic, qos)
|
||||
|
||||
@callback
|
||||
def async_remove() -> None:
|
||||
"""Remove subscription."""
|
||||
if subscription not in self.subscriptions:
|
||||
raise HomeAssistantError("Can't remove subscription twice")
|
||||
self.subscriptions.remove(subscription)
|
||||
self._matching_subscriptions.cache_clear()
|
||||
|
||||
# Only unsubscribe if currently connected.
|
||||
if self.connected:
|
||||
self.hass.async_create_task(self._async_unsubscribe(topic))
|
||||
|
||||
return async_remove
|
||||
|
||||
async def _async_unsubscribe(self, topic: str) -> None:
|
||||
"""Unsubscribe from a topic.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if any(other.topic == topic for other in self.subscriptions):
|
||||
# Other subscriptions on topic remaining - don't unsubscribe.
|
||||
return
|
||||
|
||||
async with self._paho_lock:
|
||||
result: int | None = None
|
||||
result, mid = await self.hass.async_add_executor_job(
|
||||
self._mqttc.unsubscribe, topic
|
||||
)
|
||||
_LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid)
|
||||
_raise_on_error(result)
|
||||
await self._wait_for_mid(mid)
|
||||
|
||||
async def _async_perform_subscription(self, topic: str, qos: int) -> None:
|
||||
"""Perform a paho-mqtt subscription."""
|
||||
async with self._paho_lock:
|
||||
result: int | None = None
|
||||
result, mid = await self.hass.async_add_executor_job(
|
||||
self._mqttc.subscribe, topic, qos
|
||||
)
|
||||
_LOGGER.debug("Subscribing to %s, mid: %s", topic, mid)
|
||||
_raise_on_error(result)
|
||||
await self._wait_for_mid(mid)
|
||||
|
||||
def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None:
|
||||
"""On connect callback.
|
||||
|
||||
Resubscribe to all topics we were subscribed to and publish birth
|
||||
message.
|
||||
"""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
if result_code != mqtt.CONNACK_ACCEPTED:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to the MQTT broker: %s",
|
||||
mqtt.connack_string(result_code),
|
||||
)
|
||||
return
|
||||
|
||||
self.connected = True
|
||||
dispatcher_send(self.hass, MQTT_CONNECTED)
|
||||
_LOGGER.info(
|
||||
"Connected to MQTT server %s:%s (%s)",
|
||||
self.conf[CONF_BROKER],
|
||||
self.conf[CONF_PORT],
|
||||
result_code,
|
||||
)
|
||||
|
||||
# Group subscriptions to only re-subscribe once for each topic.
|
||||
keyfunc = attrgetter("topic")
|
||||
for topic, subs in groupby(sorted(self.subscriptions, key=keyfunc), keyfunc):
|
||||
# Re-subscribe with the highest requested qos
|
||||
max_qos = max(subscription.qos for subscription in subs)
|
||||
self.hass.add_job(self._async_perform_subscription, topic, max_qos)
|
||||
|
||||
if (
|
||||
CONF_BIRTH_MESSAGE in self.conf
|
||||
and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE]
|
||||
):
|
||||
|
||||
async def publish_birth_message(birth_message):
|
||||
await self._ha_started.wait() # Wait for Home Assistant to start
|
||||
await self._discovery_cooldown() # Wait for MQTT discovery to cool down
|
||||
await self.async_publish(
|
||||
topic=birth_message.topic,
|
||||
payload=birth_message.payload,
|
||||
qos=birth_message.qos,
|
||||
retain=birth_message.retain,
|
||||
)
|
||||
|
||||
birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE])
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
publish_birth_message(birth_message), self.hass.loop
|
||||
)
|
||||
|
||||
def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None:
|
||||
"""Message received callback."""
|
||||
self.hass.add_job(self._mqtt_handle_message, msg)
|
||||
|
||||
@lru_cache(2048)
|
||||
def _matching_subscriptions(self, topic):
|
||||
subscriptions = []
|
||||
for subscription in self.subscriptions:
|
||||
if subscription.matcher(topic):
|
||||
subscriptions.append(subscription)
|
||||
return subscriptions
|
||||
|
||||
@callback
|
||||
def _mqtt_handle_message(self, msg) -> None:
|
||||
_LOGGER.debug(
|
||||
"Received message on %s%s: %s",
|
||||
msg.topic,
|
||||
" (retained)" if msg.retain else "",
|
||||
msg.payload[0:8192],
|
||||
)
|
||||
timestamp = dt_util.utcnow()
|
||||
|
||||
subscriptions = self._matching_subscriptions(msg.topic)
|
||||
|
||||
for subscription in subscriptions:
|
||||
|
||||
payload: SubscribePayloadType = msg.payload
|
||||
if subscription.encoding is not None:
|
||||
try:
|
||||
payload = msg.payload.decode(subscription.encoding)
|
||||
except (AttributeError, UnicodeDecodeError):
|
||||
_LOGGER.warning(
|
||||
"Can't decode payload %s on %s with encoding %s (for %s)",
|
||||
msg.payload[0:8192],
|
||||
msg.topic,
|
||||
subscription.encoding,
|
||||
subscription.job,
|
||||
)
|
||||
continue
|
||||
|
||||
self.hass.async_run_hass_job(
|
||||
subscription.job,
|
||||
ReceiveMessage(
|
||||
msg.topic,
|
||||
payload,
|
||||
msg.qos,
|
||||
msg.retain,
|
||||
subscription.topic,
|
||||
timestamp,
|
||||
),
|
||||
)
|
||||
|
||||
def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None:
|
||||
"""Publish / Subscribe / Unsubscribe callback."""
|
||||
self.hass.add_job(self._mqtt_handle_mid, mid)
|
||||
|
||||
@callback
|
||||
def _mqtt_handle_mid(self, mid) -> None:
|
||||
# Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid
|
||||
# may be executed first.
|
||||
if mid not in self._pending_operations:
|
||||
self._pending_operations[mid] = asyncio.Event()
|
||||
self._pending_operations[mid].set()
|
||||
|
||||
def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None:
|
||||
"""Disconnected callback."""
|
||||
self.connected = False
|
||||
dispatcher_send(self.hass, MQTT_DISCONNECTED)
|
||||
_LOGGER.warning(
|
||||
"Disconnected from MQTT server %s:%s (%s)",
|
||||
self.conf[CONF_BROKER],
|
||||
self.conf[CONF_PORT],
|
||||
result_code,
|
||||
)
|
||||
|
||||
async def _wait_for_mid(self, mid):
|
||||
"""Wait for ACK from broker."""
|
||||
# Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid
|
||||
# may be executed first.
|
||||
if mid not in self._pending_operations:
|
||||
self._pending_operations[mid] = asyncio.Event()
|
||||
try:
|
||||
await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid
|
||||
)
|
||||
finally:
|
||||
del self._pending_operations[mid]
|
||||
|
||||
async def _discovery_cooldown(self):
|
||||
now = time.time()
|
||||
# Reset discovery and subscribe cooldowns
|
||||
self.hass.data[LAST_DISCOVERY] = now
|
||||
self._last_subscribe = now
|
||||
|
||||
last_discovery = self.hass.data[LAST_DISCOVERY]
|
||||
last_subscribe = self._last_subscribe
|
||||
wait_until = max(
|
||||
last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN
|
||||
)
|
||||
while now < wait_until:
|
||||
await asyncio.sleep(wait_until - now)
|
||||
now = time.time()
|
||||
last_discovery = self.hass.data[LAST_DISCOVERY]
|
||||
last_subscribe = self._last_subscribe
|
||||
wait_until = max(
|
||||
last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN
|
||||
)
|
||||
|
||||
|
||||
def _raise_on_error(result_code: int | None) -> None:
|
||||
"""Raise error if error result."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
if result_code is not None and result_code != 0:
|
||||
raise HomeAssistantError(
|
||||
f"Error talking to MQTT: {mqtt.error_string(result_code)}"
|
||||
)
|
||||
|
||||
|
||||
def _matcher_for_topic(subscription: str) -> Any:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from paho.mqtt.matcher import MQTTMatcher
|
||||
|
||||
matcher = MQTTMatcher()
|
||||
matcher[subscription] = True
|
||||
|
||||
return lambda topic: next(matcher.iter_match(topic), False)
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT climate devices."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
|
||||
@@ -44,18 +43,20 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttCommandTemplate, MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
|
||||
from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, PAYLOAD_NONE
|
||||
from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttCommandTemplate, MqttValueTemplate
|
||||
from .util import valid_publish_topic, valid_subscribe_topic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -232,33 +233,33 @@ def valid_preset_mode_configuration(config):
|
||||
return config
|
||||
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic,
|
||||
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
|
||||
vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(
|
||||
CONF_FAN_MODE_LIST,
|
||||
default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH],
|
||||
): cv.ensure_list,
|
||||
vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic,
|
||||
# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9
|
||||
vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_HOLD_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_HOLD_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_HOLD_LIST): cv.ensure_list,
|
||||
vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(
|
||||
CONF_MODE_LIST,
|
||||
default=[
|
||||
@@ -271,54 +272,54 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
],
|
||||
): cv.ensure_list,
|
||||
vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
|
||||
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_POWER_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_PRECISION): vol.In(
|
||||
[PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
|
||||
),
|
||||
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9
|
||||
vol.Optional(CONF_SEND_IF_OFF): cv.boolean,
|
||||
vol.Optional(CONF_ACTION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_ACTION_TOPIC): valid_subscribe_topic,
|
||||
# CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together
|
||||
vol.Inclusive(
|
||||
CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes"
|
||||
): mqtt.valid_publish_topic,
|
||||
): valid_publish_topic,
|
||||
vol.Inclusive(
|
||||
CONF_PRESET_MODES_LIST, "preset_modes", default=[]
|
||||
): cv.ensure_list,
|
||||
vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(
|
||||
CONF_SWING_MODE_LIST, default=[SWING_ON, SWING_OFF]
|
||||
): cv.ensure_list,
|
||||
vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_SWING_MODE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int,
|
||||
vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float),
|
||||
vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TEMP_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMP_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
}
|
||||
@@ -375,7 +376,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT climate configured under the fan platform key (deprecated)."""
|
||||
# The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, climate.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
climate.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -386,12 +391,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
# setup for discovery
|
||||
|
||||
148
homeassistant/components/mqtt/config.py
Normal file
148
homeassistant/components/mqtt/config.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Support for MQTT message handling."""
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
CONF_DISCOVERY,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_PROTOCOL,
|
||||
CONF_USERNAME,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
ATTR_PAYLOAD,
|
||||
ATTR_QOS,
|
||||
ATTR_RETAIN,
|
||||
ATTR_TOPIC,
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_BROKER,
|
||||
CONF_CERTIFICATE,
|
||||
CONF_CLIENT_CERT,
|
||||
CONF_CLIENT_KEY,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_DISCOVERY_PREFIX,
|
||||
CONF_ENCODING,
|
||||
CONF_KEEPALIVE,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_TLS_INSECURE,
|
||||
CONF_TLS_VERSION,
|
||||
CONF_WILL_MESSAGE,
|
||||
DEFAULT_BIRTH,
|
||||
DEFAULT_DISCOVERY,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_PREFIX,
|
||||
DEFAULT_QOS,
|
||||
DEFAULT_RETAIN,
|
||||
DEFAULT_WILL,
|
||||
PLATFORMS,
|
||||
PROTOCOL_31,
|
||||
PROTOCOL_311,
|
||||
)
|
||||
from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic
|
||||
|
||||
DEFAULT_PORT = 1883
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_PROTOCOL = PROTOCOL_311
|
||||
DEFAULT_TLS_PROTOCOL = "auto"
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
CONF_BIRTH_MESSAGE: DEFAULT_BIRTH,
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
CONF_PORT: DEFAULT_PORT,
|
||||
CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL,
|
||||
CONF_WILL_MESSAGE: DEFAULT_WILL,
|
||||
}
|
||||
|
||||
CLIENT_KEY_AUTH_MSG = (
|
||||
"client_key and client_cert must both be present in "
|
||||
"the MQTT broker configuration"
|
||||
)
|
||||
|
||||
MQTT_WILL_BIRTH_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic,
|
||||
vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string,
|
||||
vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
|
||||
vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
},
|
||||
required=True,
|
||||
)
|
||||
|
||||
PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema(
|
||||
{vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend(
|
||||
{
|
||||
vol.Optional(CONF_CLIENT_ID): cv.string,
|
||||
vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=15)
|
||||
),
|
||||
vol.Optional(CONF_BROKER): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile),
|
||||
vol.Inclusive(
|
||||
CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG
|
||||
): cv.isfile,
|
||||
vol.Inclusive(
|
||||
CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG
|
||||
): cv.isfile,
|
||||
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
|
||||
vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"),
|
||||
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(
|
||||
cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])
|
||||
),
|
||||
vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
|
||||
vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
|
||||
vol.Optional(CONF_DISCOVERY): cv.boolean,
|
||||
# discovery_prefix must be a valid publish topic because if no
|
||||
# state topic is specified, it will be created with the given prefix.
|
||||
vol.Optional(
|
||||
CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX
|
||||
): valid_publish_topic,
|
||||
}
|
||||
)
|
||||
|
||||
DEPRECATED_CONFIG_KEYS = [
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_BROKER,
|
||||
CONF_DISCOVERY,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_TLS_VERSION,
|
||||
CONF_USERNAME,
|
||||
CONF_WILL_MESSAGE,
|
||||
]
|
||||
|
||||
SCHEMA_BASE = {
|
||||
vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
|
||||
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
|
||||
}
|
||||
|
||||
MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE)
|
||||
|
||||
# Sensor type platforms subscribe to MQTT events
|
||||
MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
}
|
||||
)
|
||||
|
||||
# Switch type platforms publish to MQTT and may subscribe
|
||||
MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
}
|
||||
)
|
||||
@@ -17,7 +17,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from . import MqttClientSetup
|
||||
from .client import MqttClientSetup
|
||||
from .const import (
|
||||
ATTR_PAYLOAD,
|
||||
ATTR_QOS,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Constants used by multiple MQTT modules."""
|
||||
from homeassistant.const import CONF_PAYLOAD
|
||||
from homeassistant.const import CONF_PAYLOAD, Platform
|
||||
|
||||
ATTR_DISCOVERY_HASH = "discovery_hash"
|
||||
ATTR_DISCOVERY_PAYLOAD = "discovery_payload"
|
||||
@@ -14,7 +14,9 @@ CONF_BROKER = "broker"
|
||||
CONF_BIRTH_MESSAGE = "birth_message"
|
||||
CONF_COMMAND_TEMPLATE = "command_template"
|
||||
CONF_COMMAND_TOPIC = "command_topic"
|
||||
CONF_DISCOVERY_PREFIX = "discovery_prefix"
|
||||
CONF_ENCODING = "encoding"
|
||||
CONF_KEEPALIVE = "keepalive"
|
||||
CONF_QOS = ATTR_QOS
|
||||
CONF_RETAIN = ATTR_RETAIN
|
||||
CONF_STATE_TOPIC = "state_topic"
|
||||
@@ -30,6 +32,7 @@ CONF_TLS_VERSION = "tls_version"
|
||||
|
||||
CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup"
|
||||
DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock"
|
||||
DATA_MQTT = "mqtt"
|
||||
DATA_MQTT_CONFIG = "mqtt_config"
|
||||
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"
|
||||
|
||||
@@ -66,3 +69,24 @@ PAYLOAD_NONE = "None"
|
||||
|
||||
PROTOCOL_31 = "3.1"
|
||||
PROTOCOL_311 = "3.1.1"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CAMERA,
|
||||
Platform.CLIMATE,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.COVER,
|
||||
Platform.FAN,
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SCENE,
|
||||
Platform.SENSOR,
|
||||
Platform.SIREN,
|
||||
Platform.SWITCH,
|
||||
Platform.VACUUM,
|
||||
]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT cover devices."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
from json import JSONDecodeError, loads as json_loads
|
||||
import logging
|
||||
@@ -33,8 +32,8 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttCommandTemplate, MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
@@ -46,11 +45,13 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttCommandTemplate, MqttValueTemplate
|
||||
from .util import valid_publish_topic, valid_subscribe_topic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -152,11 +153,11 @@ def validate_options(value):
|
||||
return value
|
||||
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): vol.Any(
|
||||
@@ -172,24 +173,24 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_SET_POSITION_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_SET_POSITION_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
|
||||
vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string,
|
||||
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
|
||||
vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string,
|
||||
vol.Optional(CONF_STATE_STOPPED, default=DEFAULT_STATE_STOPPED): cv.string,
|
||||
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(
|
||||
CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION
|
||||
): int,
|
||||
vol.Optional(CONF_TILT_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TILT_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int,
|
||||
vol.Optional(CONF_TILT_MIN, default=DEFAULT_TILT_MIN): int,
|
||||
vol.Optional(CONF_TILT_OPEN_POSITION, default=DEFAULT_TILT_OPEN_POSITION): int,
|
||||
vol.Optional(
|
||||
CONF_TILT_STATE_OPTIMISTIC, default=DEFAULT_TILT_OPTIMISTIC
|
||||
): cv.boolean,
|
||||
vol.Optional(CONF_TILT_STATUS_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_TILT_STATUS_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_TILT_STATUS_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template,
|
||||
@@ -225,7 +226,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT covers configured under the fan platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, cover.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
cover.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -236,13 +241,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN)
|
||||
)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
|
||||
@@ -3,8 +3,10 @@ import functools
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import device_trigger
|
||||
from .. import mqtt
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .mixins import async_setup_entry_helper
|
||||
|
||||
AUTOMATION_TYPE_TRIGGER = "trigger"
|
||||
@@ -12,10 +14,10 @@ AUTOMATION_TYPES = [AUTOMATION_TYPE_TRIGGER]
|
||||
AUTOMATION_TYPES_SCHEMA = vol.In(AUTOMATION_TYPES)
|
||||
CONF_AUTOMATION_TYPE = "automation_type"
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
).extend(MQTT_BASE_SCHEMA.schema)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
|
||||
@@ -19,8 +19,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .. import MqttValueTemplate, subscription
|
||||
from ... import mqtt
|
||||
from .. import subscription
|
||||
from ..config import MQTT_RO_SCHEMA
|
||||
from ..const import CONF_QOS, CONF_STATE_TOPIC
|
||||
from ..debug_info import log_messages
|
||||
from ..mixins import (
|
||||
@@ -29,12 +29,13 @@ from ..mixins import (
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
)
|
||||
from ..models import MqttValueTemplate
|
||||
|
||||
CONF_PAYLOAD_HOME = "payload_home"
|
||||
CONF_PAYLOAD_NOT_HOME = "payload_not_home"
|
||||
CONF_SOURCE_TYPE = "source_type"
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string,
|
||||
|
||||
@@ -7,16 +7,18 @@ from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from ... import mqtt
|
||||
from ..client import async_subscribe
|
||||
from ..config import SCHEMA_BASE
|
||||
from ..const import CONF_QOS
|
||||
from ..util import valid_subscribe_topic
|
||||
|
||||
CONF_PAYLOAD_HOME = "payload_home"
|
||||
CONF_PAYLOAD_NOT_HOME = "payload_not_home"
|
||||
CONF_SOURCE_TYPE = "source_type"
|
||||
|
||||
PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend(
|
||||
PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend(
|
||||
{
|
||||
vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic},
|
||||
vol.Required(CONF_DEVICES): {cv.string: valid_subscribe_topic},
|
||||
vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string,
|
||||
vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES),
|
||||
@@ -50,6 +52,6 @@ async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info=
|
||||
|
||||
hass.async_create_task(async_see(**see_args))
|
||||
|
||||
await mqtt.async_subscribe(hass, topic, async_message_received, qos)
|
||||
await async_subscribe(hass, topic, async_message_received, qos)
|
||||
|
||||
return True
|
||||
|
||||
@@ -29,8 +29,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import debug_info, trigger as mqtt_trigger
|
||||
from .. import mqtt
|
||||
from .const import ATTR_DISCOVERY_HASH, CONF_PAYLOAD, CONF_QOS, CONF_TOPIC, DOMAIN
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
ATTR_DISCOVERY_HASH,
|
||||
CONF_ENCODING,
|
||||
CONF_PAYLOAD,
|
||||
CONF_QOS,
|
||||
CONF_TOPIC,
|
||||
DOMAIN,
|
||||
)
|
||||
from .discovery import MQTT_DISCOVERY_DONE
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
@@ -64,7 +71,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
}
|
||||
)
|
||||
|
||||
TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||
TRIGGER_DISCOVERY_SCHEMA = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_AUTOMATION_TYPE): str,
|
||||
vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
@@ -94,10 +101,10 @@ class TriggerInstance:
|
||||
async def async_attach_trigger(self) -> None:
|
||||
"""Attach MQTT trigger."""
|
||||
mqtt_config = {
|
||||
mqtt_trigger.CONF_PLATFORM: mqtt.DOMAIN,
|
||||
mqtt_trigger.CONF_TOPIC: self.trigger.topic,
|
||||
mqtt_trigger.CONF_ENCODING: DEFAULT_ENCODING,
|
||||
mqtt_trigger.CONF_QOS: self.trigger.qos,
|
||||
CONF_PLATFORM: DOMAIN,
|
||||
CONF_TOPIC: self.trigger.topic,
|
||||
CONF_ENCODING: DEFAULT_ENCODING,
|
||||
CONF_QOS: self.trigger.qos,
|
||||
}
|
||||
if self.trigger.payload:
|
||||
mqtt_config[CONF_PAYLOAD] = self.trigger.payload
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT fans."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
import math
|
||||
@@ -34,8 +33,8 @@ from homeassistant.util.percentage import (
|
||||
ranged_value_to_percentage,
|
||||
)
|
||||
|
||||
from . import MqttCommandTemplate, MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
@@ -50,11 +49,13 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttCommandTemplate, MqttValueTemplate
|
||||
from .util import valid_publish_topic, valid_subscribe_topic
|
||||
|
||||
CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"
|
||||
CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"
|
||||
@@ -125,28 +126,28 @@ def valid_preset_mode_configuration(config):
|
||||
return config
|
||||
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_OSCILLATION_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_OSCILLATION_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_OSCILLATION_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template,
|
||||
# CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together
|
||||
vol.Inclusive(
|
||||
CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes"
|
||||
): mqtt.valid_publish_topic,
|
||||
): valid_publish_topic,
|
||||
vol.Inclusive(
|
||||
CONF_PRESET_MODES_LIST, "preset_modes", default=[]
|
||||
): cv.ensure_list,
|
||||
vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(
|
||||
CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN
|
||||
@@ -168,8 +169,8 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
vol.Optional(
|
||||
CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD
|
||||
): cv.string,
|
||||
vol.Optional(CONF_SPEED_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_SPEED_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_SPEED_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_SPEED_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
|
||||
}
|
||||
@@ -215,7 +216,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT fans configured under the fan platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, fan.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
fan.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -226,13 +231,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN)
|
||||
)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT humidifiers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
|
||||
@@ -30,8 +29,8 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttCommandTemplate, MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
@@ -46,11 +45,13 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttCommandTemplate, MqttValueTemplate
|
||||
from .util import valid_publish_topic, valid_subscribe_topic
|
||||
|
||||
CONF_AVAILABLE_MODES_LIST = "modes"
|
||||
CONF_DEVICE_CLASS = "device_class"
|
||||
@@ -103,15 +104,13 @@ def valid_humidity_range_configuration(config):
|
||||
return config
|
||||
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
# CONF_AVAIALABLE_MODES_LIST and CONF_MODE_COMMAND_TOPIC must be used together
|
||||
vol.Inclusive(
|
||||
CONF_AVAILABLE_MODES_LIST, "available_modes", default=[]
|
||||
): cv.ensure_list,
|
||||
vol.Inclusive(
|
||||
CONF_MODE_COMMAND_TOPIC, "available_modes"
|
||||
): mqtt.valid_publish_topic,
|
||||
vol.Inclusive(CONF_MODE_COMMAND_TOPIC, "available_modes"): valid_publish_topic,
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(
|
||||
CONF_DEVICE_CLASS, default=HumidifierDeviceClass.HUMIDIFIER
|
||||
@@ -119,14 +118,14 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
[HumidifierDeviceClass.HUMIDIFIER, HumidifierDeviceClass.DEHUMIDIFIER]
|
||||
),
|
||||
vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(
|
||||
CONF_TARGET_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY
|
||||
@@ -135,7 +134,7 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
CONF_TARGET_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY
|
||||
): cv.positive_int,
|
||||
vol.Optional(CONF_TARGET_HUMIDITY_STATE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(
|
||||
CONF_PAYLOAD_RESET_HUMIDITY, default=DEFAULT_PAYLOAD_RESET
|
||||
): cv.string,
|
||||
@@ -173,7 +172,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT humidifier configured under the fan platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, humidifier.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
humidifier.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -184,14 +187,12 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
) # setup for discovery
|
||||
)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT lights."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -14,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from ..mixins import (
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
@@ -97,7 +96,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT light through configuration.yaml (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, light.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
light.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -108,13 +111,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT lights configured under the light platform key (deprecated)."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN)
|
||||
)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
|
||||
@@ -42,8 +42,8 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .. import MqttCommandTemplate, MqttValueTemplate, subscription
|
||||
from ... import mqtt
|
||||
from .. import subscription
|
||||
from ..config import MQTT_RW_SCHEMA
|
||||
from ..const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
@@ -55,6 +55,8 @@ from ..const import (
|
||||
)
|
||||
from ..debug_info import log_messages
|
||||
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
|
||||
from ..models import MqttCommandTemplate, MqttValueTemplate
|
||||
from ..util import valid_publish_topic, valid_subscribe_topic
|
||||
from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -156,28 +158,28 @@ VALUE_TEMPLATE_KEYS = [
|
||||
]
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = (
|
||||
mqtt.MQTT_RW_SCHEMA.extend(
|
||||
MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_BRIGHTNESS_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(
|
||||
CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_COLOR_MODE_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_COLOR_TEMP_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_EFFECT_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_EFFECT_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_EFFECT_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_HS_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_HS_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_HS_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_HS_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_MAX_MIREDS): cv.positive_int,
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.positive_int,
|
||||
@@ -189,30 +191,30 @@ _PLATFORM_SCHEMA_BASE = (
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_RGB_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_RGB_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_RGB_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_RGBW_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_RGBW_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_RGBW_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_RGBW_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_RGBW_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_RGBW_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_RGBWW_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_RGBWW_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_RGBWW_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_RGBWW_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_WHITE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_WHITE_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(
|
||||
CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_XY_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_XY_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_XY_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_XY_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .. import subscription
|
||||
from ... import mqtt
|
||||
from ..config import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA
|
||||
from ..const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
@@ -61,6 +61,7 @@ from ..const import (
|
||||
)
|
||||
from ..debug_info import log_messages
|
||||
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
|
||||
from ..util import valid_subscribe_topic
|
||||
from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
|
||||
from .schema_basic import CONF_BRIGHTNESS_SCALE, MQTT_LIGHT_ATTRIBUTES_BLOCKED
|
||||
|
||||
@@ -103,7 +104,7 @@ def valid_color_configuration(config):
|
||||
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = (
|
||||
mqtt.MQTT_RW_SCHEMA.extend(
|
||||
MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean,
|
||||
vol.Optional(
|
||||
@@ -126,12 +127,12 @@ _PLATFORM_SCHEMA_BASE = (
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.positive_int,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All(
|
||||
vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All(
|
||||
vol.Coerce(int), vol.In([0, 1, 2])
|
||||
),
|
||||
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
|
||||
vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.In(VALID_COLOR_MODES)],
|
||||
|
||||
@@ -31,8 +31,8 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .. import MqttValueTemplate, subscription
|
||||
from ... import mqtt
|
||||
from .. import subscription
|
||||
from ..config import MQTT_RW_SCHEMA
|
||||
from ..const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
@@ -43,6 +43,7 @@ from ..const import (
|
||||
)
|
||||
from ..debug_info import log_messages
|
||||
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity
|
||||
from ..models import MqttValueTemplate
|
||||
from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
|
||||
from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED
|
||||
|
||||
@@ -67,7 +68,7 @@ CONF_RED_TEMPLATE = "red_template"
|
||||
CONF_WHITE_VALUE_TEMPLATE = "white_value_template"
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = (
|
||||
mqtt.MQTT_RW_SCHEMA.extend(
|
||||
MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_BLUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT locks."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -15,8 +14,8 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
@@ -28,11 +27,12 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttValueTemplate
|
||||
|
||||
CONF_PAYLOAD_LOCK = "payload_lock"
|
||||
CONF_PAYLOAD_UNLOCK = "payload_unlock"
|
||||
@@ -56,7 +56,7 @@ MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
@@ -87,7 +87,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT locks configured under the lock platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, lock.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
lock.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -98,13 +102,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN)
|
||||
)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import logging
|
||||
@@ -27,10 +28,11 @@ from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
discovery,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
|
||||
@@ -46,17 +48,14 @@ from homeassistant.helpers.entity import (
|
||||
async_generate_entity_id,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.reload import (
|
||||
async_integration_yaml_config,
|
||||
async_setup_reload_service,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import (
|
||||
DATA_MQTT,
|
||||
PLATFORMS,
|
||||
MqttValueTemplate,
|
||||
async_publish,
|
||||
debug_info,
|
||||
subscription,
|
||||
)
|
||||
from . import debug_info, subscription
|
||||
from .client import async_publish
|
||||
from .const import (
|
||||
ATTR_DISCOVERY_HASH,
|
||||
ATTR_DISCOVERY_PAYLOAD,
|
||||
@@ -65,6 +64,7 @@ from .const import (
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_TOPIC,
|
||||
DATA_MQTT,
|
||||
DATA_MQTT_CONFIG,
|
||||
DATA_MQTT_RELOAD_NEEDED,
|
||||
DEFAULT_ENCODING,
|
||||
@@ -73,6 +73,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
MQTT_CONNECTED,
|
||||
MQTT_DISCONNECTED,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .debug_info import log_message, log_messages
|
||||
from .discovery import (
|
||||
@@ -82,7 +83,7 @@ from .discovery import (
|
||||
clear_discovery_hash,
|
||||
set_discovery_hash,
|
||||
)
|
||||
from .models import PublishPayloadType, ReceiveMessage
|
||||
from .models import MqttValueTemplate, PublishPayloadType, ReceiveMessage
|
||||
from .subscription import (
|
||||
async_prepare_subscribe_topics,
|
||||
async_subscribe_topics,
|
||||
@@ -264,8 +265,44 @@ class SetupEntity(Protocol):
|
||||
"""Define setup_entities type."""
|
||||
|
||||
|
||||
async def async_setup_platform_discovery(
|
||||
hass: HomeAssistant, platform_domain: str, schema: vol.Schema
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Set up platform discovery for manual config."""
|
||||
|
||||
async def _async_discover_entities(event: Event | None) -> None:
|
||||
"""Discover entities for a platform."""
|
||||
if event:
|
||||
# The platform has been reloaded
|
||||
config_yaml = await async_integration_yaml_config(hass, DOMAIN)
|
||||
if not config_yaml:
|
||||
return
|
||||
config_yaml = config_yaml.get(DOMAIN, {})
|
||||
else:
|
||||
config_yaml = hass.data.get(DATA_MQTT_CONFIG, {})
|
||||
if not config_yaml:
|
||||
return
|
||||
if platform_domain not in config_yaml:
|
||||
return
|
||||
await asyncio.gather(
|
||||
*(
|
||||
discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {})
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, platform_domain, schema, config_yaml
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
unsub = hass.bus.async_listen("event_mqtt_reloaded", _async_discover_entities)
|
||||
await _async_discover_entities(None)
|
||||
return unsub
|
||||
|
||||
|
||||
async def async_get_platform_config_from_yaml(
|
||||
hass: HomeAssistant, domain: str, schema: vol.Schema
|
||||
hass: HomeAssistant,
|
||||
platform_domain: str,
|
||||
schema: vol.Schema,
|
||||
config_yaml: ConfigType = None,
|
||||
) -> list[ConfigType]:
|
||||
"""Return a list of validated configurations for the domain."""
|
||||
|
||||
@@ -279,12 +316,15 @@ async def async_get_platform_config_from_yaml(
|
||||
try:
|
||||
validated_config.append(schema(config_item))
|
||||
except vol.MultipleInvalid as err:
|
||||
async_log_exception(err, domain, config_item, hass)
|
||||
async_log_exception(err, platform_domain, config_item, hass)
|
||||
|
||||
return validated_config
|
||||
|
||||
config_yaml: ConfigType = hass.data.get(DATA_MQTT_CONFIG, {})
|
||||
if not (platform_configs := config_yaml.get(domain)):
|
||||
if config_yaml is None:
|
||||
config_yaml = hass.data.get(DATA_MQTT_CONFIG)
|
||||
if not config_yaml:
|
||||
return []
|
||||
if not (platform_configs := config_yaml.get(platform_domain)):
|
||||
return []
|
||||
return async_validate_config(hass, platform_configs)
|
||||
|
||||
@@ -314,7 +354,7 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema):
|
||||
async def async_setup_platform_helper(
|
||||
hass: HomeAssistant,
|
||||
platform_domain: str,
|
||||
config: ConfigType,
|
||||
config: ConfigType | DiscoveryInfoType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
async_setup_entities: SetupEntity,
|
||||
) -> None:
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
"""Models used by multiple MQTT modules."""
|
||||
from __future__ import annotations
|
||||
|
||||
from ast import literal_eval
|
||||
from collections.abc import Awaitable, Callable
|
||||
import datetime as dt
|
||||
from typing import Union
|
||||
from typing import Any, Union
|
||||
|
||||
import attr
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import TemplateVarsType
|
||||
|
||||
_SENTINEL = object()
|
||||
|
||||
PublishPayloadType = Union[str, bytes, int, float, None]
|
||||
ReceivePayloadType = Union[str, bytes]
|
||||
|
||||
@@ -35,3 +44,118 @@ class ReceiveMessage:
|
||||
|
||||
AsyncMessageCallbackType = Callable[[ReceiveMessage], Awaitable[None]]
|
||||
MessageCallbackType = Callable[[ReceiveMessage], None]
|
||||
|
||||
|
||||
class MqttCommandTemplate:
|
||||
"""Class for rendering MQTT payload with command templates."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command_template: template.Template | None,
|
||||
*,
|
||||
hass: HomeAssistant | None = None,
|
||||
entity: Entity | None = None,
|
||||
) -> None:
|
||||
"""Instantiate a command template."""
|
||||
self._attr_command_template = command_template
|
||||
if command_template is None:
|
||||
return
|
||||
|
||||
self._entity = entity
|
||||
|
||||
command_template.hass = hass
|
||||
|
||||
if entity:
|
||||
command_template.hass = entity.hass
|
||||
|
||||
@callback
|
||||
def async_render(
|
||||
self,
|
||||
value: PublishPayloadType = None,
|
||||
variables: TemplateVarsType = None,
|
||||
) -> PublishPayloadType:
|
||||
"""Render or convert the command template with given value or variables."""
|
||||
|
||||
def _convert_outgoing_payload(
|
||||
payload: PublishPayloadType,
|
||||
) -> PublishPayloadType:
|
||||
"""Ensure correct raw MQTT payload is passed as bytes for publishing."""
|
||||
if isinstance(payload, str):
|
||||
try:
|
||||
native_object = literal_eval(payload)
|
||||
if isinstance(native_object, bytes):
|
||||
return native_object
|
||||
|
||||
except (ValueError, TypeError, SyntaxError, MemoryError):
|
||||
pass
|
||||
|
||||
return payload
|
||||
|
||||
if self._attr_command_template is None:
|
||||
return value
|
||||
|
||||
values = {"value": value}
|
||||
if self._entity:
|
||||
values[ATTR_ENTITY_ID] = self._entity.entity_id
|
||||
values[ATTR_NAME] = self._entity.name
|
||||
if variables is not None:
|
||||
values.update(variables)
|
||||
return _convert_outgoing_payload(
|
||||
self._attr_command_template.async_render(values, parse_result=False)
|
||||
)
|
||||
|
||||
|
||||
class MqttValueTemplate:
|
||||
"""Class for rendering MQTT value template with possible json values."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
value_template: template.Template | None,
|
||||
*,
|
||||
hass: HomeAssistant | None = None,
|
||||
entity: Entity | None = None,
|
||||
config_attributes: TemplateVarsType = None,
|
||||
) -> None:
|
||||
"""Instantiate a value template."""
|
||||
self._value_template = value_template
|
||||
self._config_attributes = config_attributes
|
||||
if value_template is None:
|
||||
return
|
||||
|
||||
value_template.hass = hass
|
||||
self._entity = entity
|
||||
|
||||
if entity:
|
||||
value_template.hass = entity.hass
|
||||
|
||||
@callback
|
||||
def async_render_with_possible_json_value(
|
||||
self,
|
||||
payload: ReceivePayloadType,
|
||||
default: ReceivePayloadType | object = _SENTINEL,
|
||||
variables: TemplateVarsType = None,
|
||||
) -> ReceivePayloadType:
|
||||
"""Render with possible json value or pass-though a received MQTT value."""
|
||||
if self._value_template is None:
|
||||
return payload
|
||||
|
||||
values: dict[str, Any] = {}
|
||||
|
||||
if variables is not None:
|
||||
values.update(variables)
|
||||
|
||||
if self._config_attributes is not None:
|
||||
values.update(self._config_attributes)
|
||||
|
||||
if self._entity:
|
||||
values[ATTR_ENTITY_ID] = self._entity.entity_id
|
||||
values[ATTR_NAME] = self._entity.name
|
||||
|
||||
if default == _SENTINEL:
|
||||
return self._value_template.async_render_with_possible_json_value(
|
||||
payload, variables=values
|
||||
)
|
||||
|
||||
return self._value_template.async_render_with_possible_json_value(
|
||||
payload, default, variables=values
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Configure number in a device through MQTT topic."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
|
||||
@@ -27,8 +26,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttCommandTemplate, MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
@@ -41,11 +40,12 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttCommandTemplate, MqttValueTemplate
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -75,7 +75,7 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float),
|
||||
@@ -118,7 +118,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT number configured under the number platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, number.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
number.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -129,12 +133,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT number through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
# setup for discovery
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT scenes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -15,25 +14,27 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .. import mqtt
|
||||
from .client import async_publish
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN
|
||||
from .mixins import (
|
||||
CONF_ENABLED_BY_DEFAULT,
|
||||
CONF_OBJECT_ID,
|
||||
MQTT_AVAILABILITY_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .util import valid_publish_topic
|
||||
|
||||
DEFAULT_NAME = "MQTT Scene"
|
||||
DEFAULT_RETAIN = False
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON): cv.string,
|
||||
@@ -63,7 +64,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT scene configured under the scene platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, scene.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
scene.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -74,13 +79,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN)
|
||||
)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
@@ -128,7 +128,7 @@ class MqttScene(
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
await mqtt.async_publish(
|
||||
await async_publish(
|
||||
self.hass,
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
self._config[CONF_PAYLOAD_ON],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Configure select in a device through MQTT topic."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
|
||||
@@ -17,8 +16,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttCommandTemplate, MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
@@ -31,11 +30,12 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttCommandTemplate, MqttValueTemplate
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,7 +51,7 @@ MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset(
|
||||
)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
@@ -79,7 +79,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT select configured under the select platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, select.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
select.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -90,12 +94,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT select through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
# setup for discovery
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import functools
|
||||
import logging
|
||||
@@ -34,19 +33,21 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_RO_SCHEMA
|
||||
from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC
|
||||
from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttAvailability,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttValueTemplate
|
||||
from .util import valid_subscribe_topic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -89,12 +90,12 @@ def validate_options(conf):
|
||||
return conf
|
||||
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RO_SCHEMA.extend(
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
vol.Optional(CONF_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_LAST_RESET_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
|
||||
@@ -131,7 +132,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT sensors configured under the fan platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, sensor.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
sensor.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -142,12 +147,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
# setup for discovery
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT sirens."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import functools
|
||||
import json
|
||||
@@ -35,8 +34,8 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttCommandTemplate, MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
@@ -52,11 +51,12 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttCommandTemplate, MqttValueTemplate
|
||||
|
||||
DEFAULT_NAME = "MQTT Siren"
|
||||
DEFAULT_PAYLOAD_ON = "ON"
|
||||
@@ -74,7 +74,7 @@ CONF_SUPPORT_VOLUME_SET = "support_volume_set"
|
||||
|
||||
STATE = "state"
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_AVAILABLE_TONES): cv.ensure_list,
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
@@ -128,7 +128,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT sirens configured under the fan platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, siren.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
siren.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -139,13 +143,8 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN)
|
||||
)
|
||||
# setup for discovery
|
||||
setup = functools.partial(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT switches."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -24,8 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
@@ -38,11 +37,12 @@ from .debug_info import log_messages
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_COMMON_SCHEMA,
|
||||
MqttEntity,
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
warn_for_legacy_schema,
|
||||
)
|
||||
from .models import MqttValueTemplate
|
||||
|
||||
DEFAULT_NAME = "MQTT Switch"
|
||||
DEFAULT_PAYLOAD_ON = "ON"
|
||||
@@ -51,7 +51,7 @@ DEFAULT_OPTIMISTIC = False
|
||||
CONF_STATE_ON = "state_on"
|
||||
CONF_STATE_OFF = "state_off"
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
@@ -82,7 +82,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT switch configured under the fan platform key (deprecated)."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, switch.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
switch.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -93,12 +97,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
# setup for discovery
|
||||
|
||||
@@ -11,8 +11,8 @@ from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import MqttValueTemplate, subscription
|
||||
from .. import mqtt
|
||||
from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC
|
||||
from .mixins import (
|
||||
MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
@@ -21,7 +21,7 @@ from .mixins import (
|
||||
send_discovery_done,
|
||||
update_device,
|
||||
)
|
||||
from .models import ReceiveMessage
|
||||
from .models import MqttValueTemplate, ReceiveMessage
|
||||
from .subscription import EntitySubscription
|
||||
from .util import valid_subscribe_topic
|
||||
|
||||
@@ -30,7 +30,7 @@ LOG_NAME = "Tag"
|
||||
TAG = "tag"
|
||||
TAGS = "mqtt_tags"
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
vol.Optional(CONF_PLATFORM): "mqtt",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for MQTT vacuums."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -13,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from ..mixins import (
|
||||
async_get_platform_config_from_yaml,
|
||||
async_setup_entry_helper,
|
||||
async_setup_platform_discovery,
|
||||
async_setup_platform_helper,
|
||||
)
|
||||
from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE
|
||||
@@ -77,7 +76,11 @@ async def async_setup_platform(
|
||||
"""Set up MQTT vacuum through configuration.yaml."""
|
||||
# Deprecated in HA Core 2022.6
|
||||
await async_setup_platform_helper(
|
||||
hass, vacuum.DOMAIN, config, async_add_entities, _async_setup_entity
|
||||
hass,
|
||||
vacuum.DOMAIN,
|
||||
discovery_info or config,
|
||||
async_add_entities,
|
||||
_async_setup_entity,
|
||||
)
|
||||
|
||||
|
||||
@@ -88,12 +91,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery."""
|
||||
# load and initialize platform config from configuration.yaml
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_async_setup_entity(hass, async_add_entities, config, config_entry)
|
||||
for config in await async_get_platform_config_from_yaml(
|
||||
hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
config_entry.async_on_unload(
|
||||
await async_setup_platform_discovery(
|
||||
hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN
|
||||
)
|
||||
)
|
||||
# setup for discovery
|
||||
|
||||
@@ -15,11 +15,13 @@ from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
|
||||
from .. import MqttValueTemplate, subscription
|
||||
from ... import mqtt
|
||||
from .. import subscription
|
||||
from ..config import MQTT_BASE_SCHEMA
|
||||
from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN
|
||||
from ..debug_info import log_messages
|
||||
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema
|
||||
from ..models import MqttValueTemplate
|
||||
from ..util import valid_publish_topic
|
||||
from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED
|
||||
from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
|
||||
|
||||
@@ -96,25 +98,23 @@ MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED = MQTT_VACUUM_ATTRIBUTES_BLOCKED | frozens
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA_LEGACY_MODERN = (
|
||||
mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Inclusive(CONF_BATTERY_LEVEL_TEMPLATE, "battery"): cv.template,
|
||||
vol.Inclusive(
|
||||
CONF_BATTERY_LEVEL_TOPIC, "battery"
|
||||
): mqtt.valid_publish_topic,
|
||||
vol.Inclusive(CONF_BATTERY_LEVEL_TOPIC, "battery"): valid_publish_topic,
|
||||
vol.Inclusive(CONF_CHARGING_TEMPLATE, "charging"): cv.template,
|
||||
vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): mqtt.valid_publish_topic,
|
||||
vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): valid_publish_topic,
|
||||
vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template,
|
||||
vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): mqtt.valid_publish_topic,
|
||||
vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): valid_publish_topic,
|
||||
vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template,
|
||||
vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): mqtt.valid_publish_topic,
|
||||
vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): valid_publish_topic,
|
||||
vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template,
|
||||
vol.Inclusive(CONF_ERROR_TOPIC, "error"): mqtt.valid_publish_topic,
|
||||
vol.Inclusive(CONF_ERROR_TOPIC, "error"): valid_publish_topic,
|
||||
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
),
|
||||
vol.Inclusive(CONF_FAN_SPEED_TEMPLATE, "fan_speed"): cv.template,
|
||||
vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): mqtt.valid_publish_topic,
|
||||
vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): valid_publish_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(
|
||||
CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT
|
||||
@@ -135,12 +135,12 @@ PLATFORM_SCHEMA_LEGACY_MODERN = (
|
||||
vol.Optional(
|
||||
CONF_PAYLOAD_TURN_ON, default=DEFAULT_PAYLOAD_TURN_ON
|
||||
): cv.string,
|
||||
vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic,
|
||||
vol.Optional(
|
||||
CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS
|
||||
): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]),
|
||||
vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .. import subscription
|
||||
from ... import mqtt
|
||||
from ..config import MQTT_BASE_SCHEMA
|
||||
from ..const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
@@ -33,6 +33,7 @@ from ..const import (
|
||||
)
|
||||
from ..debug_info import log_messages
|
||||
from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema
|
||||
from ..util import valid_publish_topic
|
||||
from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED
|
||||
from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services
|
||||
|
||||
@@ -105,7 +106,7 @@ DEFAULT_PAYLOAD_START = "start"
|
||||
DEFAULT_PAYLOAD_PAUSE = "pause"
|
||||
|
||||
PLATFORM_SCHEMA_STATE_MODERN = (
|
||||
mqtt.MQTT_BASE_SCHEMA.extend(
|
||||
MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
@@ -123,13 +124,13 @@ PLATFORM_SCHEMA_STATE_MODERN = (
|
||||
vol.Optional(CONF_PAYLOAD_START, default=DEFAULT_PAYLOAD_START): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_PAUSE, default=DEFAULT_PAYLOAD_PAUSE): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
|
||||
vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_STATE_TOPIC): valid_publish_topic,
|
||||
vol.Optional(
|
||||
CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS
|
||||
): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]),
|
||||
vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
}
|
||||
)
|
||||
@@ -178,7 +179,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
supported_feature_strings, STRING_TO_SERVICE
|
||||
)
|
||||
self._fan_speed_list = config[CONF_FAN_SPEED_LIST]
|
||||
self._command_topic = config.get(mqtt.CONF_COMMAND_TOPIC)
|
||||
self._command_topic = config.get(CONF_COMMAND_TOPIC)
|
||||
self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC)
|
||||
self._send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ GPS_JSON_PAYLOAD_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend(
|
||||
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend(
|
||||
{vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}}
|
||||
)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
}
|
||||
).extend(mqtt.MQTT_RO_PLATFORM_SCHEMA.schema)
|
||||
).extend(mqtt.config.MQTT_RO_SCHEMA.schema)
|
||||
|
||||
MQTT_PAYLOAD = vol.Schema(
|
||||
vol.All(
|
||||
|
||||
@@ -85,9 +85,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Receive touch event."""
|
||||
gesture_type = TOUCH_GESTURE_TRIGGER_MAP.get(event.gesture_id)
|
||||
if gesture_type is None:
|
||||
_LOGGER.debug("Received unknown touch gesture ID %s", event.gesture_id)
|
||||
_LOGGER.warning(
|
||||
"Received unknown touch gesture ID %s", event.gesture_id
|
||||
)
|
||||
return
|
||||
_LOGGER.warning("Received touch gesture %s", gesture_type)
|
||||
_LOGGER.debug("Received touch gesture %s", gesture_type)
|
||||
hass.bus.async_fire(
|
||||
NANOLEAF_EVENT,
|
||||
{CONF_DEVICE_ID: device_entry.id, CONF_TYPE: gesture_type},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "netgear",
|
||||
"name": "NETGEAR",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netgear",
|
||||
"requirements": ["pynetgear==0.10.0"],
|
||||
"requirements": ["pynetgear==0.10.4"],
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
|
||||
"iot_class": "local_polling",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/plex",
|
||||
"requirements": [
|
||||
"plexapi==4.11.1",
|
||||
"plexapi==4.11.2",
|
||||
"plexauth==0.0.6",
|
||||
"plexwebsocket==0.0.13"
|
||||
],
|
||||
|
||||
@@ -50,10 +50,7 @@ from .const import (
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC"
|
||||
DEFAULT_ICON = "mdi:water"
|
||||
DEFAULT_SSL = True
|
||||
DEFAULT_UPDATE_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""This platform provides binary sensors for key RainMachine data."""
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
@@ -21,6 +20,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
from .model import RainMachineDescriptionMixinApiCategory
|
||||
from .util import key_exists
|
||||
|
||||
TYPE_FLOW_SENSOR = "flow_sensor"
|
||||
TYPE_FREEZE = "freeze"
|
||||
@@ -46,6 +46,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
name="Flow Sensor",
|
||||
icon="mdi:water-pump",
|
||||
api_category=DATA_PROVISION_SETTINGS,
|
||||
data_key="useFlowSensor",
|
||||
),
|
||||
RainMachineBinarySensorDescription(
|
||||
key=TYPE_FREEZE,
|
||||
@@ -53,6 +54,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
icon="mdi:cancel",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="freeze",
|
||||
),
|
||||
RainMachineBinarySensorDescription(
|
||||
key=TYPE_FREEZE_PROTECTION,
|
||||
@@ -60,6 +62,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
icon="mdi:weather-snowy",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
api_category=DATA_RESTRICTIONS_UNIVERSAL,
|
||||
data_key="freezeProtectEnabled",
|
||||
),
|
||||
RainMachineBinarySensorDescription(
|
||||
key=TYPE_HOT_DAYS,
|
||||
@@ -67,6 +70,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
icon="mdi:thermometer-lines",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
api_category=DATA_RESTRICTIONS_UNIVERSAL,
|
||||
data_key="hotDaysExtraWatering",
|
||||
),
|
||||
RainMachineBinarySensorDescription(
|
||||
key=TYPE_HOURLY,
|
||||
@@ -75,6 +79,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="hourly",
|
||||
),
|
||||
RainMachineBinarySensorDescription(
|
||||
key=TYPE_MONTH,
|
||||
@@ -83,6 +88,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="month",
|
||||
),
|
||||
RainMachineBinarySensorDescription(
|
||||
key=TYPE_RAINDELAY,
|
||||
@@ -91,6 +97,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="rainDelay",
|
||||
),
|
||||
RainMachineBinarySensorDescription(
|
||||
key=TYPE_RAINSENSOR,
|
||||
@@ -99,6 +106,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="rainSensor",
|
||||
),
|
||||
RainMachineBinarySensorDescription(
|
||||
key=TYPE_WEEKDAY,
|
||||
@@ -107,6 +115,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_RESTRICTIONS_CURRENT,
|
||||
data_key="weekDay",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -118,35 +127,23 @@ async def async_setup_entry(
|
||||
controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER]
|
||||
coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
@callback
|
||||
def async_get_sensor_by_api_category(api_category: str) -> partial:
|
||||
"""Generate the appropriate sensor object for an API category."""
|
||||
if api_category == DATA_PROVISION_SETTINGS:
|
||||
return partial(
|
||||
ProvisionSettingsBinarySensor,
|
||||
entry,
|
||||
coordinators[DATA_PROVISION_SETTINGS],
|
||||
)
|
||||
|
||||
if api_category == DATA_RESTRICTIONS_CURRENT:
|
||||
return partial(
|
||||
CurrentRestrictionsBinarySensor,
|
||||
entry,
|
||||
coordinators[DATA_RESTRICTIONS_CURRENT],
|
||||
)
|
||||
|
||||
return partial(
|
||||
UniversalRestrictionsBinarySensor,
|
||||
entry,
|
||||
coordinators[DATA_RESTRICTIONS_UNIVERSAL],
|
||||
)
|
||||
api_category_sensor_map = {
|
||||
DATA_PROVISION_SETTINGS: ProvisionSettingsBinarySensor,
|
||||
DATA_RESTRICTIONS_CURRENT: CurrentRestrictionsBinarySensor,
|
||||
DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsBinarySensor,
|
||||
}
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
async_get_sensor_by_api_category(description.api_category)(
|
||||
controller, description
|
||||
api_category_sensor_map[description.api_category](
|
||||
entry, coordinator, controller, description
|
||||
)
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
if (
|
||||
(coordinator := coordinators[description.api_category]) is not None
|
||||
and coordinator.data
|
||||
and key_exists(coordinator.data, description.data_key)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -158,17 +155,17 @@ class CurrentRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity):
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self.entity_description.key == TYPE_FREEZE:
|
||||
self._attr_is_on = self.coordinator.data["freeze"]
|
||||
self._attr_is_on = self.coordinator.data.get("freeze")
|
||||
elif self.entity_description.key == TYPE_HOURLY:
|
||||
self._attr_is_on = self.coordinator.data["hourly"]
|
||||
self._attr_is_on = self.coordinator.data.get("hourly")
|
||||
elif self.entity_description.key == TYPE_MONTH:
|
||||
self._attr_is_on = self.coordinator.data["month"]
|
||||
self._attr_is_on = self.coordinator.data.get("month")
|
||||
elif self.entity_description.key == TYPE_RAINDELAY:
|
||||
self._attr_is_on = self.coordinator.data["rainDelay"]
|
||||
self._attr_is_on = self.coordinator.data.get("rainDelay")
|
||||
elif self.entity_description.key == TYPE_RAINSENSOR:
|
||||
self._attr_is_on = self.coordinator.data["rainSensor"]
|
||||
self._attr_is_on = self.coordinator.data.get("rainSensor")
|
||||
elif self.entity_description.key == TYPE_WEEKDAY:
|
||||
self._attr_is_on = self.coordinator.data["weekDay"]
|
||||
self._attr_is_on = self.coordinator.data.get("weekDay")
|
||||
|
||||
|
||||
class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity):
|
||||
@@ -188,6 +185,6 @@ class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity):
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self.entity_description.key == TYPE_FREEZE_PROTECTION:
|
||||
self._attr_is_on = self.coordinator.data["freezeProtectEnabled"]
|
||||
self._attr_is_on = self.coordinator.data.get("freezeProtectEnabled")
|
||||
elif self.entity_description.key == TYPE_HOT_DAYS:
|
||||
self._attr_is_on = self.coordinator.data["hotDaysExtraWatering"]
|
||||
self._attr_is_on = self.coordinator.data.get("hotDaysExtraWatering")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "RainMachine",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
|
||||
"requirements": ["regenmaschine==2022.05.0"],
|
||||
"requirements": ["regenmaschine==2022.06.0"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "local_polling",
|
||||
"homekit": {
|
||||
|
||||
@@ -7,6 +7,7 @@ class RainMachineDescriptionMixinApiCategory:
|
||||
"""Define an entity description mixin for binary and regular sensors."""
|
||||
|
||||
api_category: str
|
||||
data_key: str
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -33,6 +32,7 @@ from .model import (
|
||||
RainMachineDescriptionMixinApiCategory,
|
||||
RainMachineDescriptionMixinUid,
|
||||
)
|
||||
from .util import key_exists
|
||||
|
||||
DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE = timedelta(seconds=5)
|
||||
|
||||
@@ -68,6 +68,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
api_category=DATA_PROVISION_SETTINGS,
|
||||
data_key="flowSensorClicksPerCubicMeter",
|
||||
),
|
||||
RainMachineSensorDescriptionApiCategory(
|
||||
key=TYPE_FLOW_SENSOR_CONSUMED_LITERS,
|
||||
@@ -78,6 +79,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
api_category=DATA_PROVISION_SETTINGS,
|
||||
data_key="flowSensorWateringClicks",
|
||||
),
|
||||
RainMachineSensorDescriptionApiCategory(
|
||||
key=TYPE_FLOW_SENSOR_START_INDEX,
|
||||
@@ -87,6 +89,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
native_unit_of_measurement="index",
|
||||
entity_registry_enabled_default=False,
|
||||
api_category=DATA_PROVISION_SETTINGS,
|
||||
data_key="flowSensorStartIndex",
|
||||
),
|
||||
RainMachineSensorDescriptionApiCategory(
|
||||
key=TYPE_FLOW_SENSOR_WATERING_CLICKS,
|
||||
@@ -97,6 +100,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
api_category=DATA_PROVISION_SETTINGS,
|
||||
data_key="flowSensorWateringClicks",
|
||||
),
|
||||
RainMachineSensorDescriptionApiCategory(
|
||||
key=TYPE_FREEZE_TEMP,
|
||||
@@ -107,6 +111,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
api_category=DATA_RESTRICTIONS_UNIVERSAL,
|
||||
data_key="freezeProtectTemp",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -118,27 +123,21 @@ async def async_setup_entry(
|
||||
controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER]
|
||||
coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
|
||||
|
||||
@callback
|
||||
def async_get_sensor_by_api_category(api_category: str) -> partial:
|
||||
"""Generate the appropriate sensor object for an API category."""
|
||||
if api_category == DATA_PROVISION_SETTINGS:
|
||||
return partial(
|
||||
ProvisionSettingsSensor,
|
||||
entry,
|
||||
coordinators[DATA_PROVISION_SETTINGS],
|
||||
)
|
||||
|
||||
return partial(
|
||||
UniversalRestrictionsSensor,
|
||||
entry,
|
||||
coordinators[DATA_RESTRICTIONS_UNIVERSAL],
|
||||
)
|
||||
api_category_sensor_map = {
|
||||
DATA_PROVISION_SETTINGS: ProvisionSettingsSensor,
|
||||
DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsSensor,
|
||||
}
|
||||
|
||||
sensors = [
|
||||
async_get_sensor_by_api_category(description.api_category)(
|
||||
controller, description
|
||||
api_category_sensor_map[description.api_category](
|
||||
entry, coordinator, controller, description
|
||||
)
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if (
|
||||
(coordinator := coordinators[description.api_category]) is not None
|
||||
and coordinator.data
|
||||
and key_exists(coordinator.data, description.data_key)
|
||||
)
|
||||
]
|
||||
|
||||
zone_coordinator = coordinators[DATA_ZONES]
|
||||
@@ -198,7 +197,7 @@ class UniversalRestrictionsSensor(RainMachineEntity, SensorEntity):
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the state."""
|
||||
if self.entity_description.key == TYPE_FREEZE_TEMP:
|
||||
self._attr_native_value = self.coordinator.data["freezeProtectTemp"]
|
||||
self._attr_native_value = self.coordinator.data.get("freezeProtectTemp")
|
||||
|
||||
|
||||
class ZoneTimeRemainingSensor(RainMachineEntity, SensorEntity):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user