mirror of
https://github.com/home-assistant/core.git
synced 2025-07-31 19:25:12 +02:00
Merge branch 'dev' into epenet-20250527-1510
This commit is contained in:
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.18
|
||||
uses: github/codeql-action/init@v3.28.19
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.18
|
||||
uses: github/codeql-action/analyze@v3.28.19
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
@@ -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."""
|
||||
|
||||
|
89
homeassistant/components/abode/services.py
Normal file
89
homeassistant/components/abode/services.py
Normal file
@@ -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,
|
||||
)
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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
|
||||
),
|
||||
|
@@ -118,5 +118,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.0.5"]
|
||||
"requirements": ["aioamazondevices==3.0.6"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
|
61
homeassistant/components/amcrest/services.py
Normal file
61
homeassistant/components/amcrest/services.py
Normal file
@@ -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])
|
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.48.2"
|
||||
"habluetooth==3.49.0"
|
||||
]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -1 +1,6 @@
|
||||
"""The eddystone_temperature component."""
|
||||
|
||||
DOMAIN = "eddystone_temperature"
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
@@ -23,17 +23,18 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_BT_DEVICE_ID = "bt_device_id"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
|
||||
BEACON_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -58,6 +59,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Validate configuration, create devices and start monitoring thread."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Eddystone",
|
||||
},
|
||||
)
|
||||
|
||||
bt_device_id: int = config[CONF_BT_DEVICE_ID]
|
||||
|
||||
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
|
||||
|
@@ -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
|
||||
)
|
||||
|
77
homeassistant/components/elkm1/services.py
Normal file
77
homeassistant/components/elkm1/services.py
Normal file
@@ -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)
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==31.1.0",
|
||||
"aioesphomeapi==32.2.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
|
@@ -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
|
||||
|
9
homeassistant/components/ffmpeg/const.py
Normal file
9
homeassistant/components/ffmpeg/const.py
Normal file
@@ -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")
|
51
homeassistant/components/ffmpeg/services.py
Normal file
51
homeassistant/components/ffmpeg/services.py
Normal file
@@ -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
|
||||
)
|
@@ -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,
|
||||
)
|
||||
|
87
homeassistant/components/google_sheets/services.py
Normal file
87
homeassistant/components/google_sheets/services.py
Normal file
@@ -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,
|
||||
)
|
@@ -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"]
|
||||
}
|
||||
|
@@ -83,3 +83,54 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the reconfigure flow."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input:
|
||||
self.homee = Homee(
|
||||
user_input[CONF_HOST],
|
||||
reconfigure_entry.data[CONF_USERNAME],
|
||||
reconfigure_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
await self.homee.wait_until_connected()
|
||||
self.homee.disconnect()
|
||||
await self.homee.wait_until_disconnected()
|
||||
|
||||
await self.async_set_unique_id(self.homee.settings.uid)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_hub")
|
||||
|
||||
_LOGGER.debug("Updated homee entry with ID %s", self.homee.settings.uid)
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(), data_updates=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_HOST, default=reconfigure_entry.data[CONF_HOST]
|
||||
): str
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"name": reconfigure_entry.runtime_data.settings.uid
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
43
homeassistant/components/homee/diagnostics.py
Normal file
43
homeassistant/components/homee/diagnostics.py
Normal file
@@ -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,
|
||||
}
|
@@ -2,7 +2,9 @@
|
||||
"config": {
|
||||
"flow_title": "homee {name} ({host})",
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"wrong_hub": "Address belongs to a different homee."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -22,6 +24,16 @@
|
||||
"username": "The username for your homee.",
|
||||
"password": "The password for your homee."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure homee {name}",
|
||||
"description": "Reconfigure the IP address of your homee.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of your homee."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -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
|
||||
|
40
homeassistant/components/imeon_inverter/entity.py
Normal file
40
homeassistant/components/imeon_inverter/entity.py
Normal file
@@ -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.8.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"]
|
||||
}
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyiskra"],
|
||||
"requirements": ["pyiskra==0.1.19"]
|
||||
"requirements": ["pyiskra==0.1.21"]
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -131,7 +131,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.",
|
||||
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal digits expected.",
|
||||
"invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'",
|
||||
"invalid_ip_address": "Invalid IPv4 address.",
|
||||
"keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
|
||||
|
@@ -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 = "^[!|#][^:]*:.*"
|
||||
|
61
homeassistant/components/matrix/services.py
Normal file
61
homeassistant/components/matrix/services.py
Normal file
@@ -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,
|
||||
)
|
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/modbus",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymodbus"],
|
||||
"requirements": ["pymodbus==3.8.3"]
|
||||
"requirements": ["pymodbus==3.9.2"]
|
||||
}
|
||||
|
@@ -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({}))
|
||||
|
@@ -21,6 +21,7 @@ from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_NUM_CTX,
|
||||
CONF_PROMPT,
|
||||
CONF_THINK,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -33,6 +34,7 @@ __all__ = [
|
||||
"CONF_MODEL",
|
||||
"CONF_NUM_CTX",
|
||||
"CONF_PROMPT",
|
||||
"CONF_THINK",
|
||||
"CONF_URL",
|
||||
"DOMAIN",
|
||||
]
|
||||
|
@@ -22,6 +22,7 @@ from homeassistant.const import CONF_LLM_HASS_API, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
NumberSelector,
|
||||
NumberSelectorConfig,
|
||||
NumberSelectorMode,
|
||||
@@ -41,10 +42,12 @@ from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_NUM_CTX,
|
||||
CONF_PROMPT,
|
||||
CONF_THINK,
|
||||
DEFAULT_KEEP_ALIVE,
|
||||
DEFAULT_MAX_HISTORY,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_NUM_CTX,
|
||||
DEFAULT_THINK,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
MAX_NUM_CTX,
|
||||
@@ -280,6 +283,12 @@ def ollama_config_option_schema(
|
||||
min=-1, max=sys.maxsize, step=1, mode=NumberSelectorMode.BOX
|
||||
)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_THINK,
|
||||
description={
|
||||
"suggested_value": options.get("think", DEFAULT_THINK),
|
||||
},
|
||||
): BooleanSelector(),
|
||||
}
|
||||
|
||||
|
||||
|
@@ -4,6 +4,7 @@ DOMAIN = "ollama"
|
||||
|
||||
CONF_MODEL = "model"
|
||||
CONF_PROMPT = "prompt"
|
||||
CONF_THINK = "think"
|
||||
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
DEFAULT_KEEP_ALIVE = -1 # seconds. -1 = indefinite, 0 = never
|
||||
@@ -15,6 +16,7 @@ CONF_NUM_CTX = "num_ctx"
|
||||
DEFAULT_NUM_CTX = 8192
|
||||
MIN_NUM_CTX = 2048
|
||||
MAX_NUM_CTX = 131072
|
||||
DEFAULT_THINK = False
|
||||
|
||||
CONF_MAX_HISTORY = "max_history"
|
||||
DEFAULT_MAX_HISTORY = 20
|
||||
|
@@ -24,6 +24,7 @@ from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_NUM_CTX,
|
||||
CONF_PROMPT,
|
||||
CONF_THINK,
|
||||
DEFAULT_KEEP_ALIVE,
|
||||
DEFAULT_MAX_HISTORY,
|
||||
DEFAULT_NUM_CTX,
|
||||
@@ -256,6 +257,7 @@ class OllamaConversationEntity(
|
||||
# keep_alive requires specifying unit. In this case, seconds
|
||||
keep_alive=f"{settings.get(CONF_KEEP_ALIVE, DEFAULT_KEEP_ALIVE)}s",
|
||||
options={CONF_NUM_CTX: settings.get(CONF_NUM_CTX, DEFAULT_NUM_CTX)},
|
||||
think=settings.get(CONF_THINK),
|
||||
)
|
||||
except (ollama.RequestError, ollama.ResponseError) as err:
|
||||
_LOGGER.error("Unexpected error talking to Ollama server: %s", err)
|
||||
|
@@ -30,12 +30,14 @@
|
||||
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
|
||||
"max_history": "Max history messages",
|
||||
"num_ctx": "Context window size",
|
||||
"keep_alive": "Keep alive"
|
||||
"keep_alive": "Keep alive",
|
||||
"think": "Think before responding"
|
||||
},
|
||||
"data_description": {
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||
"keep_alive": "Duration in seconds for Ollama to keep model in memory. -1 = indefinite, 0 = never.",
|
||||
"num_ctx": "Maximum number of text tokens the model can process. Lower to reduce Ollama RAM, or increase for a large number of exposed entities."
|
||||
"num_ctx": "Maximum number of text tokens the model can process. Lower to reduce Ollama RAM, or increase for a large number of exposed entities.",
|
||||
"think": "If enabled, the LLM will think before responding. This can improve response quality but may increase latency."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ from .coordinator import (
|
||||
OneDriveRuntimeData,
|
||||
OneDriveUpdateCoordinator,
|
||||
)
|
||||
from .services import async_register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
@@ -44,7 +44,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the OneDrive integration."""
|
||||
async_register_services(hass)
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
|
@@ -70,7 +70,7 @@ def _read_file_contents(
|
||||
return results
|
||||
|
||||
|
||||
def async_register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register OneDrive services."""
|
||||
|
||||
async def async_handle_upload(call: ServiceCall) -> ServiceResponse:
|
||||
|
@@ -18,7 +18,7 @@ from .const import (
|
||||
ListeningMode,
|
||||
)
|
||||
from .receiver import Receiver, async_interview
|
||||
from .services import DATA_MP_ENTITIES, async_register_services
|
||||
from .services import DATA_MP_ENTITIES, async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,7 +41,7 @@ type OnkyoConfigEntry = ConfigEntry[OnkyoData]
|
||||
|
||||
async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
||||
"""Set up Onkyo component."""
|
||||
await async_register_services(hass)
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
|
@@ -40,7 +40,7 @@ ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema(
|
||||
SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output"
|
||||
|
||||
|
||||
async def async_register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register Onkyo services."""
|
||||
|
||||
hass.data.setdefault(DATA_MP_ENTITIES, {})
|
||||
|
@@ -5,7 +5,7 @@ from contextlib import suppress
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
from httpx import RequestError
|
||||
import aiohttp
|
||||
from onvif.exceptions import ONVIFError
|
||||
from onvif.util import is_auth_error, stringify_onvif_error
|
||||
from zeep.exceptions import Fault, TransportError
|
||||
@@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await device.async_setup()
|
||||
if not entry.data.get(CONF_SNAPSHOT_AUTH):
|
||||
await async_populate_snapshot_auth(hass, device, entry)
|
||||
except RequestError as err:
|
||||
except (TimeoutError, aiohttp.ClientError) as err:
|
||||
await device.device.close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not connect to camera {device.device.host}:{device.device.port}: {err}"
|
||||
@@ -119,7 +119,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if device.capabilities.events and device.events.started:
|
||||
try:
|
||||
await device.events.async_stop()
|
||||
except (ONVIFError, Fault, RequestError, TransportError):
|
||||
except (TimeoutError, ONVIFError, Fault, aiohttp.ClientError, TransportError):
|
||||
LOGGER.warning("Error while stopping events: %s", device.name)
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, device.platforms)
|
||||
|
@@ -1,8 +1,9 @@
|
||||
"""Constants for the onvif component."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from httpx import RequestError
|
||||
import aiohttp
|
||||
from onvif.exceptions import ONVIFError
|
||||
from zeep.exceptions import Fault, TransportError
|
||||
|
||||
@@ -48,4 +49,10 @@ SERVICE_PTZ = "ptz"
|
||||
|
||||
# Some cameras don't support the GetServiceCapabilities call
|
||||
# and will return a 404 error which is caught by TransportError
|
||||
GET_CAPABILITIES_EXCEPTIONS = (ONVIFError, Fault, RequestError, TransportError)
|
||||
GET_CAPABILITIES_EXCEPTIONS = (
|
||||
ONVIFError,
|
||||
Fault,
|
||||
aiohttp.ClientError,
|
||||
asyncio.TimeoutError,
|
||||
TransportError,
|
||||
)
|
||||
|
@@ -9,7 +9,7 @@ import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from httpx import RequestError
|
||||
import aiohttp
|
||||
import onvif
|
||||
from onvif import ONVIFCamera
|
||||
from onvif.exceptions import ONVIFError
|
||||
@@ -235,7 +235,7 @@ class ONVIFDevice:
|
||||
LOGGER.debug("%s: Retrieving current device date/time", self.name)
|
||||
try:
|
||||
device_time = await device_mgmt.GetSystemDateAndTime()
|
||||
except (RequestError, Fault) as err:
|
||||
except (TimeoutError, aiohttp.ClientError, Fault) as err:
|
||||
LOGGER.warning(
|
||||
"Couldn't get device '%s' date/time. Error: %s", self.name, err
|
||||
)
|
||||
@@ -303,7 +303,7 @@ class ONVIFDevice:
|
||||
# Set Date and Time ourselves if Date and Time is set manually in the camera.
|
||||
try:
|
||||
await self.async_manually_set_date_and_time()
|
||||
except (RequestError, TransportError, IndexError, Fault):
|
||||
except (TimeoutError, aiohttp.ClientError, TransportError, IndexError, Fault):
|
||||
LOGGER.warning("%s: Could not sync date/time on this camera", self.name)
|
||||
self._async_log_time_out_of_sync(cam_date_utc, system_date)
|
||||
|
||||
|
@@ -6,8 +6,8 @@ import asyncio
|
||||
from collections.abc import Callable
|
||||
import datetime as dt
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.web import Request
|
||||
from httpx import RemoteProtocolError, RequestError, TransportError
|
||||
from onvif import ONVIFCamera
|
||||
from onvif.client import (
|
||||
NotificationManager,
|
||||
@@ -16,7 +16,7 @@ from onvif.client import (
|
||||
)
|
||||
from onvif.exceptions import ONVIFError
|
||||
from onvif.util import stringify_onvif_error
|
||||
from zeep.exceptions import Fault, ValidationError, XMLParseError
|
||||
from zeep.exceptions import Fault, TransportError, ValidationError, XMLParseError
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -34,10 +34,23 @@ from .parsers import PARSERS
|
||||
UNHANDLED_TOPICS: set[str] = {"tns1:MediaControl/VideoEncoderConfiguration"}
|
||||
|
||||
SUBSCRIPTION_ERRORS = (Fault, TimeoutError, TransportError)
|
||||
CREATE_ERRORS = (ONVIFError, Fault, RequestError, XMLParseError, ValidationError)
|
||||
CREATE_ERRORS = (
|
||||
ONVIFError,
|
||||
Fault,
|
||||
aiohttp.ClientError,
|
||||
asyncio.TimeoutError,
|
||||
XMLParseError,
|
||||
ValidationError,
|
||||
)
|
||||
SET_SYNCHRONIZATION_POINT_ERRORS = (*SUBSCRIPTION_ERRORS, TypeError)
|
||||
UNSUBSCRIBE_ERRORS = (XMLParseError, *SUBSCRIPTION_ERRORS)
|
||||
RENEW_ERRORS = (ONVIFError, RequestError, XMLParseError, *SUBSCRIPTION_ERRORS)
|
||||
RENEW_ERRORS = (
|
||||
ONVIFError,
|
||||
aiohttp.ClientError,
|
||||
asyncio.TimeoutError,
|
||||
XMLParseError,
|
||||
*SUBSCRIPTION_ERRORS,
|
||||
)
|
||||
#
|
||||
# We only keep the subscription alive for 10 minutes, and will keep
|
||||
# renewing it every 8 minutes. This is to avoid the camera
|
||||
@@ -372,13 +385,13 @@ class PullPointManager:
|
||||
"%s: PullPoint skipped because Home Assistant is not running yet",
|
||||
self._name,
|
||||
)
|
||||
except RemoteProtocolError as err:
|
||||
except aiohttp.ServerDisconnectedError as err:
|
||||
# Either a shutdown event or the camera closed the connection. Because
|
||||
# http://datatracker.ietf.org/doc/html/rfc2616#section-8.1.4 allows the server
|
||||
# to close the connection at any time, we treat this as a normal. Some
|
||||
# cameras may close the connection if there are no messages to pull.
|
||||
LOGGER.debug(
|
||||
"%s: PullPoint subscription encountered a remote protocol error "
|
||||
"%s: PullPoint subscription encountered a server disconnected error "
|
||||
"(this is normal for some cameras): %s",
|
||||
self._name,
|
||||
stringify_onvif_error(err),
|
||||
@@ -394,7 +407,12 @@ class PullPointManager:
|
||||
# Treat errors as if the camera restarted. Assume that the pullpoint
|
||||
# subscription is no longer valid.
|
||||
self._pullpoint_manager.resume()
|
||||
except (XMLParseError, RequestError, TimeoutError, TransportError) as err:
|
||||
except (
|
||||
XMLParseError,
|
||||
aiohttp.ClientError,
|
||||
TimeoutError,
|
||||
TransportError,
|
||||
) as err:
|
||||
LOGGER.debug(
|
||||
"%s: PullPoint subscription encountered an unexpected error and will be retried "
|
||||
"(this is normal for some cameras): %s",
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/onvif",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["onvif", "wsdiscovery", "zeep"],
|
||||
"requirements": ["onvif-zeep-async==3.2.5", "WSDiscovery==2.1.2"]
|
||||
"requirements": ["onvif-zeep-async==4.0.1", "WSDiscovery==2.1.2"]
|
||||
}
|
||||
|
@@ -1,62 +1,40 @@
|
||||
"""Support for OpenTherm Gateway devices."""
|
||||
|
||||
import asyncio
|
||||
from datetime import date, datetime
|
||||
import logging
|
||||
|
||||
from pyotgw import OpenThermGateway
|
||||
import pyotgw.vars as gw_vars
|
||||
from serial import SerialException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DATE,
|
||||
ATTR_ID,
|
||||
ATTR_MODE,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TIME,
|
||||
CONF_DEVICE,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
ATTR_CH_OVRD,
|
||||
ATTR_DHW_OVRD,
|
||||
ATTR_GW_ID,
|
||||
ATTR_LEVEL,
|
||||
ATTR_TRANSP_ARG,
|
||||
ATTR_TRANSP_CMD,
|
||||
CONF_TEMPORARY_OVRD_MODE,
|
||||
CONNECTION_TIMEOUT,
|
||||
DATA_GATEWAYS,
|
||||
DATA_OPENTHERM_GW,
|
||||
DOMAIN,
|
||||
SERVICE_RESET_GATEWAY,
|
||||
SERVICE_SEND_TRANSP_CMD,
|
||||
SERVICE_SET_CH_OVRD,
|
||||
SERVICE_SET_CLOCK,
|
||||
SERVICE_SET_CONTROL_SETPOINT,
|
||||
SERVICE_SET_GPIO_MODE,
|
||||
SERVICE_SET_HOT_WATER_OVRD,
|
||||
SERVICE_SET_HOT_WATER_SETPOINT,
|
||||
SERVICE_SET_LED_MODE,
|
||||
SERVICE_SET_MAX_MOD,
|
||||
SERVICE_SET_OAT,
|
||||
SERVICE_SET_SB_TEMP,
|
||||
OpenThermDataSource,
|
||||
OpenThermDeviceIdentifier,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
@@ -67,6 +45,14 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up OpenTherm Gateway integration."""
|
||||
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]]
|
||||
@@ -95,273 +81,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
register_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
def register_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for the component."""
|
||||
service_reset_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
)
|
||||
}
|
||||
)
|
||||
service_set_central_heating_ovrd_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_CH_OVRD): cv.boolean,
|
||||
}
|
||||
)
|
||||
service_set_clock_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Optional(ATTR_DATE, default=date.today): cv.date,
|
||||
vol.Optional(ATTR_TIME, default=lambda: datetime.now().time()): cv.time,
|
||||
}
|
||||
)
|
||||
service_set_control_setpoint_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=90)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_hot_water_setpoint_schema = service_set_control_setpoint_schema
|
||||
service_set_hot_water_ovrd_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_DHW_OVRD): vol.Any(
|
||||
vol.Equal("A"), vol.All(vol.Coerce(int), vol.Range(min=0, max=1))
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_gpio_mode_schema = vol.Schema(
|
||||
vol.Any(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_ID): vol.Equal("A"),
|
||||
vol.Required(ATTR_MODE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=6)
|
||||
),
|
||||
}
|
||||
),
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_ID): vol.Equal("B"),
|
||||
vol.Required(ATTR_MODE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=7)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
service_set_led_mode_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_ID): vol.In("ABCDEF"),
|
||||
vol.Required(ATTR_MODE): vol.In("RXTBOFHWCEMP"),
|
||||
}
|
||||
)
|
||||
service_set_max_mod_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_LEVEL): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=-1, max=100)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_oat_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=-40, max=99)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_sb_temp_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=30)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_send_transp_cmd_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(
|
||||
cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
|
||||
),
|
||||
vol.Required(ATTR_TRANSP_CMD): vol.All(
|
||||
cv.string, vol.Length(min=2, max=2), vol.Coerce(str.upper)
|
||||
),
|
||||
vol.Required(ATTR_TRANSP_ARG): vol.All(
|
||||
cv.string, vol.Length(min=1, max=12)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
async def reset_gateway(call: ServiceCall) -> None:
|
||||
"""Reset the OpenTherm Gateway."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
mode_rst = gw_vars.OTGW_MODE_RESET
|
||||
await gw_hub.gateway.set_mode(mode_rst)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESET_GATEWAY, reset_gateway, service_reset_schema
|
||||
)
|
||||
|
||||
async def set_ch_ovrd(call: ServiceCall) -> None:
|
||||
"""Set the central heating override on the OpenTherm Gateway."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
await gw_hub.gateway.set_ch_enable_bit(1 if call.data[ATTR_CH_OVRD] else 0)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CH_OVRD,
|
||||
set_ch_ovrd,
|
||||
service_set_central_heating_ovrd_schema,
|
||||
)
|
||||
|
||||
async def set_control_setpoint(call: ServiceCall) -> None:
|
||||
"""Set the control setpoint on the OpenTherm Gateway."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
await gw_hub.gateway.set_control_setpoint(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONTROL_SETPOINT,
|
||||
set_control_setpoint,
|
||||
service_set_control_setpoint_schema,
|
||||
)
|
||||
|
||||
async def set_dhw_ovrd(call: ServiceCall) -> None:
|
||||
"""Set the domestic hot water override on the OpenTherm Gateway."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
await gw_hub.gateway.set_hot_water_ovrd(call.data[ATTR_DHW_OVRD])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_OVRD,
|
||||
set_dhw_ovrd,
|
||||
service_set_hot_water_ovrd_schema,
|
||||
)
|
||||
|
||||
async def set_dhw_setpoint(call: ServiceCall) -> None:
|
||||
"""Set the domestic hot water setpoint on the OpenTherm Gateway."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
await gw_hub.gateway.set_dhw_setpoint(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SETPOINT,
|
||||
set_dhw_setpoint,
|
||||
service_set_hot_water_setpoint_schema,
|
||||
)
|
||||
|
||||
async def set_device_clock(call: ServiceCall) -> None:
|
||||
"""Set the clock on the OpenTherm Gateway."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
attr_date = call.data[ATTR_DATE]
|
||||
attr_time = call.data[ATTR_TIME]
|
||||
await gw_hub.gateway.set_clock(datetime.combine(attr_date, attr_time))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_CLOCK, set_device_clock, service_set_clock_schema
|
||||
)
|
||||
|
||||
async def set_gpio_mode(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway GPIO modes."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
gpio_id = call.data[ATTR_ID]
|
||||
gpio_mode = call.data[ATTR_MODE]
|
||||
await gw_hub.gateway.set_gpio_mode(gpio_id, gpio_mode)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_GPIO_MODE, set_gpio_mode, service_set_gpio_mode_schema
|
||||
)
|
||||
|
||||
async def set_led_mode(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway LED modes."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
led_id = call.data[ATTR_ID]
|
||||
led_mode = call.data[ATTR_MODE]
|
||||
await gw_hub.gateway.set_led_mode(led_id, led_mode)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_LED_MODE, set_led_mode, service_set_led_mode_schema
|
||||
)
|
||||
|
||||
async def set_max_mod(call: ServiceCall) -> None:
|
||||
"""Set the max modulation level."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
level = call.data[ATTR_LEVEL]
|
||||
if level == -1:
|
||||
# Backend only clears setting on non-numeric values.
|
||||
level = "-"
|
||||
await gw_hub.gateway.set_max_relative_mod(level)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_MAX_MOD, set_max_mod, service_set_max_mod_schema
|
||||
)
|
||||
|
||||
async def set_outside_temp(call: ServiceCall) -> None:
|
||||
"""Provide the outside temperature to the OpenTherm Gateway."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
await gw_hub.gateway.set_outside_temp(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OAT, set_outside_temp, service_set_oat_schema
|
||||
)
|
||||
|
||||
async def set_setback_temp(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway SetBack temperature."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
await gw_hub.gateway.set_setback_temp(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SB_TEMP, set_setback_temp, service_set_sb_temp_schema
|
||||
)
|
||||
|
||||
async def send_transparent_cmd(call: ServiceCall) -> None:
|
||||
"""Send a transparent OpenTherm Gateway command."""
|
||||
gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
|
||||
transp_cmd = call.data[ATTR_TRANSP_CMD]
|
||||
transp_arg = call.data[ATTR_TRANSP_ARG]
|
||||
await gw_hub.gateway.send_transparent_command(transp_cmd, transp_arg)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_TRANSP_CMD,
|
||||
send_transparent_cmd,
|
||||
service_send_transp_cmd_schema,
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Cleanup and disconnect from gateway."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
296
homeassistant/components/opentherm_gw/services.py
Normal file
296
homeassistant/components/opentherm_gw/services.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""Support for OpenTherm Gateway devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pyotgw.vars as gw_vars
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_DATE,
|
||||
ATTR_ID,
|
||||
ATTR_MODE,
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TIME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
ATTR_CH_OVRD,
|
||||
ATTR_DHW_OVRD,
|
||||
ATTR_GW_ID,
|
||||
ATTR_LEVEL,
|
||||
ATTR_TRANSP_ARG,
|
||||
ATTR_TRANSP_CMD,
|
||||
DATA_GATEWAYS,
|
||||
DATA_OPENTHERM_GW,
|
||||
DOMAIN,
|
||||
SERVICE_RESET_GATEWAY,
|
||||
SERVICE_SEND_TRANSP_CMD,
|
||||
SERVICE_SET_CH_OVRD,
|
||||
SERVICE_SET_CLOCK,
|
||||
SERVICE_SET_CONTROL_SETPOINT,
|
||||
SERVICE_SET_GPIO_MODE,
|
||||
SERVICE_SET_HOT_WATER_OVRD,
|
||||
SERVICE_SET_HOT_WATER_SETPOINT,
|
||||
SERVICE_SET_LED_MODE,
|
||||
SERVICE_SET_MAX_MOD,
|
||||
SERVICE_SET_OAT,
|
||||
SERVICE_SET_SB_TEMP,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import OpenThermGatewayHub
|
||||
|
||||
|
||||
def _get_gateway(call: ServiceCall) -> OpenThermGatewayHub:
|
||||
gw_id: str = call.data[ATTR_GW_ID]
|
||||
gw_hub: OpenThermGatewayHub | None = (
|
||||
call.hass.data.get(DATA_OPENTHERM_GW, {}).get(DATA_GATEWAYS, {}).get(gw_id)
|
||||
)
|
||||
if gw_hub is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_gateway_id",
|
||||
translation_placeholders={"gw_id": gw_id},
|
||||
)
|
||||
return gw_hub
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for the component."""
|
||||
service_reset_schema = vol.Schema({vol.Required(ATTR_GW_ID): vol.All(cv.string)})
|
||||
service_set_central_heating_ovrd_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_CH_OVRD): cv.boolean,
|
||||
}
|
||||
)
|
||||
service_set_clock_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Optional(ATTR_DATE, default=date.today): cv.date,
|
||||
vol.Optional(ATTR_TIME, default=lambda: datetime.now().time()): cv.time,
|
||||
}
|
||||
)
|
||||
service_set_control_setpoint_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=90)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_hot_water_setpoint_schema = service_set_control_setpoint_schema
|
||||
service_set_hot_water_ovrd_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_DHW_OVRD): vol.Any(
|
||||
vol.Equal("A"), vol.All(vol.Coerce(int), vol.Range(min=0, max=1))
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_gpio_mode_schema = vol.Schema(
|
||||
vol.Any(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_ID): vol.Equal("A"),
|
||||
vol.Required(ATTR_MODE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=6)
|
||||
),
|
||||
}
|
||||
),
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_ID): vol.Equal("B"),
|
||||
vol.Required(ATTR_MODE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=7)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
service_set_led_mode_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_ID): vol.In("ABCDEF"),
|
||||
vol.Required(ATTR_MODE): vol.In("RXTBOFHWCEMP"),
|
||||
}
|
||||
)
|
||||
service_set_max_mod_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_LEVEL): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=-1, max=100)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_oat_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=-40, max=99)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_sb_temp_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=30)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_send_transp_cmd_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TRANSP_CMD): vol.All(
|
||||
cv.string, vol.Length(min=2, max=2), vol.Coerce(str.upper)
|
||||
),
|
||||
vol.Required(ATTR_TRANSP_ARG): vol.All(
|
||||
cv.string, vol.Length(min=1, max=12)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
async def reset_gateway(call: ServiceCall) -> None:
|
||||
"""Reset the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
mode_rst = gw_vars.OTGW_MODE_RESET
|
||||
await gw_hub.gateway.set_mode(mode_rst)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESET_GATEWAY, reset_gateway, service_reset_schema
|
||||
)
|
||||
|
||||
async def set_ch_ovrd(call: ServiceCall) -> None:
|
||||
"""Set the central heating override on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_ch_enable_bit(1 if call.data[ATTR_CH_OVRD] else 0)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CH_OVRD,
|
||||
set_ch_ovrd,
|
||||
service_set_central_heating_ovrd_schema,
|
||||
)
|
||||
|
||||
async def set_control_setpoint(call: ServiceCall) -> None:
|
||||
"""Set the control setpoint on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_control_setpoint(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONTROL_SETPOINT,
|
||||
set_control_setpoint,
|
||||
service_set_control_setpoint_schema,
|
||||
)
|
||||
|
||||
async def set_dhw_ovrd(call: ServiceCall) -> None:
|
||||
"""Set the domestic hot water override on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_hot_water_ovrd(call.data[ATTR_DHW_OVRD])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_OVRD,
|
||||
set_dhw_ovrd,
|
||||
service_set_hot_water_ovrd_schema,
|
||||
)
|
||||
|
||||
async def set_dhw_setpoint(call: ServiceCall) -> None:
|
||||
"""Set the domestic hot water setpoint on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_dhw_setpoint(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SETPOINT,
|
||||
set_dhw_setpoint,
|
||||
service_set_hot_water_setpoint_schema,
|
||||
)
|
||||
|
||||
async def set_device_clock(call: ServiceCall) -> None:
|
||||
"""Set the clock on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
attr_date = call.data[ATTR_DATE]
|
||||
attr_time = call.data[ATTR_TIME]
|
||||
await gw_hub.gateway.set_clock(datetime.combine(attr_date, attr_time))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_CLOCK, set_device_clock, service_set_clock_schema
|
||||
)
|
||||
|
||||
async def set_gpio_mode(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway GPIO modes."""
|
||||
gw_hub = _get_gateway(call)
|
||||
gpio_id = call.data[ATTR_ID]
|
||||
gpio_mode = call.data[ATTR_MODE]
|
||||
await gw_hub.gateway.set_gpio_mode(gpio_id, gpio_mode)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_GPIO_MODE, set_gpio_mode, service_set_gpio_mode_schema
|
||||
)
|
||||
|
||||
async def set_led_mode(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway LED modes."""
|
||||
gw_hub = _get_gateway(call)
|
||||
led_id = call.data[ATTR_ID]
|
||||
led_mode = call.data[ATTR_MODE]
|
||||
await gw_hub.gateway.set_led_mode(led_id, led_mode)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_LED_MODE, set_led_mode, service_set_led_mode_schema
|
||||
)
|
||||
|
||||
async def set_max_mod(call: ServiceCall) -> None:
|
||||
"""Set the max modulation level."""
|
||||
gw_hub = _get_gateway(call)
|
||||
level = call.data[ATTR_LEVEL]
|
||||
if level == -1:
|
||||
# Backend only clears setting on non-numeric values.
|
||||
level = "-"
|
||||
await gw_hub.gateway.set_max_relative_mod(level)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_MAX_MOD, set_max_mod, service_set_max_mod_schema
|
||||
)
|
||||
|
||||
async def set_outside_temp(call: ServiceCall) -> None:
|
||||
"""Provide the outside temperature to the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_outside_temp(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OAT, set_outside_temp, service_set_oat_schema
|
||||
)
|
||||
|
||||
async def set_setback_temp(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway SetBack temperature."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_setback_temp(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SB_TEMP, set_setback_temp, service_set_sb_temp_schema
|
||||
)
|
||||
|
||||
async def send_transparent_cmd(call: ServiceCall) -> None:
|
||||
"""Send a transparent OpenTherm Gateway command."""
|
||||
gw_hub = _get_gateway(call)
|
||||
transp_cmd = call.data[ATTR_TRANSP_CMD]
|
||||
transp_arg = call.data[ATTR_TRANSP_ARG]
|
||||
await gw_hub.gateway.send_transparent_command(transp_cmd, transp_arg)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_TRANSP_CMD,
|
||||
send_transparent_cmd,
|
||||
service_send_transp_cmd_schema,
|
||||
)
|
@@ -354,6 +354,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_gateway_id": {
|
||||
"message": "Gateway {gw_id} not found or not loaded!"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
DEGREE,
|
||||
PERCENTAGE,
|
||||
UV_INDEX,
|
||||
@@ -170,7 +169,7 @@ AIRPOLLUTION_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_AIRPOLLUTION_CO,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.CO,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
|
@@ -10,7 +10,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_API, CONF_COORDINATOR, DOMAIN
|
||||
from .coordinator import PicnicUpdateCoordinator
|
||||
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.TODO]
|
||||
@@ -19,7 +19,7 @@ PLATFORMS = [Platform.SENSOR, Platform.TODO]
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Picnic integration."""
|
||||
|
||||
await async_register_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
@@ -26,7 +26,7 @@ class PicnicServiceException(Exception):
|
||||
"""Exception for Picnic services."""
|
||||
|
||||
|
||||
async def async_register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for the Picnic integration, if not registered yet."""
|
||||
|
||||
async def async_add_product_service(call: ServiceCall):
|
||||
|
@@ -29,7 +29,7 @@ from homeassistant.util.json import JsonObjectType, load_json_object
|
||||
|
||||
from .config_flow import PlayStation4FlowHandler # noqa: F401
|
||||
from .const import ATTR_MEDIA_IMAGE_URL, COUNTRYCODE_NAMES, DOMAIN, GAMES_FILE, PS4_DATA
|
||||
from .services import register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .media_player import PS4Device
|
||||
@@ -58,7 +58,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
protocol=protocol,
|
||||
)
|
||||
_LOGGER.debug("PS4 DDP endpoint created: %s, %s", transport, protocol)
|
||||
register_services(hass)
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
|
@@ -29,7 +29,7 @@ async def async_service_command(call: ServiceCall) -> None:
|
||||
await device.async_send_command(command)
|
||||
|
||||
|
||||
def register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Handle for services."""
|
||||
|
||||
hass.services.async_register(
|
||||
|
@@ -9,7 +9,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
@@ -211,10 +211,10 @@ def create_rest_data_from_config(hass: HomeAssistant, config: ConfigType) -> Res
|
||||
if not resource:
|
||||
raise HomeAssistantError("Resource not set for RestData")
|
||||
|
||||
auth: httpx.DigestAuth | tuple[str, str] | None = None
|
||||
auth: aiohttp.DigestAuthMiddleware | tuple[str, str] | None = None
|
||||
if username and password:
|
||||
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
|
||||
auth = httpx.DigestAuth(username, password)
|
||||
auth = aiohttp.DigestAuthMiddleware(username, password)
|
||||
else:
|
||||
auth = (username, password)
|
||||
|
||||
|
@@ -3,14 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import ssl
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import aiohttp
|
||||
from multidict import CIMultiDictProxy
|
||||
import xmltodict
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.httpx_client import create_async_httpx_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.util.ssl import SSLCipherList
|
||||
|
||||
@@ -30,7 +31,7 @@ class RestData:
|
||||
method: str,
|
||||
resource: str,
|
||||
encoding: str,
|
||||
auth: httpx.DigestAuth | tuple[str, str] | None,
|
||||
auth: aiohttp.DigestAuthMiddleware | aiohttp.BasicAuth | tuple[str, str] | None,
|
||||
headers: dict[str, str] | None,
|
||||
params: dict[str, str] | None,
|
||||
data: str | None,
|
||||
@@ -43,17 +44,25 @@ class RestData:
|
||||
self._method = method
|
||||
self._resource = resource
|
||||
self._encoding = encoding
|
||||
self._auth = auth
|
||||
|
||||
# Convert auth tuple to aiohttp.BasicAuth if needed
|
||||
if isinstance(auth, tuple) and len(auth) == 2:
|
||||
self._auth: aiohttp.BasicAuth | aiohttp.DigestAuthMiddleware | None = (
|
||||
aiohttp.BasicAuth(auth[0], auth[1])
|
||||
)
|
||||
else:
|
||||
self._auth = auth
|
||||
|
||||
self._headers = headers
|
||||
self._params = params
|
||||
self._request_data = data
|
||||
self._timeout = timeout
|
||||
self._timeout = aiohttp.ClientTimeout(total=timeout)
|
||||
self._verify_ssl = verify_ssl
|
||||
self._ssl_cipher_list = SSLCipherList(ssl_cipher_list)
|
||||
self._async_client: httpx.AsyncClient | None = None
|
||||
self._session: aiohttp.ClientSession | None = None
|
||||
self.data: str | None = None
|
||||
self.last_exception: Exception | None = None
|
||||
self.headers: httpx.Headers | None = None
|
||||
self.headers: CIMultiDictProxy[str] | None = None
|
||||
|
||||
def set_payload(self, payload: str) -> None:
|
||||
"""Set request data."""
|
||||
@@ -84,38 +93,49 @@ class RestData:
|
||||
|
||||
async def async_update(self, log_errors: bool = True) -> None:
|
||||
"""Get the latest data from REST service with provided method."""
|
||||
if not self._async_client:
|
||||
self._async_client = create_async_httpx_client(
|
||||
if not self._session:
|
||||
self._session = async_get_clientsession(
|
||||
self._hass,
|
||||
verify_ssl=self._verify_ssl,
|
||||
default_encoding=self._encoding,
|
||||
ssl_cipher_list=self._ssl_cipher_list,
|
||||
ssl_cipher=self._ssl_cipher_list,
|
||||
)
|
||||
|
||||
rendered_headers = template.render_complex(self._headers, parse_result=False)
|
||||
rendered_params = template.render_complex(self._params)
|
||||
|
||||
_LOGGER.debug("Updating from %s", self._resource)
|
||||
# Create request kwargs
|
||||
request_kwargs: dict[str, Any] = {
|
||||
"headers": rendered_headers,
|
||||
"params": rendered_params,
|
||||
"timeout": self._timeout,
|
||||
}
|
||||
|
||||
# Handle authentication
|
||||
if isinstance(self._auth, aiohttp.BasicAuth):
|
||||
request_kwargs["auth"] = self._auth
|
||||
elif isinstance(self._auth, aiohttp.DigestAuthMiddleware):
|
||||
request_kwargs["middlewares"] = (self._auth,)
|
||||
|
||||
# Handle data/content
|
||||
if self._request_data:
|
||||
request_kwargs["data"] = self._request_data
|
||||
try:
|
||||
response = await self._async_client.request(
|
||||
self._method,
|
||||
self._resource,
|
||||
headers=rendered_headers,
|
||||
params=rendered_params,
|
||||
auth=self._auth,
|
||||
content=self._request_data,
|
||||
timeout=self._timeout,
|
||||
follow_redirects=True,
|
||||
)
|
||||
self.data = response.text
|
||||
self.headers = response.headers
|
||||
except httpx.TimeoutException as ex:
|
||||
# Make the request
|
||||
async with self._session.request(
|
||||
self._method, self._resource, **request_kwargs
|
||||
) as response:
|
||||
# Read the response
|
||||
self.data = await response.text(encoding=self._encoding)
|
||||
self.headers = response.headers
|
||||
|
||||
except TimeoutError as ex:
|
||||
if log_errors:
|
||||
_LOGGER.error("Timeout while fetching data: %s", self._resource)
|
||||
self.last_exception = ex
|
||||
self.data = None
|
||||
self.headers = None
|
||||
except httpx.RequestError as ex:
|
||||
except aiohttp.ClientError as ex:
|
||||
if log_errors:
|
||||
_LOGGER.error(
|
||||
"Error fetching data: %s failed with %s", self._resource, ex
|
||||
@@ -123,11 +143,3 @@ class RestData:
|
||||
self.last_exception = ex
|
||||
self.data = None
|
||||
self.headers = None
|
||||
except ssl.SSLError as ex:
|
||||
if log_errors:
|
||||
_LOGGER.error(
|
||||
"Error connecting to %s failed with %s", self._resource, ex
|
||||
)
|
||||
self.last_exception = ex
|
||||
self.data = None
|
||||
self.headers = None
|
||||
|
@@ -34,10 +34,10 @@
|
||||
"title": "Set up SMA Solar"
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"title": "[%key:component::sma::config::step::user::title]",
|
||||
"description": "Do you want to setup the discovered SMA ({host})?",
|
||||
"title": "[%key:component::sma::config::step::user::title%]",
|
||||
"description": "Do you want to set up the discovered SMA device ({host})?",
|
||||
"data": {
|
||||
"group": "[%key:component::sma::config::step::user::data::group]",
|
||||
"group": "[%key:component::sma::config::step::user::data::group%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
|
@@ -6,7 +6,7 @@ DOMAIN = "smarla"
|
||||
|
||||
HOST = "https://devices.swing2sleep.de"
|
||||
|
||||
PLATFORMS = [Platform.SWITCH]
|
||||
PLATFORMS = [Platform.NUMBER, Platform.SWITCH]
|
||||
|
||||
DEVICE_MODEL_NAME = "Smarla"
|
||||
MANUFACTURER_NAME = "Swing2Sleep"
|
||||
|
@@ -4,6 +4,11 @@
|
||||
"smart_mode": {
|
||||
"default": "mdi:refresh-auto"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"intensity": {
|
||||
"default": "mdi:sine-wave"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
62
homeassistant/components/smarla/number.py
Normal file
62
homeassistant/components/smarla/number.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Support for the Swing2Sleep Smarla number entities."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pysmarlaapi.federwiege.classes import Property
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import FederwiegeConfigEntry
|
||||
from .entity import SmarlaBaseEntity, SmarlaEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SmarlaNumberEntityDescription(SmarlaEntityDescription, NumberEntityDescription):
|
||||
"""Class describing Swing2Sleep Smarla number entities."""
|
||||
|
||||
|
||||
NUMBERS: list[SmarlaNumberEntityDescription] = [
|
||||
SmarlaNumberEntityDescription(
|
||||
key="intensity",
|
||||
translation_key="intensity",
|
||||
service="babywiege",
|
||||
property="intensity",
|
||||
native_max_value=100,
|
||||
native_min_value=0,
|
||||
native_step=1,
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: FederwiegeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Smarla numbers from config entry."""
|
||||
federwiege = config_entry.runtime_data
|
||||
async_add_entities(SmarlaNumber(federwiege, desc) for desc in NUMBERS)
|
||||
|
||||
|
||||
class SmarlaNumber(SmarlaBaseEntity, NumberEntity):
|
||||
"""Representation of Smarla number."""
|
||||
|
||||
entity_description: SmarlaNumberEntityDescription
|
||||
|
||||
_property: Property[int]
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
return self._property.get()
|
||||
|
||||
def set_native_value(self, value: float) -> None:
|
||||
"""Update to the smarla device."""
|
||||
self._property.set(int(value))
|
@@ -23,6 +23,11 @@
|
||||
"smart_mode": {
|
||||
"name": "Smart Mode"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"intensity": {
|
||||
"name": "Intensity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["spotifyaio"],
|
||||
"requirements": ["spotifyaio==0.8.11"],
|
||||
"zeroconf": ["_spotify-connect._tcp.local."]
|
||||
"requirements": ["spotifyaio==0.8.11"]
|
||||
}
|
||||
|
@@ -7,9 +7,6 @@
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}"
|
||||
},
|
||||
"oauth_discovery": {
|
||||
"description": "Home Assistant has found Spotify on your network. Press **Submit** to continue setting up Spotify."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.8.0", "av==13.1.0", "numpy==2.2.6"]
|
||||
"requirements": ["PyTurboJPEG==1.8.0", "av==13.1.0", "numpy==2.3.0"]
|
||||
}
|
||||
|
@@ -9,11 +9,11 @@ import voluptuous as vol
|
||||
from homeassistant.components.homeassistant import exposed_entities
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ENTITY_ID
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
|
||||
from .const import CONF_INVERT, CONF_TARGET_DOMAIN
|
||||
from .const import CONF_INVERT, CONF_TARGET_DOMAIN, DOMAIN
|
||||
from .light import LightSwitch
|
||||
|
||||
__all__ = ["LightSwitch"]
|
||||
@@ -44,10 +44,12 @@ def async_add_to_device(
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
registry = er.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
try:
|
||||
entity_id = er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID])
|
||||
entity_id = er.async_validate_entity_id(
|
||||
entity_registry, entry.options[CONF_ENTITY_ID]
|
||||
)
|
||||
except vol.Invalid:
|
||||
# The entity is identified by an unknown entity registry ID
|
||||
_LOGGER.error(
|
||||
@@ -68,24 +70,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return
|
||||
|
||||
if "entity_id" in data["changes"]:
|
||||
# Entity_id changed, reload the config entry
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
# Entity_id changed, update or reload the config entry
|
||||
if valid_entity_id(entry.options[CONF_ENTITY_ID]):
|
||||
# If the entity is pointed to by an entity ID, update the entry
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_ENTITY_ID: data["entity_id"]},
|
||||
)
|
||||
else:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
if device_id and "device_id" in data["changes"]:
|
||||
# If the tracked switch is no longer in the device, remove our config entry
|
||||
# Handle the wrapped switch being moved to a different device or removed
|
||||
# from the device
|
||||
if (
|
||||
not (entity_entry := registry.async_get(data[CONF_ENTITY_ID]))
|
||||
not (entity_entry := entity_registry.async_get(data[CONF_ENTITY_ID]))
|
||||
or not device_registry.async_get(device_id)
|
||||
or entity_entry.device_id == device_id
|
||||
):
|
||||
# No need to do any cleanup
|
||||
return
|
||||
|
||||
# The wrapped switch has been moved to a different device, update the
|
||||
# switch_as_x entity and the device entry to include our config entry
|
||||
switch_as_x_entity_id = entity_registry.async_get_entity_id(
|
||||
entry.options[CONF_TARGET_DOMAIN], DOMAIN, entry.entry_id
|
||||
)
|
||||
if switch_as_x_entity_id:
|
||||
# Update the switch_as_x entity to point to the new device (or no device)
|
||||
entity_registry.async_update_entity(
|
||||
switch_as_x_entity_id, device_id=entity_entry.device_id
|
||||
)
|
||||
|
||||
if entity_entry.device_id is not None:
|
||||
device_registry.async_update_device(
|
||||
entity_entry.device_id, add_config_entry_id=entry.entry_id
|
||||
)
|
||||
|
||||
device_registry.async_update_device(
|
||||
device_id, remove_config_entry_id=entry.entry_id
|
||||
)
|
||||
|
||||
# Reload the config entry so the switch_as_x entity is recreated with
|
||||
# correct device info
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
hass, entity_id, async_registry_updated
|
||||
|
@@ -41,5 +41,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["PySwitchbot==0.64.1"]
|
||||
"requirements": ["PySwitchbot==0.65.0"]
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ from homeassistant.const import (
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
@@ -94,7 +95,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
|
||||
),
|
||||
"current": SensorEntityDescription(
|
||||
key="current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
),
|
||||
@@ -110,6 +111,12 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = {
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[member.name.lower() for member in AirQualityLevel],
|
||||
),
|
||||
"energy": SensorEntityDescription(
|
||||
key="energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["switchbot_api"],
|
||||
"requirements": ["switchbot-api==2.4.0"]
|
||||
"requirements": ["switchbot-api==2.5.0"]
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["synology_dsm"],
|
||||
"requirements": ["py-synologydsm-api==2.7.2"],
|
||||
"requirements": ["py-synologydsm-api==2.7.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Synology",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user