forked from home-assistant/core
Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 668262aa1a | |||
| c4c8f88765 | |||
| f908e0cf4d | |||
| 29c720a66d | |||
| 4e628dbd9f | |||
| 37d904dfdc | |||
| a53997dfc7 | |||
| dd216ac15b | |||
| 2afdec4711 | |||
| 5b4c309170 | |||
| 8deec55204 | |||
| f0a2c4e30a | |||
| e9a71a8d7f | |||
| 1462366764 | |||
| 33528eb6bd | |||
| 776a014ab0 | |||
| ea202eff66 | |||
| b7404f5a05 | |||
| d015dff855 | |||
| 2f1977fa0c | |||
| 26fe23eb5c | |||
| dbfecf99dc | |||
| 4d28992f2b | |||
| 7a428a66bd | |||
| 481bf2694b | |||
| 5cc9cc3c99 | |||
| 87ce683b39 | |||
| 936d56f9af | |||
| d71ddcf69e | |||
| 3af2746fea | |||
| 5b6d7142fb | |||
| 7aa9301038 | |||
| 627831dfaf | |||
| db8a6f8583 | |||
| 014010acbd | |||
| 9b90ed04e5 | |||
| 0f27d0bf4a | |||
| 1fa55f96f8 | |||
| 2d60115ec6 | |||
| 3b81480091 | |||
| 255acfa8c0 | |||
| 4617cc4e0a | |||
| b9e8cfb291 | |||
| 7da1671b06 | |||
| 6c5f7eabff | |||
| f448f488ba | |||
| 20b5d5a755 | |||
| bb38a3a8ac | |||
| d0d1fb2da7 | |||
| d82be09ed4 | |||
| 110627e16e | |||
| b77ef7304a | |||
| 16a0b7f44e | |||
| 4fdbb9c0e2 | |||
| c32a988838 | |||
| 927c9d3480 | |||
| bf776d33b2 | |||
| 279539265b | |||
| 4acad77437 | |||
| 0c5b7401b9 | |||
| ce739fd9b6 | |||
| 11d9014be0 | |||
| c9dcb1c11b | |||
| ef7f32a28d | |||
| 4f5cf5797f | |||
| 4c5485ad04 | |||
| 5ad96dedfa | |||
| 0c18fe35e5 | |||
| 6a23ad96ca | |||
| def0384608 | |||
| a4d12694da | |||
| 2278e3f06f | |||
| 0144a0bb1f | |||
| 7cc8f91bf9 | |||
| d58157ca9e | |||
| f401ffb08c | |||
| 8f7b831b94 | |||
| 9ed6b591a5 | |||
| 98ea067285 | |||
| 7e507dd378 | |||
| 8e87223c40 | |||
| 0cce4d1b81 | |||
| 46dcc91510 | |||
| b1a2af9fd3 | |||
| 5d58cdd98e | |||
| a8aebbce9a | |||
| f1244c182a | |||
| 560eeac457 | |||
| d33080d79e | |||
| 25f02c5b38 | |||
| cb01af9f92 | |||
| 9a6ebb0848 | |||
| fd30dd0aee | |||
| 4a5e261709 | |||
| 2842f55460 | |||
| 7573a74cb0 | |||
| 636b484d9d | |||
| a979f884f9 | |||
| 990ea78dec | |||
| ee6db3bd23 | |||
| ae5606aa2f | |||
| 7f9f106729 | |||
| 44c63ce6f1 | |||
| cbf7ca6a9a | |||
| eb892df65a | |||
| 24b5886d88 | |||
| d5e902a170 | |||
| d907e4c10b | |||
| c4be3c4de2 | |||
| 626591f832 | |||
| 2bd3196183 | |||
| fd93cf375d | |||
| 6bf8b84d26 | |||
| c72fea57a1 | |||
| 17dad7d8ae | |||
| 14664719d9 | |||
| b14cd1e14b | |||
| fd38d9788d | |||
| 0b3b641328 | |||
| 6ef77f8243 | |||
| 3a27143012 | |||
| 9a6c642bdf | |||
| 38b8d0b018 | |||
| 4d3443dbf5 | |||
| 4f99e54402 | |||
| d6615e3d44 | |||
| 9c23331ead | |||
| 5fb2802bf4 | |||
| b4864e6a8a | |||
| 04c34877f4 | |||
| bdeb61fafc | |||
| 76d4257f51 |
@@ -14,30 +14,24 @@ from jaraco.abode.exceptions import (
|
||||
)
|
||||
from jaraco.abode.helpers.timeline import Groups as GROUPS
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DATE,
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TIME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_POLLING, DOMAIN, LOGGER
|
||||
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
||||
from .services import async_setup_services
|
||||
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
ATTR_DEVICE_TYPE = "device_type"
|
||||
@@ -45,22 +39,12 @@ ATTR_EVENT_CODE = "event_code"
|
||||
ATTR_EVENT_NAME = "event_name"
|
||||
ATTR_EVENT_TYPE = "event_type"
|
||||
ATTR_EVENT_UTC = "event_utc"
|
||||
ATTR_SETTING = "setting"
|
||||
ATTR_USER_NAME = "user_name"
|
||||
ATTR_APP_TYPE = "app_type"
|
||||
ATTR_EVENT_BY = "event_by"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
CHANGE_SETTING_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
|
||||
)
|
||||
|
||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -85,7 +69,7 @@ class AbodeSystem:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Abode component."""
|
||||
setup_hass_services(hass)
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
@@ -138,60 +122,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return unload_ok
|
||||
|
||||
|
||||
def setup_hass_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
def change_setting(call: ServiceCall) -> None:
|
||||
"""Change an Abode system setting."""
|
||||
setting = call.data[ATTR_SETTING]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
try:
|
||||
hass.data[DOMAIN].abode.set_setting(setting, value)
|
||||
except AbodeException as ex:
|
||||
LOGGER.warning(ex)
|
||||
|
||||
def capture_image(call: ServiceCall) -> None:
|
||||
"""Capture a new image."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_camera_capture_{entity_id}"
|
||||
dispatcher_send(hass, signal)
|
||||
|
||||
def trigger_automation(call: ServiceCall) -> None:
|
||||
"""Trigger an Abode automation."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_trigger_automation_{entity_id}"
|
||||
dispatcher_send(hass, signal)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def setup_hass_events(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant start and stop callbacks."""
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Support for the Abode Security System."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from jaraco.abode.exceptions import Exception as AbodeException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
||||
|
||||
ATTR_SETTING = "setting"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
|
||||
CHANGE_SETTING_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
|
||||
)
|
||||
|
||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
|
||||
def _change_setting(call: ServiceCall) -> None:
|
||||
"""Change an Abode system setting."""
|
||||
setting = call.data[ATTR_SETTING]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
try:
|
||||
call.hass.data[DOMAIN].abode.set_setting(setting, value)
|
||||
except AbodeException as ex:
|
||||
LOGGER.warning(ex)
|
||||
|
||||
|
||||
def _capture_image(call: ServiceCall) -> None:
|
||||
"""Capture a new image."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_camera_capture_{entity_id}"
|
||||
dispatcher_send(call.hass, signal)
|
||||
|
||||
|
||||
def _trigger_automation(call: ServiceCall) -> None:
|
||||
"""Trigger an Abode automation."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_trigger_automation_{entity_id}"
|
||||
dispatcher_send(call.hass, signal)
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SETTINGS, _change_setting, schema=CHANGE_SETTING_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, _capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_TRIGGER_AUTOMATION,
|
||||
_trigger_automation,
|
||||
schema=AUTOMATION_SCHEMA,
|
||||
)
|
||||
@@ -51,9 +51,16 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
self._current_version = (
|
||||
await self.client.get_current_measures()
|
||||
).firmware_version
|
||||
try:
|
||||
self._current_version = (
|
||||
await self.client.get_current_measures()
|
||||
).firmware_version
|
||||
except AirGradientError as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": str(error)},
|
||||
) from error
|
||||
|
||||
async def _async_update_data(self) -> AirGradientData:
|
||||
try:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairq"],
|
||||
"requirements": ["aioairq==0.4.4"]
|
||||
"requirements": ["aioairq==0.4.6"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["airtouch5py"],
|
||||
"requirements": ["airtouch5py==0.2.11"]
|
||||
"requirements": ["airtouch5py==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -34,10 +35,12 @@ BINARY_SENSORS: Final = (
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="online",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
is_on_fn=lambda _device: _device.online,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="bluetooth",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
translation_key="bluetooth",
|
||||
is_on_fn=lambda _device: _device.bluetooth_state,
|
||||
),
|
||||
|
||||
@@ -25,15 +25,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_num = serial_num
|
||||
model_details = coordinator.api.get_model_details(self.device)
|
||||
model = model_details["model"] if model_details else None
|
||||
model_details = coordinator.api.get_model_details(self.device) or {}
|
||||
model = model_details.get("model")
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer="Amazon",
|
||||
hw_version=model_details["hw_version"] if model_details else None,
|
||||
manufacturer=model_details.get("manufacturer", "Amazon"),
|
||||
hw_version=model_details.get("hw_version"),
|
||||
sw_version=(
|
||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
||||
),
|
||||
|
||||
@@ -3,120 +3,10 @@
|
||||
"name": "Amazon Devices",
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{ "macaddress": "007147*" },
|
||||
{ "macaddress": "00FC8B*" },
|
||||
{ "macaddress": "0812A5*" },
|
||||
{ "macaddress": "086AE5*" },
|
||||
{ "macaddress": "08849D*" },
|
||||
{ "macaddress": "089115*" },
|
||||
{ "macaddress": "08A6BC*" },
|
||||
{ "macaddress": "08C224*" },
|
||||
{ "macaddress": "0CDC91*" },
|
||||
{ "macaddress": "0CEE99*" },
|
||||
{ "macaddress": "1009F9*" },
|
||||
{ "macaddress": "109693*" },
|
||||
{ "macaddress": "10BF67*" },
|
||||
{ "macaddress": "10CE02*" },
|
||||
{ "macaddress": "140AC5*" },
|
||||
{ "macaddress": "149138*" },
|
||||
{ "macaddress": "1848BE*" },
|
||||
{ "macaddress": "1C12B0*" },
|
||||
{ "macaddress": "1C4D66*" },
|
||||
{ "macaddress": "1C93C4*" },
|
||||
{ "macaddress": "1CFE2B*" },
|
||||
{ "macaddress": "244CE3*" },
|
||||
{ "macaddress": "24CE33*" },
|
||||
{ "macaddress": "2873F6*" },
|
||||
{ "macaddress": "2C71FF*" },
|
||||
{ "macaddress": "34AFB3*" },
|
||||
{ "macaddress": "34D270*" },
|
||||
{ "macaddress": "38F73D*" },
|
||||
{ "macaddress": "3C5CC4*" },
|
||||
{ "macaddress": "3CE441*" },
|
||||
{ "macaddress": "440049*" },
|
||||
{ "macaddress": "40A2DB*" },
|
||||
{ "macaddress": "40A9CF*" },
|
||||
{ "macaddress": "40B4CD*" },
|
||||
{ "macaddress": "443D54*" },
|
||||
{ "macaddress": "44650D*" },
|
||||
{ "macaddress": "485F2D*" },
|
||||
{ "macaddress": "48785E*" },
|
||||
{ "macaddress": "48B423*" },
|
||||
{ "macaddress": "4C1744*" },
|
||||
{ "macaddress": "4CEFC0*" },
|
||||
{ "macaddress": "5007C3*" },
|
||||
{ "macaddress": "50D45C*" },
|
||||
{ "macaddress": "50DCE7*" },
|
||||
{ "macaddress": "50F5DA*" },
|
||||
{ "macaddress": "5C415A*" },
|
||||
{ "macaddress": "6837E9*" },
|
||||
{ "macaddress": "6854FD*" },
|
||||
{ "macaddress": "689A87*" },
|
||||
{ "macaddress": "68B691*" },
|
||||
{ "macaddress": "68DBF5*" },
|
||||
{ "macaddress": "68F63B*" },
|
||||
{ "macaddress": "6C0C9A*" },
|
||||
{ "macaddress": "6C5697*" },
|
||||
{ "macaddress": "7458F3*" },
|
||||
{ "macaddress": "74C246*" },
|
||||
{ "macaddress": "74D637*" },
|
||||
{ "macaddress": "74E20C*" },
|
||||
{ "macaddress": "74ECB2*" },
|
||||
{ "macaddress": "786C84*" },
|
||||
{ "macaddress": "78A03F*" },
|
||||
{ "macaddress": "7C6166*" },
|
||||
{ "macaddress": "7C6305*" },
|
||||
{ "macaddress": "7CD566*" },
|
||||
{ "macaddress": "8871E5*" },
|
||||
{ "macaddress": "901195*" },
|
||||
{ "macaddress": "90235B*" },
|
||||
{ "macaddress": "90A822*" },
|
||||
{ "macaddress": "90F82E*" },
|
||||
{ "macaddress": "943A91*" },
|
||||
{ "macaddress": "98226E*" },
|
||||
{ "macaddress": "98CCF3*" },
|
||||
{ "macaddress": "9CC8E9*" },
|
||||
{ "macaddress": "A002DC*" },
|
||||
{ "macaddress": "A0D2B1*" },
|
||||
{ "macaddress": "A40801*" },
|
||||
{ "macaddress": "A8E621*" },
|
||||
{ "macaddress": "AC416A*" },
|
||||
{ "macaddress": "AC63BE*" },
|
||||
{ "macaddress": "ACCCFC*" },
|
||||
{ "macaddress": "B0739C*" },
|
||||
{ "macaddress": "B0CFCB*" },
|
||||
{ "macaddress": "B0F7C4*" },
|
||||
{ "macaddress": "B85F98*" },
|
||||
{ "macaddress": "C091B9*" },
|
||||
{ "macaddress": "C095CF*" },
|
||||
{ "macaddress": "C49500*" },
|
||||
{ "macaddress": "C86C3D*" },
|
||||
{ "macaddress": "CC9EA2*" },
|
||||
{ "macaddress": "CCF735*" },
|
||||
{ "macaddress": "DC54D7*" },
|
||||
{ "macaddress": "D8BE65*" },
|
||||
{ "macaddress": "D8FBD6*" },
|
||||
{ "macaddress": "DC91BF*" },
|
||||
{ "macaddress": "DCA0D0*" },
|
||||
{ "macaddress": "E0F728*" },
|
||||
{ "macaddress": "EC2BEB*" },
|
||||
{ "macaddress": "EC8AC4*" },
|
||||
{ "macaddress": "ECA138*" },
|
||||
{ "macaddress": "F02F9E*" },
|
||||
{ "macaddress": "F0272D*" },
|
||||
{ "macaddress": "F0F0A4*" },
|
||||
{ "macaddress": "F4032A*" },
|
||||
{ "macaddress": "F854B8*" },
|
||||
{ "macaddress": "FC492D*" },
|
||||
{ "macaddress": "FC65DE*" },
|
||||
{ "macaddress": "FCA183*" },
|
||||
{ "macaddress": "FCE9D8*" }
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.0.5"]
|
||||
"requirements": ["aioamazondevices==3.0.6"]
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ rules:
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Network information not relevant
|
||||
discovery: done
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
|
||||
@@ -16,10 +16,7 @@ from amcrest import AmcrestError, ApiWrapper, LoginError
|
||||
import httpx
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_AUTHENTICATION,
|
||||
CONF_BINARY_SENSORS,
|
||||
CONF_HOST,
|
||||
@@ -30,21 +27,17 @@ from homeassistant.const import (
|
||||
CONF_SENSORS,
|
||||
CONF_SWITCHES,
|
||||
CONF_USERNAME,
|
||||
ENTITY_MATCH_ALL,
|
||||
ENTITY_MATCH_NONE,
|
||||
HTTP_BASIC_AUTHENTICATION,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .binary_sensor import BINARY_SENSOR_KEYS, BINARY_SENSORS, check_binary_sensors
|
||||
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
|
||||
from .camera import STREAM_SOURCE_LIST
|
||||
from .const import (
|
||||
CAMERAS,
|
||||
COMM_RETRIES,
|
||||
@@ -58,6 +51,7 @@ from .const import (
|
||||
)
|
||||
from .helpers import service_signal
|
||||
from .sensor import SENSOR_KEYS
|
||||
from .services import async_setup_services
|
||||
from .switch import SWITCH_KEYS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -455,47 +449,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if not hass.data[DATA_AMCREST][DEVICES]:
|
||||
return False
|
||||
|
||||
def have_permission(user: User | None, entity_id: str) -> bool:
|
||||
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call: ServiceCall) -> list[str]:
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
|
||||
return []
|
||||
|
||||
call_ids = await async_extract_entity_ids(hass, call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call: ServiceCall) -> None:
|
||||
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Support for Amcrest IP cameras."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
|
||||
from .camera import CAMERA_SERVICES
|
||||
from .const import CAMERAS, DATA_AMCREST, DOMAIN
|
||||
from .helpers import service_signal
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the Amcrest IP Camera services."""
|
||||
|
||||
def have_permission(user: User | None, entity_id: str) -> bool:
|
||||
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call: ServiceCall) -> list[str]:
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
|
||||
return []
|
||||
|
||||
call_ids = await async_extract_entity_ids(hass, call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call: ServiceCall) -> None:
|
||||
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["APsystemsEZ1"],
|
||||
"requirements": ["apsystems-ez1==2.6.0"]
|
||||
"requirements": ["apsystems-ez1==2.7.0"]
|
||||
}
|
||||
|
||||
@@ -1207,6 +1207,15 @@ class PipelineRun:
|
||||
|
||||
self._streamed_response_text = True
|
||||
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.INTENT_PROGRESS,
|
||||
{
|
||||
"tts_start_streaming": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
async def tts_input_stream_generator() -> AsyncGenerator[str]:
|
||||
"""Yield TTS input stream."""
|
||||
while (tts_input := await tts_input_stream.get()) is not None:
|
||||
|
||||
@@ -50,7 +50,7 @@ class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
|
||||
|
||||
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
|
||||
"""Initialise a Bosch Alarm control panel entity."""
|
||||
super().__init__(panel, area_id, unique_id, False, False, True)
|
||||
super().__init__(panel, area_id, unique_id, True, False, True)
|
||||
self._attr_unique_id = self._area_unique_id
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"requirements": ["python-bsblan==1.2.1"]
|
||||
"requirements": ["python-bsblan==2.1.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.3.9", "icalendar==6.1.0"]
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import socket
|
||||
|
||||
@@ -26,8 +27,18 @@ from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type CloudflareConfigEntry = ConfigEntry[CloudflareRuntimeData]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@dataclass
|
||||
class CloudflareRuntimeData:
|
||||
"""Runtime data for Cloudflare config entry."""
|
||||
|
||||
client: pycfdns.Client
|
||||
dns_zone: pycfdns.ZoneModel
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -> bool:
|
||||
"""Set up Cloudflare from a config entry."""
|
||||
session = async_get_clientsession(hass)
|
||||
client = pycfdns.Client(
|
||||
@@ -45,12 +56,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except pycfdns.ComunicationException as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
async def update_records(now):
|
||||
entry.runtime_data = CloudflareRuntimeData(client, dns_zone)
|
||||
|
||||
async def update_records(now: datetime) -> None:
|
||||
"""Set up recurring update."""
|
||||
try:
|
||||
await _async_update_cloudflare(
|
||||
hass, client, dns_zone, entry.data[CONF_RECORDS]
|
||||
)
|
||||
await _async_update_cloudflare(hass, entry)
|
||||
except (
|
||||
pycfdns.AuthenticationException,
|
||||
pycfdns.ComunicationException,
|
||||
@@ -60,9 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def update_records_service(call: ServiceCall) -> None:
|
||||
"""Set up service for manual trigger."""
|
||||
try:
|
||||
await _async_update_cloudflare(
|
||||
hass, client, dns_zone, entry.data[CONF_RECORDS]
|
||||
)
|
||||
await _async_update_cloudflare(hass, entry)
|
||||
except (
|
||||
pycfdns.AuthenticationException,
|
||||
pycfdns.ComunicationException,
|
||||
@@ -79,7 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -> bool:
|
||||
"""Unload Cloudflare config entry."""
|
||||
|
||||
return True
|
||||
@@ -87,10 +96,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def _async_update_cloudflare(
|
||||
hass: HomeAssistant,
|
||||
client: pycfdns.Client,
|
||||
dns_zone: pycfdns.ZoneModel,
|
||||
target_records: list[str],
|
||||
entry: CloudflareConfigEntry,
|
||||
) -> None:
|
||||
client = entry.runtime_data.client
|
||||
dns_zone = entry.runtime_data.dns_zone
|
||||
target_records: list[str] = entry.data[CONF_RECORDS]
|
||||
|
||||
_LOGGER.debug("Starting update for zone %s", dns_zone["name"])
|
||||
|
||||
records = await client.list_dns_records(zone_id=dns_zone["id"], type="A")
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["numpy==2.2.6"]
|
||||
"requirements": ["numpy==2.3.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_SOURCE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -17,6 +19,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass, entry.entry_id, entry.options[CONF_SOURCE]
|
||||
)
|
||||
|
||||
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SOURCE: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we need to clean the device links.
|
||||
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_SOURCE]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_SOURCE],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
@@ -34,7 +35,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
|
||||
def sort_ips(ips: list, querytype: str) -> list:
|
||||
def sort_ips(ips: list, querytype: Literal["A", "AAAA"]) -> list:
|
||||
"""Join IPs into a single string."""
|
||||
|
||||
if querytype == "AAAA":
|
||||
@@ -89,7 +90,7 @@ class WanIpSensor(SensorEntity):
|
||||
self.hostname = hostname
|
||||
self.resolver = aiodns.DNSResolver(tcp_port=port, udp_port=port)
|
||||
self.resolver.nameservers = [resolver]
|
||||
self.querytype = "AAAA" if ipv6 else "A"
|
||||
self.querytype: Literal["A", "AAAA"] = "AAAA" if ipv6 else "A"
|
||||
self._retries = DEFAULT_RETRIES
|
||||
self._attr_extra_state_attributes = {
|
||||
"resolver": resolver,
|
||||
@@ -106,7 +107,7 @@ class WanIpSensor(SensorEntity):
|
||||
async def async_update(self) -> None:
|
||||
"""Get the current DNS IP address for hostname."""
|
||||
try:
|
||||
response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload]
|
||||
response = await self.resolver.query(self.hostname, self.querytype)
|
||||
except DNSError as err:
|
||||
_LOGGER.warning("Exception while resolving host: %s", err)
|
||||
response = None
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import _LOGGER, CONF_DOWNLOAD_DIR
|
||||
from .services import register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -25,6 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
return False
|
||||
|
||||
register_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
@@ -141,7 +141,7 @@ def download_file(service: ServiceCall) -> None:
|
||||
threading.Thread(target=do_download).start()
|
||||
|
||||
|
||||
def register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the services for the downloader component."""
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
from typing import Any
|
||||
|
||||
from elkm1_lib.elements import Element
|
||||
from elkm1_lib.elk import Elk, Panel
|
||||
from elkm1_lib.elk import Elk
|
||||
from elkm1_lib.util import parse_url
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -26,12 +26,11 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.network import is_ip_address
|
||||
|
||||
from .const import (
|
||||
@@ -62,6 +61,7 @@ from .discovery import (
|
||||
async_update_entry_from_discovery,
|
||||
)
|
||||
from .models import ELKM1Data
|
||||
from .services import async_setup_services
|
||||
|
||||
type ElkM1ConfigEntry = ConfigEntry[ELKM1Data]
|
||||
|
||||
@@ -79,19 +79,6 @@ PLATFORMS = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
SPEAK_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SET_TIME_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def hostname_from_url(url: str) -> str:
|
||||
"""Return the hostname from a url."""
|
||||
@@ -179,7 +166,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
||||
"""Set up the Elk M1 platform."""
|
||||
_create_elk_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
async def _async_discovery(*_: Any) -> None:
|
||||
async_trigger_discovery(
|
||||
@@ -326,17 +313,6 @@ def _included(ranges: list[tuple[int, int]], set_to: bool, values: list[bool]) -
|
||||
values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
|
||||
|
||||
|
||||
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
|
||||
"""Search all config entries for a given prefix."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if not entry.runtime_data:
|
||||
continue
|
||||
elk_data: ELKM1Data = entry.runtime_data
|
||||
if elk_data.prefix == prefix:
|
||||
return elk_data.elk
|
||||
return None
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -390,39 +366,3 @@ async def async_wait_for_elk_to_sync(
|
||||
_LOGGER.debug("Received %s event", name)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel:
|
||||
"""Get the ElkM1 panel from a service call."""
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk.panel
|
||||
|
||||
|
||||
def _create_elk_services(hass: HomeAssistant) -> None:
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
@callback
|
||||
def _speak_word_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(hass, service).speak_word(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _speak_phrase_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(hass, service).speak_phrase(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _set_time_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(hass, service).set_time(dt_util.now())
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from elkm1_lib.elk import Elk, Panel
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import ELKM1Data
|
||||
|
||||
SPEAK_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SET_TIME_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
|
||||
"""Search all config entries for a given prefix."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if not entry.runtime_data:
|
||||
continue
|
||||
elk_data: ELKM1Data = entry.runtime_data
|
||||
if elk_data.prefix == prefix:
|
||||
return elk_data.elk
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_elk_panel(service: ServiceCall) -> Panel:
|
||||
"""Get the ElkM1 panel from a service call."""
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(service.hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk.panel
|
||||
|
||||
|
||||
@callback
|
||||
def _speak_word_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).speak_word(service.data["number"])
|
||||
|
||||
|
||||
@callback
|
||||
def _speak_phrase_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).speak_phrase(service.data["number"])
|
||||
|
||||
|
||||
@callback
|
||||
def _set_time_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).set_time(dt_util.now())
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
|
||||
)
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from pyenphase import Envoy
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -10,14 +9,9 @@ from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
OPTION_DISABLE_KEEP_ALIVE,
|
||||
OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
|
||||
|
||||
|
||||
@@ -25,19 +19,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
|
||||
"""Set up Enphase Envoy from a config entry."""
|
||||
|
||||
host = entry.data[CONF_HOST]
|
||||
options = entry.options
|
||||
envoy = (
|
||||
Envoy(
|
||||
host,
|
||||
httpx.AsyncClient(
|
||||
verify=False, limits=httpx.Limits(max_keepalive_connections=0)
|
||||
),
|
||||
)
|
||||
if options.get(
|
||||
OPTION_DISABLE_KEEP_ALIVE, OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE
|
||||
)
|
||||
else Envoy(host, get_async_client(hass, verify_ssl=False))
|
||||
)
|
||||
session = async_create_clientsession(hass, verify_ssl=False)
|
||||
envoy = Envoy(host, session)
|
||||
coordinator = EnphaseUpdateCoordinator(hass, envoy, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
@@ -63,7 +63,7 @@ async def validate_input(
|
||||
description_placeholders: dict[str, str],
|
||||
) -> Envoy:
|
||||
"""Validate the user input allows us to connect."""
|
||||
envoy = Envoy(host, get_async_client(hass, verify_ssl=False))
|
||||
envoy = Envoy(host, async_get_clientsession(hass, verify_ssl=False))
|
||||
try:
|
||||
await envoy.setup()
|
||||
await envoy.authenticate(username=username, password=password)
|
||||
|
||||
@@ -180,9 +180,15 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
)
|
||||
return
|
||||
|
||||
device_registry.async_update_device(
|
||||
device_id=envoy_device.id,
|
||||
new_connections={connection},
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
self.envoy_serial_number,
|
||||
)
|
||||
},
|
||||
connections={connection},
|
||||
)
|
||||
_LOGGER.debug("added connection: %s to %s", connection, self.name)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import copy
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohttp import ClientResponse
|
||||
from attr import asdict
|
||||
from pyenphase.envoy import Envoy
|
||||
from pyenphase.exceptions import EnvoyError
|
||||
@@ -69,14 +70,14 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
|
||||
for end_point in end_points:
|
||||
try:
|
||||
response = await envoy.request(end_point)
|
||||
fixture_data[end_point] = response.text.replace("\n", "").replace(
|
||||
serial, CLEAN_TEXT
|
||||
response: ClientResponse = await envoy.request(end_point)
|
||||
fixture_data[end_point] = (
|
||||
(await response.text()).replace("\n", "").replace(serial, CLEAN_TEXT)
|
||||
)
|
||||
fixture_data[f"{end_point}_log"] = json_dumps(
|
||||
{
|
||||
"headers": dict(response.headers.items()),
|
||||
"code": response.status_code,
|
||||
"code": response.status,
|
||||
}
|
||||
)
|
||||
except EnvoyError as err:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from httpx import HTTPError
|
||||
from aiohttp import ClientError
|
||||
from pyenphase import EnvoyData
|
||||
from pyenphase.exceptions import EnvoyError
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EnphaseUpdateCoordinator
|
||||
|
||||
ACTIONERRORS = (EnvoyError, HTTPError)
|
||||
ACTIONERRORS = (EnvoyError, ClientError)
|
||||
|
||||
|
||||
class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator]):
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==1.26.1"],
|
||||
"requirements": ["pyenphase==2.0.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.10.2"]
|
||||
"requirements": ["env-canada==0.11.2"]
|
||||
}
|
||||
|
||||
@@ -226,6 +226,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
unique_id: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
from aioesphomeapi import (
|
||||
@@ -23,6 +22,7 @@ from aioesphomeapi import (
|
||||
RequiresEncryptionAPIError,
|
||||
UserService,
|
||||
UserServiceArgType,
|
||||
parse_log_message,
|
||||
)
|
||||
from awesomeversion import AwesomeVersion
|
||||
import voluptuous as vol
|
||||
@@ -110,11 +110,6 @@ LOGGER_TO_LOG_LEVEL = {
|
||||
logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
|
||||
logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR,
|
||||
}
|
||||
# 7-bit and 8-bit C1 ANSI sequences
|
||||
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
|
||||
ANSI_ESCAPE_78BIT = re.compile(
|
||||
rb"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])"
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -387,13 +382,15 @@ class ESPHomeManager:
|
||||
|
||||
def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
|
||||
"""Handle a log message from the API."""
|
||||
log: bytes = msg.message
|
||||
_LOGGER.log(
|
||||
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
|
||||
"%s: %s",
|
||||
self.entry.title,
|
||||
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"),
|
||||
)
|
||||
for line in parse_log_message(
|
||||
msg.message.decode("utf-8", "backslashreplace"), "", strip_ansi_escapes=True
|
||||
):
|
||||
_LOGGER.log(
|
||||
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
|
||||
"%s: %s",
|
||||
self.entry.title,
|
||||
line,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_get_equivalent_log_level(self) -> LogLevel:
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==32.0.0",
|
||||
"aioesphomeapi==32.2.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
|
||||
@@ -78,7 +78,7 @@ class EsphomeMediaPlayer(
|
||||
if self._static_info.supports_pause:
|
||||
flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
|
||||
self._attr_supported_features = flags
|
||||
self._entry_data.media_player_formats[static_info.unique_id] = cast(
|
||||
self._entry_data.media_player_formats[self.unique_id] = cast(
|
||||
MediaPlayerInfo, static_info
|
||||
).supported_formats
|
||||
|
||||
@@ -114,9 +114,8 @@ class EsphomeMediaPlayer(
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
|
||||
bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY)
|
||||
|
||||
supported_formats: list[MediaPlayerSupportedFormat] | None = (
|
||||
self._entry_data.media_player_formats.get(self._static_info.unique_id)
|
||||
self._entry_data.media_player_formats.get(self.unique_id)
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -139,7 +138,7 @@ class EsphomeMediaPlayer(
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Handle entity being removed."""
|
||||
await super().async_will_remove_from_hass()
|
||||
self._entry_data.media_player_formats.pop(self.entity_id, None)
|
||||
self._entry_data.media_player_formats.pop(self.unique_id, None)
|
||||
|
||||
def _get_proxy_url(
|
||||
self,
|
||||
|
||||
@@ -11,32 +11,25 @@ from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONTENT_TYPE_MULTIPART,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
from homeassistant.util.system_info import is_official_image
|
||||
|
||||
DOMAIN = "ffmpeg"
|
||||
|
||||
SERVICE_START = "start"
|
||||
SERVICE_STOP = "stop"
|
||||
SERVICE_RESTART = "restart"
|
||||
|
||||
SIGNAL_FFMPEG_START = SignalType[list[str] | None]("ffmpeg.start")
|
||||
SIGNAL_FFMPEG_STOP = SignalType[list[str] | None]("ffmpeg.stop")
|
||||
SIGNAL_FFMPEG_RESTART = SignalType[list[str] | None]("ffmpeg.restart")
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SIGNAL_FFMPEG_RESTART,
|
||||
SIGNAL_FFMPEG_START,
|
||||
SIGNAL_FFMPEG_STOP,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
DATA_FFMPEG = "ffmpeg"
|
||||
|
||||
@@ -63,8 +56,6 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the FFmpeg component."""
|
||||
@@ -74,29 +65,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
await manager.async_get_version()
|
||||
|
||||
# Register service
|
||||
async def async_service_handle(service: ServiceCall) -> None:
|
||||
"""Handle service ffmpeg process."""
|
||||
entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if service.service == SERVICE_START:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids)
|
||||
elif service.service == SERVICE_STOP:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids)
|
||||
else:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_STOP, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTART, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
async_setup_services(hass)
|
||||
|
||||
hass.data[DATA_FFMPEG] = manager
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Support for FFmpeg."""
|
||||
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
|
||||
DOMAIN = "ffmpeg"
|
||||
|
||||
SIGNAL_FFMPEG_START = SignalType[list[str] | None]("ffmpeg.start")
|
||||
SIGNAL_FFMPEG_STOP = SignalType[list[str] | None]("ffmpeg.stop")
|
||||
SIGNAL_FFMPEG_RESTART = SignalType[list[str] | None]("ffmpeg.restart")
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Support for FFmpeg."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SIGNAL_FFMPEG_RESTART,
|
||||
SIGNAL_FFMPEG_START,
|
||||
SIGNAL_FFMPEG_STOP,
|
||||
)
|
||||
|
||||
SERVICE_START = "start"
|
||||
SERVICE_STOP = "stop"
|
||||
SERVICE_RESTART = "restart"
|
||||
|
||||
SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
|
||||
|
||||
|
||||
async def _async_service_handle(service: ServiceCall) -> None:
|
||||
"""Handle service ffmpeg process."""
|
||||
entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if service.service == SERVICE_START:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_START, entity_ids)
|
||||
elif service.service == SERVICE_STOP:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_STOP, entity_ids)
|
||||
else:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_RESTART, entity_ids)
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register FFmpeg services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_STOP, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTART, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
@@ -10,7 +10,6 @@ from typing import Any
|
||||
import aiohttp
|
||||
from gcal_sync.api import GoogleCalendarService
|
||||
from gcal_sync.exceptions import ApiException, AuthException
|
||||
from gcal_sync.model import DateOrDatetime, Event
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
@@ -21,32 +20,14 @@ from homeassistant.const import (
|
||||
CONF_OFFSET,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
from .api import ApiAuthImpl, get_feature_access
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
EVENT_DESCRIPTION,
|
||||
EVENT_END_DATE,
|
||||
EVENT_END_DATETIME,
|
||||
EVENT_IN,
|
||||
EVENT_IN_DAYS,
|
||||
EVENT_IN_WEEKS,
|
||||
EVENT_LOCATION,
|
||||
EVENT_START_DATE,
|
||||
EVENT_START_DATETIME,
|
||||
EVENT_SUMMARY,
|
||||
EVENT_TYPES_CONF,
|
||||
FeatureAccess,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
from .store import GoogleConfigEntry, GoogleRuntimeData, LocalCalendarStore
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -63,10 +44,6 @@ CONF_MAX_RESULTS = "max_results"
|
||||
|
||||
DEFAULT_CONF_OFFSET = "!!"
|
||||
|
||||
EVENT_CALENDAR_ID = "calendar_id"
|
||||
|
||||
SERVICE_ADD_EVENT = "add_event"
|
||||
|
||||
YAML_DEVICES = f"{DOMAIN}_calendars.yaml"
|
||||
|
||||
PLATFORMS = [Platform.CALENDAR]
|
||||
@@ -100,41 +77,6 @@ DEVICE_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
_EVENT_IN_TYPES = vol.Schema(
|
||||
{
|
||||
vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES_CONF): cv.positive_int,
|
||||
vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES_CONF): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
ADD_EVENT_SERVICE_SCHEMA = vol.All(
|
||||
cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
|
||||
cv.has_at_most_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
|
||||
{
|
||||
vol.Required(EVENT_CALENDAR_ID): cv.string,
|
||||
vol.Required(EVENT_SUMMARY): cv.string,
|
||||
vol.Optional(EVENT_DESCRIPTION, default=""): cv.string,
|
||||
vol.Optional(EVENT_LOCATION, default=""): cv.string,
|
||||
vol.Inclusive(
|
||||
EVENT_START_DATE, "dates", "Start and end dates must both be specified"
|
||||
): cv.date,
|
||||
vol.Inclusive(
|
||||
EVENT_END_DATE, "dates", "Start and end dates must both be specified"
|
||||
): cv.date,
|
||||
vol.Inclusive(
|
||||
EVENT_START_DATETIME,
|
||||
"datetimes",
|
||||
"Start and end datetimes must both be specified",
|
||||
): cv.datetime,
|
||||
vol.Inclusive(
|
||||
EVENT_END_DATETIME,
|
||||
"datetimes",
|
||||
"Start and end datetimes must both be specified",
|
||||
): cv.datetime,
|
||||
vol.Optional(EVENT_IN): _EVENT_IN_TYPES,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bool:
|
||||
"""Set up Google from a config entry."""
|
||||
@@ -190,10 +132,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bo
|
||||
|
||||
hass.config_entries.async_update_entry(entry, unique_id=primary_calendar.id)
|
||||
|
||||
# Only expose the add event service if we have the correct permissions
|
||||
if get_feature_access(entry) is FeatureAccess.read_write:
|
||||
await async_setup_add_event_service(hass, calendar_service)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
@@ -225,79 +163,6 @@ async def async_remove_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> N
|
||||
await store.async_remove()
|
||||
|
||||
|
||||
async def async_setup_add_event_service(
|
||||
hass: HomeAssistant,
|
||||
calendar_service: GoogleCalendarService,
|
||||
) -> None:
|
||||
"""Add the service to add events."""
|
||||
|
||||
async def _add_event(call: ServiceCall) -> None:
|
||||
"""Add a new event to calendar."""
|
||||
_LOGGER.warning(
|
||||
"The Google Calendar add_event service has been deprecated, and "
|
||||
"will be removed in a future Home Assistant release. Please move "
|
||||
"calls to the create_event service"
|
||||
)
|
||||
|
||||
start: DateOrDatetime | None = None
|
||||
end: DateOrDatetime | None = None
|
||||
|
||||
if EVENT_IN in call.data:
|
||||
if EVENT_IN_DAYS in call.data[EVENT_IN]:
|
||||
now = datetime.now()
|
||||
|
||||
start_in = now + timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS])
|
||||
end_in = start_in + timedelta(days=1)
|
||||
|
||||
start = DateOrDatetime(date=start_in)
|
||||
end = DateOrDatetime(date=end_in)
|
||||
|
||||
elif EVENT_IN_WEEKS in call.data[EVENT_IN]:
|
||||
now = datetime.now()
|
||||
|
||||
start_in = now + timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
|
||||
end_in = start_in + timedelta(days=1)
|
||||
|
||||
start = DateOrDatetime(date=start_in)
|
||||
end = DateOrDatetime(date=end_in)
|
||||
|
||||
elif EVENT_START_DATE in call.data and EVENT_END_DATE in call.data:
|
||||
start = DateOrDatetime(date=call.data[EVENT_START_DATE])
|
||||
end = DateOrDatetime(date=call.data[EVENT_END_DATE])
|
||||
|
||||
elif EVENT_START_DATETIME in call.data and EVENT_END_DATETIME in call.data:
|
||||
start_dt = call.data[EVENT_START_DATETIME]
|
||||
end_dt = call.data[EVENT_END_DATETIME]
|
||||
start = DateOrDatetime(
|
||||
date_time=start_dt, timezone=str(hass.config.time_zone)
|
||||
)
|
||||
end = DateOrDatetime(date_time=end_dt, timezone=str(hass.config.time_zone))
|
||||
|
||||
if start is None or end is None:
|
||||
raise ValueError(
|
||||
"Missing required fields to set start or end date/datetime"
|
||||
)
|
||||
event = Event(
|
||||
summary=call.data[EVENT_SUMMARY],
|
||||
description=call.data[EVENT_DESCRIPTION],
|
||||
start=start,
|
||||
end=end,
|
||||
)
|
||||
if location := call.data.get(EVENT_LOCATION):
|
||||
event.location = location
|
||||
try:
|
||||
await calendar_service.async_create_event(
|
||||
call.data[EVENT_CALENDAR_ID],
|
||||
event,
|
||||
)
|
||||
except ApiException as err:
|
||||
raise HomeAssistantError(str(err)) from err
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
def get_calendar_info(
|
||||
hass: HomeAssistant, calendar: Mapping[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
|
||||
@@ -2,21 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
import aiohttp
|
||||
from gassist_text import TextAssistant
|
||||
from google.oauth2.credentials import Credentials
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery, intent
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
@@ -31,21 +23,9 @@ from .helpers import (
|
||||
GoogleAssistantSDKConfigEntry,
|
||||
GoogleAssistantSDKRuntimeData,
|
||||
InMemoryStorage,
|
||||
async_send_text_commands,
|
||||
best_matching_language_code,
|
||||
)
|
||||
|
||||
SERVICE_SEND_TEXT_COMMAND = "send_text_command"
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command"
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player"
|
||||
SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All(
|
||||
cv.ensure_list, [vol.All(str, vol.Length(min=1))]
|
||||
),
|
||||
vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids,
|
||||
},
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
@@ -58,6 +38,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -81,8 +63,6 @@ async def async_setup_entry(
|
||||
mem_storage = InMemoryStorage(hass)
|
||||
hass.http.register_view(GoogleAssistantSDKAudioView(mem_storage))
|
||||
|
||||
await async_setup_service(hass)
|
||||
|
||||
entry.runtime_data = GoogleAssistantSDKRuntimeData(
|
||||
session=session, mem_storage=mem_storage
|
||||
)
|
||||
@@ -105,36 +85,6 @@ async def async_unload_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Assistant SDK."""
|
||||
|
||||
async def send_text_command(call: ServiceCall) -> ServiceResponse:
|
||||
"""Send a text command to Google Assistant SDK."""
|
||||
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
|
||||
media_players: list[str] | None = call.data.get(
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
|
||||
)
|
||||
command_response_list = await async_send_text_commands(
|
||||
hass, commands, media_players
|
||||
)
|
||||
if call.return_response:
|
||||
return {
|
||||
"responses": [
|
||||
dataclasses.asdict(command_response)
|
||||
for command_response in command_response_list
|
||||
]
|
||||
}
|
||||
return None
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_TEXT_COMMAND,
|
||||
send_text_command,
|
||||
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
|
||||
|
||||
class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
|
||||
"""Google Assistant SDK conversation agent."""
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import aiohttp
|
||||
from aiohttp import web
|
||||
from gassist_text import TextAssistant
|
||||
from google.oauth2.credentials import Credentials
|
||||
from grpc import RpcError
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -25,6 +26,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
@@ -83,7 +85,17 @@ async def async_send_text_commands(
|
||||
) as assistant:
|
||||
command_response_list = []
|
||||
for command in commands:
|
||||
resp = await hass.async_add_executor_job(assistant.assist, command)
|
||||
try:
|
||||
resp = await hass.async_add_executor_job(assistant.assist, command)
|
||||
except RpcError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to send command '%s' to Google Assistant: %s",
|
||||
command,
|
||||
err,
|
||||
)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="grpc_error"
|
||||
) from err
|
||||
text_response = resp[0]
|
||||
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
|
||||
audio_response = resp[2]
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Support for Google Assistant SDK."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN
|
||||
from .helpers import async_send_text_commands
|
||||
|
||||
SERVICE_SEND_TEXT_COMMAND = "send_text_command"
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command"
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player"
|
||||
SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All(
|
||||
cv.ensure_list, [vol.All(str, vol.Length(min=1))]
|
||||
),
|
||||
vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _send_text_command(call: ServiceCall) -> ServiceResponse:
|
||||
"""Send a text command to Google Assistant SDK."""
|
||||
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
|
||||
media_players: list[str] | None = call.data.get(
|
||||
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
|
||||
)
|
||||
command_response_list = await async_send_text_commands(
|
||||
call.hass, commands, media_players
|
||||
)
|
||||
if call.return_response:
|
||||
return {
|
||||
"responses": [
|
||||
dataclasses.asdict(command_response)
|
||||
for command_response in command_response_list
|
||||
]
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Assistant SDK."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_TEXT_COMMAND,
|
||||
_send_text_command,
|
||||
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
@@ -57,5 +57,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"grpc_error": {
|
||||
"message": "Failed to communicate with Google Assistant"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from . import api
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GooglePhotosConfigEntry, GooglePhotosUpdateCoordinator
|
||||
from .services import async_register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
__all__ = ["DOMAIN"]
|
||||
|
||||
@@ -24,7 +24,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Google Photos integration."""
|
||||
|
||||
async_register_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ def _read_file_contents(
|
||||
return results
|
||||
|
||||
|
||||
def async_register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register Google Photos services."""
|
||||
|
||||
async def async_handle_upload(call: ServiceCall) -> ServiceResponse:
|
||||
|
||||
@@ -2,48 +2,33 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from gspread import Client
|
||||
from gspread.exceptions import APIError
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEFAULT_ACCESS, DOMAIN
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
type GoogleSheetsConfigEntry = ConfigEntry[OAuth2Session]
|
||||
|
||||
DATA = "data"
|
||||
DATA_CONFIG_ENTRY = "config_entry"
|
||||
WORKSHEET = "worksheet"
|
||||
|
||||
SERVICE_APPEND_SHEET = "append_sheet"
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Activate the Google Sheets component."""
|
||||
|
||||
SHEET_SERVICE_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
||||
vol.Optional(WORKSHEET): cv.string,
|
||||
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
|
||||
},
|
||||
)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -67,8 +52,6 @@ async def async_setup_entry(
|
||||
raise ConfigEntryAuthFailed("Required scopes are not present, reauth required")
|
||||
entry.runtime_data = session
|
||||
|
||||
await async_setup_service(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -81,55 +64,4 @@ async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for service_name in hass.services.async_services_for_domain(DOMAIN):
|
||||
hass.services.async_remove(DOMAIN, service_name)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Sheets."""
|
||||
|
||||
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
|
||||
"""Run append in the executor."""
|
||||
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
|
||||
try:
|
||||
sheet = service.open_by_key(entry.unique_id)
|
||||
except RefreshError:
|
||||
entry.async_start_reauth(hass)
|
||||
raise
|
||||
except APIError as ex:
|
||||
raise HomeAssistantError("Failed to write data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
now = str(datetime.now())
|
||||
rows = []
|
||||
for d in call.data[DATA]:
|
||||
row_data = {"created": now} | d
|
||||
row = [row_data.get(column, "") for column in columns]
|
||||
for key, value in row_data.items():
|
||||
if key not in columns:
|
||||
columns.append(key)
|
||||
worksheet.update_cell(1, len(columns), key)
|
||||
row.append(value)
|
||||
rows.append(row)
|
||||
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
|
||||
|
||||
async def append_to_sheet(call: ServiceCall) -> None:
|
||||
"""Append new line of data to a Google Sheets document."""
|
||||
entry: GoogleSheetsConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
call.data[DATA_CONFIG_ENTRY]
|
||||
)
|
||||
if not entry or not hasattr(entry, "runtime_data"):
|
||||
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
|
||||
await entry.runtime_data.async_ensure_token_valid()
|
||||
await hass.async_add_executor_job(_append_to_sheet, call, entry)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_APPEND_SHEET,
|
||||
append_to_sheet,
|
||||
schema=SHEET_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Support for Google Sheets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from gspread import Client
|
||||
from gspread.exceptions import APIError
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import GoogleSheetsConfigEntry
|
||||
|
||||
DATA = "data"
|
||||
DATA_CONFIG_ENTRY = "config_entry"
|
||||
WORKSHEET = "worksheet"
|
||||
|
||||
SERVICE_APPEND_SHEET = "append_sheet"
|
||||
|
||||
SHEET_SERVICE_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
||||
vol.Optional(WORKSHEET): cv.string,
|
||||
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
|
||||
"""Run append in the executor."""
|
||||
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
|
||||
try:
|
||||
sheet = service.open_by_key(entry.unique_id)
|
||||
except RefreshError:
|
||||
entry.async_start_reauth(call.hass)
|
||||
raise
|
||||
except APIError as ex:
|
||||
raise HomeAssistantError("Failed to write data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
now = str(datetime.now())
|
||||
rows = []
|
||||
for d in call.data[DATA]:
|
||||
row_data = {"created": now} | d
|
||||
row = [row_data.get(column, "") for column in columns]
|
||||
for key, value in row_data.items():
|
||||
if key not in columns:
|
||||
columns.append(key)
|
||||
worksheet.update_cell(1, len(columns), key)
|
||||
row.append(value)
|
||||
rows.append(row)
|
||||
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
|
||||
|
||||
|
||||
async def _async_append_to_sheet(call: ServiceCall) -> None:
|
||||
"""Append new line of data to a Google Sheets document."""
|
||||
entry: GoogleSheetsConfigEntry | None = call.hass.config_entries.async_get_entry(
|
||||
call.data[DATA_CONFIG_ENTRY]
|
||||
)
|
||||
if not entry or not hasattr(entry, "runtime_data"):
|
||||
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
|
||||
await entry.runtime_data.async_ensure_token_valid()
|
||||
await call.hass.async_add_executor_job(_append_to_sheet, call, entry)
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Sheets."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_APPEND_SHEET,
|
||||
_async_append_to_sheet,
|
||||
schema=SHEET_SERVICE_SCHEMA,
|
||||
)
|
||||
@@ -37,6 +37,7 @@ from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
discovery_flow,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.deprecation import (
|
||||
@@ -51,9 +52,11 @@ from homeassistant.helpers.hassio import (
|
||||
get_supervisor_ip as _get_supervisor_ip,
|
||||
is_hassio as _is_hassio,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.service_info.hassio import (
|
||||
HassioServiceInfo as _HassioServiceInfo,
|
||||
)
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
@@ -109,7 +112,7 @@ from .coordinator import (
|
||||
get_core_info, # noqa: F401
|
||||
get_core_stats, # noqa: F401
|
||||
get_host_info, # noqa: F401
|
||||
get_info, # noqa: F401
|
||||
get_info,
|
||||
get_issues_info, # noqa: F401
|
||||
get_os_info,
|
||||
get_supervisor_info, # noqa: F401
|
||||
@@ -168,6 +171,11 @@ SERVICE_RESTORE_PARTIAL = "restore_partial"
|
||||
|
||||
VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
|
||||
|
||||
DEPRECATION_URL = (
|
||||
"https://www.home-assistant.io/blog/2025/05/22/"
|
||||
"deprecating-core-and-supervised-installation-methods-and-32-bit-systems/"
|
||||
)
|
||||
|
||||
|
||||
def valid_addon(value: Any) -> str:
|
||||
"""Validate value is a valid addon slug."""
|
||||
@@ -546,6 +554,63 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = coordinator
|
||||
|
||||
system_info = await async_get_system_info(hass)
|
||||
|
||||
def deprecated_setup_issue() -> None:
|
||||
os_info = get_os_info(hass)
|
||||
info = get_info(hass)
|
||||
if os_info is None or info is None:
|
||||
return
|
||||
is_haos = info.get("hassos") is not None
|
||||
arch = system_info["arch"]
|
||||
board = os_info.get("board")
|
||||
supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"}
|
||||
if is_haos and arch == "armv7" and supported_board:
|
||||
issue_id = "deprecated_os_"
|
||||
if board in {"rpi3", "rpi4"}:
|
||||
issue_id += "aarch64"
|
||||
elif board in {"tinker", "odroid-xu4", "rpi2"}:
|
||||
issue_id += "armv7"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||
},
|
||||
)
|
||||
deprecated_architecture = False
|
||||
if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board):
|
||||
deprecated_architecture = True
|
||||
if not is_haos or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if not is_haos:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": "OS" if is_haos else "Supervised",
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
listener()
|
||||
|
||||
listener = coordinator.async_add_listener(deprecated_setup_issue)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -144,5 +144,5 @@ class SupervisorEntityModel(StrEnum):
|
||||
ADDON = "Home Assistant Add-on"
|
||||
OS = "Home Assistant Operating System"
|
||||
CORE = "Home Assistant Core"
|
||||
SUPERVIOSR = "Home Assistant Supervisor"
|
||||
SUPERVISOR = "Home Assistant Supervisor"
|
||||
HOST = "Home Assistant Host"
|
||||
|
||||
@@ -261,7 +261,7 @@ def async_register_supervisor_in_dev_reg(
|
||||
params = DeviceInfo(
|
||||
identifiers={(DOMAIN, "supervisor")},
|
||||
manufacturer="Home Assistant",
|
||||
model=SupervisorEntityModel.SUPERVIOSR,
|
||||
model=SupervisorEntityModel.SUPERVISOR,
|
||||
sw_version=supervisor_dict[ATTR_VERSION],
|
||||
name="Home Assistant Supervisor",
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
|
||||
@@ -25,17 +25,12 @@ def _get_obj_holidays_and_language(
|
||||
selected_categories: list[str] | None,
|
||||
) -> tuple[HolidayBase, str]:
|
||||
"""Get the object for the requested country and year."""
|
||||
if selected_categories is None:
|
||||
categories = [PUBLIC]
|
||||
else:
|
||||
categories = [PUBLIC, *selected_categories]
|
||||
|
||||
obj_holidays = country_holidays(
|
||||
country,
|
||||
subdiv=province,
|
||||
years={dt_util.now().year, dt_util.now().year + 1},
|
||||
language=language,
|
||||
categories=categories,
|
||||
categories=selected_categories,
|
||||
)
|
||||
if language == "en":
|
||||
for lang in obj_holidays.supported_languages:
|
||||
@@ -45,7 +40,7 @@ def _get_obj_holidays_and_language(
|
||||
subdiv=province,
|
||||
years={dt_util.now().year, dt_util.now().year + 1},
|
||||
language=lang,
|
||||
categories=categories,
|
||||
categories=selected_categories,
|
||||
)
|
||||
language = lang
|
||||
break
|
||||
@@ -59,7 +54,7 @@ def _get_obj_holidays_and_language(
|
||||
subdiv=province,
|
||||
years={dt_util.now().year, dt_util.now().year + 1},
|
||||
language=default_language,
|
||||
categories=categories,
|
||||
categories=selected_categories,
|
||||
)
|
||||
language = default_language
|
||||
|
||||
@@ -77,6 +72,11 @@ async def async_setup_entry(
|
||||
categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES)
|
||||
language = hass.config.language
|
||||
|
||||
if categories is None:
|
||||
categories = [PUBLIC]
|
||||
else:
|
||||
categories = [PUBLIC, *categories]
|
||||
|
||||
obj_holidays, language = await hass.async_add_executor_job(
|
||||
_get_obj_holidays_and_language, country, province, language, categories
|
||||
)
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.73", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.74", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -12,3 +12,13 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe
|
||||
authorize_url=OAUTH2_AUTHORIZE,
|
||||
token_url=OAUTH2_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Return description placeholders for the credentials dialog."""
|
||||
return {
|
||||
"developer_dashboard_url": "https://developer.home-connect.com/",
|
||||
"applications_url": "https://developer.home-connect.com/applications",
|
||||
"register_application_url": "https://developer.home-connect.com/application/add",
|
||||
"redirect_url": "https://my.home-assistant.io/redirect/oauth",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"application_credentials": {
|
||||
"description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values:\n * **Application ID**: Home Assistant (or any other name that makes sense)\n * **OAuth Flow**: Authorization Code Grant Flow\n * **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**."
|
||||
},
|
||||
"common": {
|
||||
"confirmed": "Confirmed",
|
||||
"present": "Present"
|
||||
@@ -13,7 +16,7 @@
|
||||
"description": "The Home Connect integration needs to re-authenticate your account"
|
||||
},
|
||||
"oauth_discovery": {
|
||||
"description": "Home Assistant has found a Home Connect device on your network. Press **Submit** to continue setting up Home Connect."
|
||||
"description": "Home Assistant has found a Home Connect device on your network. Be aware that the setup of Home Connect is more complicated than many other integrations. Press **Submit** to continue setting up Home Connect."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
import itertools as it
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -38,7 +38,6 @@ from homeassistant.helpers import (
|
||||
restore_state,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.service import (
|
||||
async_extract_config_entry_ids,
|
||||
@@ -402,46 +401,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
info = await async_get_system_info(hass)
|
||||
|
||||
installation_type = info["installation_type"][15:]
|
||||
deprecated_method = installation_type in {
|
||||
"Core",
|
||||
"Supervised",
|
||||
}
|
||||
arch = info["arch"]
|
||||
if arch == "armv7":
|
||||
if installation_type == "OS":
|
||||
# Local import to avoid circular dependencies
|
||||
# We use the import helper because hassio
|
||||
# may not be loaded yet and we don't want to
|
||||
# do blocking I/O in the event loop to import it.
|
||||
if TYPE_CHECKING:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components import hassio
|
||||
else:
|
||||
hassio = await async_import_module(
|
||||
hass, "homeassistant.components.hassio"
|
||||
)
|
||||
os_info = hassio.get_os_info(hass)
|
||||
assert os_info is not None
|
||||
issue_id = "deprecated_os_"
|
||||
board = os_info.get("board")
|
||||
if board in {"rpi3", "rpi4"}:
|
||||
issue_id += "aarch64"
|
||||
elif board in {"tinker", "odroid-xu4", "rpi2"}:
|
||||
issue_id += "armv7"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||
},
|
||||
)
|
||||
elif installation_type == "Container":
|
||||
if installation_type in {"Core", "Container"}:
|
||||
deprecated_method = installation_type == "Core"
|
||||
arch = info["arch"]
|
||||
if arch == "armv7" and installation_type == "Container":
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
@@ -452,29 +415,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_container_armv7",
|
||||
)
|
||||
deprecated_architecture = False
|
||||
if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method):
|
||||
deprecated_architecture = True
|
||||
if deprecated_method or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if deprecated_method:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": installation_type,
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
deprecated_architecture = False
|
||||
if arch in {"i386", "armhf"} or (
|
||||
arch == "armv7" and installation_type != "Container"
|
||||
):
|
||||
deprecated_architecture = True
|
||||
if deprecated_method or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if deprecated_method:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": installation_type,
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
},
|
||||
"deprecated_system_packages_config_flow_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue."
|
||||
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove all \"{integration_title}\" config entries."
|
||||
},
|
||||
"deprecated_system_packages_yaml_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove the {domain} entry from your configuration.yaml file and restart Home Assistant."
|
||||
},
|
||||
"historic_currency": {
|
||||
"title": "The configured currency is no longer in use",
|
||||
|
||||
@@ -83,7 +83,7 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
if ClimateEntityFeature.TURN_OFF in self.supported_features and (
|
||||
self._heating_mode is not None
|
||||
):
|
||||
if self._heating_mode.current_value == 0:
|
||||
if self._heating_mode.current_value == self._heating_mode.minimum:
|
||||
return HVACMode.OFF
|
||||
|
||||
return HVACMode.HEAT
|
||||
@@ -91,7 +91,10 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
"""Return the hvac action."""
|
||||
if self._heating_mode is not None and self._heating_mode.current_value == 0:
|
||||
if (
|
||||
self._heating_mode is not None
|
||||
and self._heating_mode.current_value == self._heating_mode.minimum
|
||||
):
|
||||
return HVACAction.OFF
|
||||
|
||||
if (
|
||||
@@ -110,10 +113,12 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
if (
|
||||
ClimateEntityFeature.PRESET_MODE in self.supported_features
|
||||
and self._heating_mode is not None
|
||||
and self._heating_mode.current_value > 0
|
||||
and self._heating_mode.current_value > self._heating_mode.minimum
|
||||
):
|
||||
assert self._attr_preset_modes is not None
|
||||
return self._attr_preset_modes[int(self._heating_mode.current_value) - 1]
|
||||
return self._attr_preset_modes[
|
||||
int(self._heating_mode.current_value - self._heating_mode.minimum) - 1
|
||||
]
|
||||
|
||||
return PRESET_NONE
|
||||
|
||||
@@ -147,14 +152,16 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
# Currently only HEAT and OFF are supported.
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, float(hvac_mode == HVACMode.HEAT)
|
||||
self._heating_mode,
|
||||
(hvac_mode == HVACMode.HEAT) + self._heating_mode.minimum,
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
assert self._heating_mode is not None and self._attr_preset_modes is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, self._attr_preset_modes.index(preset_mode) + 1
|
||||
self._heating_mode,
|
||||
self._attr_preset_modes.index(preset_mode) + self._heating_mode.minimum + 1,
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
@@ -168,12 +175,16 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(self._heating_mode, 1)
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, 1 + self._heating_mode.minimum
|
||||
)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(self._heating_mode, 0)
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, 0 + self._heating_mode.minimum
|
||||
)
|
||||
|
||||
|
||||
def get_climate_features(
|
||||
@@ -193,7 +204,10 @@ def get_climate_features(
|
||||
if attribute.maximum > 1:
|
||||
# Node supports more modes than off and heating.
|
||||
features |= ClimateEntityFeature.PRESET_MODE
|
||||
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
|
||||
if attribute.maximum < 5:
|
||||
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
|
||||
else:
|
||||
preset_modes.extend([PRESET_ECO])
|
||||
|
||||
if len(preset_modes) > 0:
|
||||
preset_modes.insert(0, PRESET_NONE)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Diagnostics for homee integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from . import DOMAIN, HomeeConfigEntry
|
||||
|
||||
TO_REDACT = [CONF_PASSWORD, CONF_USERNAME, "latitude", "longitude", "wlan_ssid"]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: HomeeConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
||||
"settings": async_redact_data(entry.runtime_data.settings.raw_data, TO_REDACT),
|
||||
"devices": [{"node": node.raw_data} for node in entry.runtime_data.nodes],
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: HomeeConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
|
||||
# Extract node_id from the device identifiers
|
||||
split_uid = next(
|
||||
identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN
|
||||
).split("-")
|
||||
# Homee hub itself only has MAC as identifier and a node_id of -1
|
||||
node_id = -1 if len(split_uid) < 2 else split_uid[1]
|
||||
|
||||
node = entry.runtime_data.get_node_by_id(int(node_id))
|
||||
assert node is not None
|
||||
return {
|
||||
"homee node": node.raw_data,
|
||||
}
|
||||
@@ -31,6 +31,22 @@ class HomeeNumberEntityDescription(NumberEntityDescription):
|
||||
|
||||
|
||||
NUMBER_DESCRIPTIONS = {
|
||||
AttributeType.BUTTON_BRIGHTNESS_ACTIVE: HomeeNumberEntityDescription(
|
||||
key="button_brightness_active",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.BUTTON_BRIGHTNESS_DIMMED: HomeeNumberEntityDescription(
|
||||
key="button_brightness_dimmed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.DISPLAY_BRIGHTNESS_ACTIVE: HomeeNumberEntityDescription(
|
||||
key="display_brightness_active",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.DISPLAY_BRIGHTNESS_DIMMED: HomeeNumberEntityDescription(
|
||||
key="display_brightness_dimmed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.DOWN_POSITION: HomeeNumberEntityDescription(
|
||||
key="down_position",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -48,6 +64,14 @@ NUMBER_DESCRIPTIONS = {
|
||||
key="endposition_configuration",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.EXTERNAL_TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
|
||||
key="external_temperature_offset",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.FLOOR_TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
|
||||
key="floor_temperature_offset",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.MOTION_ALARM_CANCELATION_DELAY: HomeeNumberEntityDescription(
|
||||
key="motion_alarm_cancelation_delay",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
@@ -83,6 +107,11 @@ NUMBER_DESCRIPTIONS = {
|
||||
key="temperature_offset",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.TEMPERATURE_REPORT_INTERVAL: HomeeNumberEntityDescription(
|
||||
key="temperature_report_interval",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.UP_TIME: HomeeNumberEntityDescription(
|
||||
key="up_time",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
|
||||
@@ -14,6 +14,11 @@ from .entity import HomeeEntity
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
SELECT_DESCRIPTIONS: dict[AttributeType, SelectEntityDescription] = {
|
||||
AttributeType.DISPLAY_TEMPERATURE_SELECTION: SelectEntityDescription(
|
||||
key="display_temperature_selection",
|
||||
options=["target", "current"],
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.REPEATER_MODE: SelectEntityDescription(
|
||||
key="repeater_mode",
|
||||
options=["off", "level1", "level2"],
|
||||
|
||||
@@ -129,6 +129,16 @@ SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
AttributeType.EXTERNAL_TEMPERATURE: HomeeSensorEntityDescription(
|
||||
key="external_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AttributeType.FLOOR_TEMPERATURE: HomeeSensorEntityDescription(
|
||||
key="floor_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AttributeType.INDOOR_RELATIVE_HUMIDITY: HomeeSensorEntityDescription(
|
||||
key="indoor_humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
|
||||
@@ -199,6 +199,18 @@
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"button_brightness_active": {
|
||||
"name": "Button brightness (active)"
|
||||
},
|
||||
"button_brightness_dimmed": {
|
||||
"name": "Button brightness (dimmed)"
|
||||
},
|
||||
"display_brightness_active": {
|
||||
"name": "Display brightness (active)"
|
||||
},
|
||||
"display_brightness_dimmed": {
|
||||
"name": "Display brightness (dimmed)"
|
||||
},
|
||||
"down_position": {
|
||||
"name": "Down position"
|
||||
},
|
||||
@@ -211,6 +223,12 @@
|
||||
"endposition_configuration": {
|
||||
"name": "End position"
|
||||
},
|
||||
"external_temperature_offset": {
|
||||
"name": "External temperature offset"
|
||||
},
|
||||
"floor_temperature_offset": {
|
||||
"name": "Floor temperature offset"
|
||||
},
|
||||
"motion_alarm_cancelation_delay": {
|
||||
"name": "Motion alarm delay"
|
||||
},
|
||||
@@ -235,6 +253,9 @@
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
},
|
||||
"temperature_report_interval": {
|
||||
"name": "Temperature report interval"
|
||||
},
|
||||
"up_time": {
|
||||
"name": "Up-movement duration"
|
||||
},
|
||||
@@ -246,6 +267,13 @@
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"display_temperature_selection": {
|
||||
"name": "Displayed temperature",
|
||||
"state": {
|
||||
"target": "Target",
|
||||
"current": "Measured"
|
||||
}
|
||||
},
|
||||
"repeater_mode": {
|
||||
"name": "Repeater mode",
|
||||
"state": {
|
||||
@@ -277,6 +305,12 @@
|
||||
"exhaust_motor_revs": {
|
||||
"name": "Exhaust motor speed"
|
||||
},
|
||||
"external_temperature": {
|
||||
"name": "External temperature"
|
||||
},
|
||||
"floor_temperature": {
|
||||
"name": "Floor temperature"
|
||||
},
|
||||
"indoor_humidity": {
|
||||
"name": "Indoor humidity"
|
||||
},
|
||||
|
||||
@@ -112,6 +112,7 @@ class HomematicipHAP:
|
||||
self.config_entry = config_entry
|
||||
|
||||
self._ws_close_requested = False
|
||||
self._ws_connection_closed = asyncio.Event()
|
||||
self._retry_task: asyncio.Task | None = None
|
||||
self._tries = 0
|
||||
self._accesspoint_connected = True
|
||||
@@ -218,6 +219,8 @@ class HomematicipHAP:
|
||||
try:
|
||||
await self.home.get_current_state_async()
|
||||
hmip_events = self.home.enable_events()
|
||||
self.home.set_on_connected_handler(self.ws_connected_handler)
|
||||
self.home.set_on_disconnected_handler(self.ws_disconnected_handler)
|
||||
tries = 0
|
||||
await hmip_events
|
||||
except HmipConnectionError:
|
||||
@@ -267,6 +270,18 @@ class HomematicipHAP:
|
||||
"Reset connection to access point id %s", self.config_entry.unique_id
|
||||
)
|
||||
|
||||
async def ws_connected_handler(self) -> None:
|
||||
"""Handle websocket connected."""
|
||||
_LOGGER.debug("WebSocket connection to HomematicIP established")
|
||||
if self._ws_connection_closed.is_set():
|
||||
await self.get_state()
|
||||
self._ws_connection_closed.clear()
|
||||
|
||||
async def ws_disconnected_handler(self) -> None:
|
||||
"""Handle websocket disconnection."""
|
||||
_LOGGER.warning("WebSocket connection to HomematicIP closed")
|
||||
self._ws_connection_closed.set()
|
||||
|
||||
async def get_hap(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
@@ -290,6 +305,7 @@ class HomematicipHAP:
|
||||
raise HmipcConnectionError from err
|
||||
home.on_update(self.async_update)
|
||||
home.on_create(self.async_create_entity)
|
||||
|
||||
hass.loop.create_task(self.async_connect())
|
||||
|
||||
return home
|
||||
|
||||
@@ -4,16 +4,16 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState
|
||||
from homematicip.base.enums import DeviceType, OpticalSignalBehaviour, RGBColorState
|
||||
from homematicip.base.functionalChannels import NotificationLightChannel
|
||||
from homematicip.device import (
|
||||
BrandDimmer,
|
||||
BrandSwitchMeasuring,
|
||||
BrandSwitchNotificationLight,
|
||||
Dimmer,
|
||||
DinRailDimmer3,
|
||||
FullFlushDimmer,
|
||||
PluggableDimmer,
|
||||
SwitchMeasuring,
|
||||
WiredDimmer3,
|
||||
)
|
||||
from packaging.version import Version
|
||||
@@ -44,9 +44,12 @@ async def async_setup_entry(
|
||||
hap = config_entry.runtime_data
|
||||
entities: list[HomematicipGenericEntity] = []
|
||||
for device in hap.home.devices:
|
||||
if isinstance(device, BrandSwitchMeasuring):
|
||||
if (
|
||||
isinstance(device, SwitchMeasuring)
|
||||
and getattr(device, "deviceType", None) == DeviceType.BRAND_SWITCH_MEASURING
|
||||
):
|
||||
entities.append(HomematicipLightMeasuring(hap, device))
|
||||
elif isinstance(device, BrandSwitchNotificationLight):
|
||||
if isinstance(device, BrandSwitchNotificationLight):
|
||||
device_version = Version(device.firmwareVersion)
|
||||
entities.append(HomematicipLight(hap, device))
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.1.1"]
|
||||
"requirements": ["homematicip==2.0.4"]
|
||||
}
|
||||
|
||||
@@ -11,12 +11,10 @@ from homematicip.base.functionalChannels import (
|
||||
FunctionalChannel,
|
||||
)
|
||||
from homematicip.device import (
|
||||
BrandSwitchMeasuring,
|
||||
EnergySensorsInterface,
|
||||
FloorTerminalBlock6,
|
||||
FloorTerminalBlock10,
|
||||
FloorTerminalBlock12,
|
||||
FullFlushSwitchMeasuring,
|
||||
HeatingThermostat,
|
||||
HeatingThermostatCompact,
|
||||
HeatingThermostatEvo,
|
||||
@@ -26,9 +24,9 @@ from homematicip.device import (
|
||||
MotionDetectorOutdoor,
|
||||
MotionDetectorPushButton,
|
||||
PassageDetector,
|
||||
PlugableSwitchMeasuring,
|
||||
PresenceDetectorIndoor,
|
||||
RoomControlDeviceAnalog,
|
||||
SwitchMeasuring,
|
||||
TemperatureDifferenceSensor2,
|
||||
TemperatureHumiditySensorDisplay,
|
||||
TemperatureHumiditySensorOutdoor,
|
||||
@@ -143,14 +141,7 @@ async def async_setup_entry(
|
||||
),
|
||||
):
|
||||
entities.append(HomematicipIlluminanceSensor(hap, device))
|
||||
if isinstance(
|
||||
device,
|
||||
(
|
||||
PlugableSwitchMeasuring,
|
||||
BrandSwitchMeasuring,
|
||||
FullFlushSwitchMeasuring,
|
||||
),
|
||||
):
|
||||
if isinstance(device, SwitchMeasuring):
|
||||
entities.append(HomematicipPowerSensor(hap, device))
|
||||
entities.append(HomematicipEnergySensor(hap, device))
|
||||
if isinstance(device, (WeatherSensor, WeatherSensorPlus, WeatherSensorPro)):
|
||||
|
||||
@@ -4,20 +4,19 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homematicip.base.enums import DeviceType
|
||||
from homematicip.device import (
|
||||
BrandSwitch2,
|
||||
BrandSwitchMeasuring,
|
||||
DinRailSwitch,
|
||||
DinRailSwitch4,
|
||||
FullFlushInputSwitch,
|
||||
FullFlushSwitchMeasuring,
|
||||
HeatingSwitch2,
|
||||
MultiIOBox,
|
||||
OpenCollector8Module,
|
||||
PlugableSwitch,
|
||||
PlugableSwitchMeasuring,
|
||||
PrintedCircuitBoardSwitch2,
|
||||
PrintedCircuitBoardSwitchBattery,
|
||||
SwitchMeasuring,
|
||||
WiredSwitch8,
|
||||
)
|
||||
from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup
|
||||
@@ -43,12 +42,10 @@ async def async_setup_entry(
|
||||
if isinstance(group, (ExtendedLinkedSwitchingGroup, SwitchingGroup))
|
||||
]
|
||||
for device in hap.home.devices:
|
||||
if isinstance(device, BrandSwitchMeasuring):
|
||||
# BrandSwitchMeasuring inherits PlugableSwitchMeasuring
|
||||
# This entity is implemented in the light platform and will
|
||||
# not be added in the switch platform
|
||||
pass
|
||||
elif isinstance(device, (PlugableSwitchMeasuring, FullFlushSwitchMeasuring)):
|
||||
if (
|
||||
isinstance(device, SwitchMeasuring)
|
||||
and getattr(device, "deviceType", None) != DeviceType.BRAND_SWITCH_MEASURING
|
||||
):
|
||||
entities.append(HomematicipSwitchMeasuring(hap, device))
|
||||
elif isinstance(device, WiredSwitch8):
|
||||
entities.extend(
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from .bridge import HueBridge, HueConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .migration import check_migration
|
||||
from .services import async_register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Hue integration."""
|
||||
|
||||
async_register_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from .const import (
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def async_register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for Hue integration."""
|
||||
|
||||
async def hue_activate_scene(call: ServiceCall, skip_reload=True) -> None:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2025.3.0"]
|
||||
"requirements": ["pydrawise==2025.6.0"]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ from .const import (
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION,
|
||||
)
|
||||
from .services import register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
@@ -28,7 +28,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up iCloud integration."""
|
||||
|
||||
register_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -115,8 +115,8 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount:
|
||||
return icloud_account
|
||||
|
||||
|
||||
def register_services(hass: HomeAssistant) -> None:
|
||||
"""Set up an iCloud account from a config entry."""
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register iCloud services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ICLOUD_PLAY_SOUND, play_sound, schema=SERVICE_SCHEMA_PLAY_SOUND
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Imeon inverter base class for entities."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import InverterCoordinator
|
||||
|
||||
type InverterConfigEntry = ConfigEntry[InverterCoordinator]
|
||||
|
||||
|
||||
class InverterEntity(CoordinatorEntity[InverterCoordinator]):
|
||||
"""Common elements for all entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: InverterCoordinator,
|
||||
entry: InverterConfigEntry,
|
||||
entity_description: EntityDescription,
|
||||
) -> None:
|
||||
"""Pass coordinator to CoordinatorEntity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._inverter = coordinator.api.inverter
|
||||
self.data_key = entity_description.key
|
||||
assert entry.unique_id
|
||||
self._attr_unique_id = f"{entry.unique_id}_{self.data_key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.unique_id)},
|
||||
name="Imeon inverter",
|
||||
manufacturer="Imeon Energy",
|
||||
model=self._inverter.get("inverter"),
|
||||
sw_version=self._inverter.get("software"),
|
||||
serial_number=self._inverter.get("serial"),
|
||||
configuration_url=self._inverter.get("url"),
|
||||
)
|
||||
@@ -21,20 +21,18 @@ from homeassistant.const import (
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import InverterCoordinator
|
||||
from .entity import InverterEntity
|
||||
|
||||
type InverterConfigEntry = ConfigEntry[InverterCoordinator]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS = (
|
||||
SENSOR_DESCRIPTIONS = (
|
||||
# Battery
|
||||
SensorEntityDescription(
|
||||
key="battery_autonomy",
|
||||
@@ -423,42 +421,18 @@ async def async_setup_entry(
|
||||
"""Create each sensor for a given config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Init sensor entities
|
||||
async_add_entities(
|
||||
InverterSensor(coordinator, entry, description)
|
||||
for description in ENTITY_DESCRIPTIONS
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class InverterSensor(CoordinatorEntity[InverterCoordinator], SensorEntity):
|
||||
"""A sensor that returns numerical values with units."""
|
||||
class InverterSensor(InverterEntity, SensorEntity):
|
||||
"""Representation of an Imeon inverter sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: InverterCoordinator,
|
||||
entry: InverterConfigEntry,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Pass coordinator to CoordinatorEntity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._inverter = coordinator.api.inverter
|
||||
self.data_key = description.key
|
||||
assert entry.unique_id
|
||||
self._attr_unique_id = f"{entry.unique_id}_{self.data_key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.unique_id)},
|
||||
name="Imeon inverter",
|
||||
manufacturer="Imeon Energy",
|
||||
model=self._inverter.get("inverter"),
|
||||
sw_version=self._inverter.get("software"),
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | None:
|
||||
"""Value of the sensor."""
|
||||
"""Return the state of the entity."""
|
||||
return self.coordinator.data.get(self.data_key)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioimmich==0.9.0"]
|
||||
"requirements": ["aioimmich==0.9.1"]
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
INSTEON_PLATFORMS,
|
||||
)
|
||||
from .services import async_register_services
|
||||
from .services import async_setup_services
|
||||
from .utils import (
|
||||
add_insteon_events,
|
||||
get_device_platforms,
|
||||
@@ -145,7 +145,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
_LOGGER.debug("Insteon device count: %s", len(devices))
|
||||
register_new_device_callback(hass)
|
||||
async_register_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
create_insteon_device(hass, devices.modem, entry.entry_id)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
"""Register services used by insteon component."""
|
||||
|
||||
save_lock = asyncio.Lock()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyiqvia"],
|
||||
"requirements": ["numpy==2.2.6", "pyiqvia==2022.04.0"]
|
||||
"requirements": ["numpy==2.3.0", "pyiqvia==2022.04.0"]
|
||||
}
|
||||
|
||||
@@ -329,8 +329,8 @@ class JellyfinSource(MediaSource):
|
||||
movies = await self._get_children(library_id, ITEM_TYPE_MOVIE)
|
||||
movies = sorted(
|
||||
movies,
|
||||
# Sort by whether a movies has an name first, then by name
|
||||
# This allows for sorting moveis with, without and with missing names
|
||||
# Sort by whether a movie has a name first, then by name
|
||||
# This allows for sorting movies with, without and with missing names
|
||||
key=lambda k: (
|
||||
ITEM_KEY_NAME not in k,
|
||||
k.get(ITEM_KEY_NAME),
|
||||
@@ -388,7 +388,7 @@ class JellyfinSource(MediaSource):
|
||||
series = await self._get_children(library_id, ITEM_TYPE_SERIES)
|
||||
series = sorted(
|
||||
series,
|
||||
# Sort by whether a seroes has an name first, then by name
|
||||
# Sort by whether a series has a name first, then by name
|
||||
# This allows for sorting series with, without and with missing names
|
||||
key=lambda k: (
|
||||
ITEM_KEY_NAME not in k,
|
||||
|
||||
@@ -225,7 +225,7 @@ async def async_setup_entry(
|
||||
JewishCalendarTimeSensor(config_entry, description)
|
||||
for description in TIME_SENSORS
|
||||
)
|
||||
async_add_entities(sensors)
|
||||
async_add_entities(sensors, update_before_add=True)
|
||||
|
||||
|
||||
class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
|
||||
@@ -233,12 +233,7 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self.async_update_data()
|
||||
|
||||
async def async_update_data(self) -> None:
|
||||
async def async_update(self) -> None:
|
||||
"""Update the state of the sensor."""
|
||||
now = dt_util.now()
|
||||
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
|
||||
|
||||
@@ -59,7 +59,7 @@ from .helpers import (
|
||||
register_lcn_address_devices,
|
||||
register_lcn_host_device,
|
||||
)
|
||||
from .services import register_services
|
||||
from .services import async_setup_services
|
||||
from .websocket import register_panel_and_ws_api
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the LCN component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
await register_services(hass)
|
||||
async_setup_services(hass)
|
||||
await register_panel_and_ws_api(hass)
|
||||
|
||||
return True
|
||||
|
||||
@@ -438,7 +438,7 @@ SERVICES = (
|
||||
)
|
||||
|
||||
|
||||
async def register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for LCN."""
|
||||
for service_name, service in SERVICES:
|
||||
hass.services.async_register(
|
||||
|
||||
@@ -31,6 +31,9 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle Zeroconf discovery."""
|
||||
|
||||
# Do not probe the device if the host is already configured
|
||||
self._async_abort_entries_match({CONF_HOST: discovery_info.host})
|
||||
|
||||
session: ClientSession = await async_get_client_session(self.hass)
|
||||
bridge: LinkPlayBridge | None = None
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.9"],
|
||||
"requirements": ["python-linkplay==0.2.10"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ from homeassistant.helpers.json import save_json
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.json import JsonObjectType, load_json_object
|
||||
|
||||
from .const import DOMAIN, FORMAT_HTML, FORMAT_TEXT, SERVICE_SEND_MESSAGE
|
||||
from .const import ATTR_FORMAT, ATTR_IMAGES, CONF_ROOMS_REGEX, DOMAIN, FORMAT_HTML
|
||||
from .services import register_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,17 +58,11 @@ CONF_WORD: Final = "word"
|
||||
CONF_EXPRESSION: Final = "expression"
|
||||
|
||||
CONF_USERNAME_REGEX = "^@[^:]*:.*"
|
||||
CONF_ROOMS_REGEX = "^[!|#][^:]*:.*"
|
||||
|
||||
EVENT_MATRIX_COMMAND = "matrix_command"
|
||||
|
||||
DEFAULT_CONTENT_TYPE = "application/octet-stream"
|
||||
|
||||
MESSAGE_FORMATS = [FORMAT_HTML, FORMAT_TEXT]
|
||||
DEFAULT_MESSAGE_FORMAT = FORMAT_TEXT
|
||||
|
||||
ATTR_FORMAT = "format" # optional message format
|
||||
ATTR_IMAGES = "images" # optional images
|
||||
|
||||
WordCommand = NewType("WordCommand", str)
|
||||
ExpressionCommand = NewType("ExpressionCommand", re.Pattern)
|
||||
@@ -117,27 +112,12 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MESSAGE): cv.string,
|
||||
vol.Optional(ATTR_DATA, default={}): {
|
||||
vol.Optional(ATTR_FORMAT, default=DEFAULT_MESSAGE_FORMAT): vol.In(
|
||||
MESSAGE_FORMATS
|
||||
),
|
||||
vol.Optional(ATTR_IMAGES): vol.All(cv.ensure_list, [cv.string]),
|
||||
},
|
||||
vol.Required(ATTR_TARGET): vol.All(
|
||||
cv.ensure_list, [cv.matches_regex(CONF_ROOMS_REGEX)]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Matrix bot component."""
|
||||
config = config[DOMAIN]
|
||||
|
||||
matrix_bot = MatrixBot(
|
||||
hass.data[DOMAIN] = MatrixBot(
|
||||
hass,
|
||||
os.path.join(hass.config.path(), SESSION_FILE),
|
||||
config[CONF_HOMESERVER],
|
||||
@@ -147,14 +127,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
config[CONF_ROOMS],
|
||||
config[CONF_COMMANDS],
|
||||
)
|
||||
hass.data[DOMAIN] = matrix_bot
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_MESSAGE,
|
||||
matrix_bot.handle_send_message,
|
||||
schema=SERVICE_SCHEMA_SEND_MESSAGE,
|
||||
)
|
||||
register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -6,3 +6,8 @@ SERVICE_SEND_MESSAGE = "send_message"
|
||||
|
||||
FORMAT_HTML = "html"
|
||||
FORMAT_TEXT = "text"
|
||||
|
||||
ATTR_FORMAT = "format" # optional message format
|
||||
ATTR_IMAGES = "images" # optional images
|
||||
|
||||
CONF_ROOMS_REGEX = "^[!|#][^:]*:.*"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""The Matrix bot component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
ATTR_FORMAT,
|
||||
ATTR_IMAGES,
|
||||
CONF_ROOMS_REGEX,
|
||||
DOMAIN,
|
||||
FORMAT_HTML,
|
||||
FORMAT_TEXT,
|
||||
SERVICE_SEND_MESSAGE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MatrixBot
|
||||
|
||||
|
||||
MESSAGE_FORMATS = [FORMAT_HTML, FORMAT_TEXT]
|
||||
DEFAULT_MESSAGE_FORMAT = FORMAT_TEXT
|
||||
|
||||
|
||||
SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MESSAGE): cv.string,
|
||||
vol.Optional(ATTR_DATA, default={}): {
|
||||
vol.Optional(ATTR_FORMAT, default=DEFAULT_MESSAGE_FORMAT): vol.In(
|
||||
MESSAGE_FORMATS
|
||||
),
|
||||
vol.Optional(ATTR_IMAGES): vol.All(cv.ensure_list, [cv.string]),
|
||||
},
|
||||
vol.Required(ATTR_TARGET): vol.All(
|
||||
cv.ensure_list, [cv.matches_regex(CONF_ROOMS_REGEX)]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def _handle_send_message(call: ServiceCall) -> None:
|
||||
"""Handle the send_message service call."""
|
||||
matrix_bot: MatrixBot = call.hass.data[DOMAIN]
|
||||
await matrix_bot.handle_send_message(call)
|
||||
|
||||
|
||||
def register_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the Matrix bot component."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_MESSAGE,
|
||||
_handle_send_message,
|
||||
schema=SERVICE_SCHEMA_SEND_MESSAGE,
|
||||
)
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mealie",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aiomealie==0.9.5"]
|
||||
"requirements": ["aiomealie==0.9.6"]
|
||||
}
|
||||
|
||||
@@ -527,6 +527,7 @@ OVEN_PROGRAM_ID: dict[int, str] = {
|
||||
116: "custom_program_20",
|
||||
323: "pyrolytic",
|
||||
326: "descale",
|
||||
327: "evaporate_water",
|
||||
335: "shabbat_program",
|
||||
336: "yom_tov",
|
||||
356: "defrost",
|
||||
|
||||
@@ -542,6 +542,7 @@
|
||||
"endive_strips": "Endive (strips)",
|
||||
"espresso": "Espresso",
|
||||
"espresso_macchiato": "Espresso macchiato",
|
||||
"evaporate_water": "Evaporate water",
|
||||
"express": "Express",
|
||||
"express_20": "Express 20'",
|
||||
"extra_quiet": "Extra quiet",
|
||||
|
||||
@@ -293,8 +293,8 @@ class MyUplinkDevicePointSensor(MyUplinkEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Sensor state value."""
|
||||
device_point = self.coordinator.data.points[self.device_id][self.point_id]
|
||||
if device_point.value == MARKER_FOR_UNKNOWN_VALUE:
|
||||
device_point = self.coordinator.data.points[self.device_id].get(self.point_id)
|
||||
if device_point is None or device_point.value == MARKER_FOR_UNKNOWN_VALUE:
|
||||
return None
|
||||
return device_point.value # type: ignore[no-any-return]
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/neato",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pybotvac"],
|
||||
"requirements": ["pybotvac==0.0.26"]
|
||||
"requirements": ["pybotvac==0.0.28"]
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.util import get_random_string
|
||||
|
||||
from . import api
|
||||
@@ -441,10 +440,3 @@ class NestFlowHandler(
|
||||
if self._structure_config_title:
|
||||
title = self._structure_config_title
|
||||
return self.async_create_entry(title=title, data=self._data)
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by discovery."""
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
return await self.async_step_user()
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The Nest integration needs to re-authenticate your account"
|
||||
},
|
||||
"oauth_discovery": {
|
||||
"description": "Home Assistant has found a Google Nest device on your network. Be aware that the set up of Google Nest is more complicated than most other integrations and requires setting up a Nest Device Access project which **requires paying Google a US $5 fee**. Press **Submit** to continue setting up Google Nest."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""NextBus data update coordinator."""
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from py_nextbus import NextBusClient
|
||||
from py_nextbus.client import NextBusFormatError, NextBusHTTPError
|
||||
@@ -15,8 +15,14 @@ from .util import RouteStop
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# At what percentage of the request limit should the coordinator pause making requests
|
||||
UPDATE_INTERVAL_SECONDS = 30
|
||||
THROTTLE_PRECENTAGE = 80
|
||||
|
||||
class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
class NextBusDataUpdateCoordinator(
|
||||
DataUpdateCoordinator[dict[RouteStop, dict[str, Any]]]
|
||||
):
|
||||
"""Class to manage fetching NextBus data."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, agency: str) -> None:
|
||||
@@ -26,7 +32,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
_LOGGER,
|
||||
config_entry=None, # It is shared between multiple entries
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=30),
|
||||
update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS),
|
||||
)
|
||||
self.client = NextBusClient(agency_id=agency)
|
||||
self._agency = agency
|
||||
@@ -49,9 +55,26 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Check if this coordinator is tracking any routes."""
|
||||
return len(self._route_stops) > 0
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[RouteStop, dict[str, Any]]:
|
||||
"""Fetch data from NextBus."""
|
||||
|
||||
if (
|
||||
# If we have predictions, check the rate limit
|
||||
self._predictions
|
||||
# If are over our rate limit percentage, we should throttle
|
||||
and self.client.rate_limit_percent >= THROTTLE_PRECENTAGE
|
||||
# But only if we have a reset time to unthrottle
|
||||
and self.client.rate_limit_reset is not None
|
||||
# Unless we are after the reset time
|
||||
and datetime.now() < self.client.rate_limit_reset
|
||||
):
|
||||
self.logger.debug(
|
||||
"Rate limit threshold reached. Skipping updates for. Routes: %s",
|
||||
str(self._route_stops),
|
||||
)
|
||||
return self._predictions
|
||||
|
||||
_stops_to_route_stops: dict[str, set[RouteStop]] = {}
|
||||
for route_stop in self._route_stops:
|
||||
_stops_to_route_stops.setdefault(route_stop.stop_id, set()).add(route_stop)
|
||||
@@ -60,7 +83,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"Updating data from API. Routes: %s", str(_stops_to_route_stops)
|
||||
)
|
||||
|
||||
def _update_data() -> dict:
|
||||
def _update_data() -> dict[RouteStop, dict[str, Any]]:
|
||||
"""Fetch data from NextBus."""
|
||||
self.logger.debug("Updating data from API (executor)")
|
||||
predictions: dict[RouteStop, dict[str, Any]] = {}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynordpool"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pynordpool==0.2.4"],
|
||||
"requirements": ["pynordpool==0.3.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DATA_COORDINATOR, DATA_UNDO_UPDATE_LISTENER, DOMAIN
|
||||
from .coordinator import NZBGetDataUpdateCoordinator
|
||||
from .services import async_register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||
@@ -17,7 +17,7 @@ PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up NZBGet integration."""
|
||||
|
||||
async_register_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def set_speed(call: ServiceCall) -> None:
|
||||
_get_coordinator(call).nzbget.rate(call.data[ATTR_SPEED])
|
||||
|
||||
|
||||
def async_register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register integration-level services."""
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_PAUSE, pause, schema=vol.Schema({}))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user