Compare commits

..

52 Commits

Author SHA1 Message Date
abmantis 0040e9e5af Restore latest version of ZHA Update entity
Required for https://github.com/zigpy/zha/pull/460
2025-06-09 18:01:57 +01:00
Abílio Costa 0cce4d1b81 Test all device classes in Sensor device condition/trigger tests (#146366) 2025-06-09 14:22:58 +01:00
Erik Montnemery 46dcc91510 Fix switch_as_x entity_id tracking (#146386) 2025-06-09 13:24:40 +02:00
Markus Adrario b1a2af9fd3 Add Homee diagnostics platform (#146340)
* Initial dignostics implementation

* Add diagnostics tests

* change data-set for device diagnostics

* adapt for upcoming pyHomee release

* other solution

* fix review and more
2025-06-09 13:24:07 +02:00
Michael Arthur 5d58cdd98e DNSIP: Add literal to querytype (#146367) 2025-06-09 09:36:17 +02:00
Simon Lamon a8aebbce9a Bump python-linkplay to v0.2.10 (#146359) 2025-06-08 16:43:20 -05:00
tronikos f1244c182a Allow different manufacturer than Amazon in Amazon Devices (#146333) 2025-06-08 11:47:46 -07:00
Simon Lamon 560eeac457 Do not probe linkplay device if another config entry already contains the host (#146305)
* Do not probe if config entry already contains the host

* Add unit test

* Use common fixture
2025-06-08 19:47:00 +02:00
J. Nick Koston d33080d79e Bump aioesphomeapi to 32.2.0 (#146344) 2025-06-08 11:15:00 -05:00
Michael 25f02c5b38 Bump py-synologydsm-api to 2.7.3 (#146338)
bump py-synologydsm-api to 2.7.3
2025-06-08 17:02:06 +01:00
Raphael Hehl cb01af9f92 Bump uiprotect to 7.12.0 (#146337) 2025-06-08 10:57:50 -05:00
Sanjay Govind 9a6ebb0848 Fix bosch alarm areas not correctly subscribing to alarms (#146322)
* Fix bosch alarm areas not correctly subscribing to alarms

* add test
2025-06-08 14:35:54 +02:00
Pete Sage fd30dd0aee Add tests for sonos switch alarms on and off (#146314)
* fix: add tests for switch on/off

* fix: simplify

* fix: simplify

* fix: comment

* fix: comment
2025-06-08 11:45:20 +02:00
tronikos 4a5e261709 Fix typo in Utility Meter always_available (#146320) 2025-06-08 10:53:48 +03:00
Marc Mueller 2842f55460 Add additional package version range checks (#146299)
* Add additional package version range checks

* Add exception for scipy
2025-06-08 00:06:20 +02:00
J. Nick Koston 7573a74cb0 Migrate rest to use aiohttp (#146306) 2025-06-07 13:44:25 -05:00
J. Nick Koston 636b484d9d Migrate onvif to use onvif-zeep-async 4.0.1 with aiohttp (#146297) 2025-06-07 13:39:59 -05:00
G Johansson a979f884f9 Bump holidays to 0.74 (#146290) 2025-06-07 20:18:24 +03:00
J. Nick Koston 990ea78dec Bump aiohttp to 3.12.11 (#146298) 2025-06-07 12:08:32 -05:00
Marc Mueller ee6db3bd23 Update numpy to 2.3.0 (#146296) 2025-06-07 18:43:18 +02:00
Arie Catsman ae5606aa2f Migrate Enphase envoy from httpx to aiohttp (#146283)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-07 10:52:54 -05:00
Marc Mueller 7f9f106729 Update airtouch5py to 0.3.0 (#146278) 2025-06-07 16:58:53 +02:00
J. Nick Koston 44c63ce6f1 Bump aiohttp-fast-zlib to 0.3.0 (#146285)
changelog: https://github.com/Bluetooth-Devices/aiohttp-fast-zlib/compare/v0.2.3...v0.3.0

proper aiohttp 3.12 support
2025-06-07 17:30:43 +03:00
hanwg cbf7ca6a9a Add bronze quality scale for Telegram bot integration (#146148)
* added quality scale

* updated appropriate-polling comment

* Remove entities comment

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-07 14:47:48 +02:00
Brett Adams eb892df65a Change default range sensors in Teslemetry (#146268) 2025-06-07 10:51:57 +02:00
Brett Adams 24b5886d88 Add missing write state to Teslemetry (#146267) 2025-06-07 04:43:16 +02:00
Willem-Jan van Rootselaar d5e902a170 Update python-bsblan requirement to version 2.1.0 (#146253) 2025-06-06 22:47:44 +03:00
hanwg d907e4c10b Handle error in setup_entry for Telegram Bot (#146242)
* handle error in setup_entry

* Update homeassistant/components/telegram_bot/__init__.py

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

---------

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-06-06 15:00:48 +01:00
Robin Lintermann c4be3c4de2 Smarla integration number platform (#145747)
Add number platform to smarla integration
2025-06-06 12:13:06 +02:00
Retha Runolfsson 626591f832 Fix unit test for switchbot integration (#146247)
fix unit test
2025-06-06 12:06:01 +02:00
epenet 2bd3196183 Move abode services to separate module (#146142)
* Move abode services to separate module

* Rename

* Adjust test imports
2025-06-06 10:20:57 +02:00
epenet fd93cf375d Tweak zwave_js service registration (#146244) 2025-06-06 09:41:51 +02:00
epenet 6bf8b84d26 Rename service registration method (#146236) 2025-06-06 08:08:06 +02:00
Michael c72fea57a1 Bump aioimmich to 0.9.1 (#146222)
bump aioimmich to 0.9.1
2025-06-05 21:50:19 +02:00
Renat Sibgatulin 17dad7d8ae Bump aioairq to v0.4.6 (#146169)
This version exposes an API to control LED brightness.
2025-06-05 18:27:20 +02:00
Joost Lekkerkerker 14664719d9 Remove zeroconf discovery from Spotify (#146213) 2025-06-05 18:02:11 +02:00
epenet b14cd1e14b Move elkm1 services to separate module (#146147)
* Move elkm1 services to separate module

* Rename
2025-06-05 16:51:01 +02:00
Retha Runolfsson fd38d9788d Bump pyswitchbot to 0.65.0 (#146133)
* update pyswitchbot to 0.65.0

* fix relay switch 1pm test

* fix ma to a
2025-06-05 16:42:24 +02:00
epenet 0b3b641328 Move services to separate module in opentherm_gw (#146098)
* Move services to separate module in opentherm_gw

* Rename
2025-06-05 16:40:18 +02:00
Brett Adams 6ef77f8243 Fix Export Rule Select Entity in Tessie (#146203)
Fix TessieExportRuleSelectEntity
2025-06-05 16:39:55 +02:00
Ludovic BOUÉ 3a27143012 Matter add Service Area Cluster to vacuum_cleaner fixture (#145743)
Update vacuum_cleaner.json

Service Area Cluster
2025-06-05 16:39:08 +02:00
Samuel Xiao 9a6c642bdf Bump switchbot-api to 2.5.0 (#146205)
* update switchbot-api to 2.5.0

* update switchbot-api to 2.5.0
2025-06-05 16:16:45 +02:00
epenet 38b8d0b018 Move google_sheets services to separate module (#146160)
* Move google_sheets services to separate module

* Move to async_setup

* Do not remove the services

* hassfest

* Rename
2025-06-05 15:07:15 +02:00
epenet 4d3443dbf5 Move amcrest services to separate module (#146144)
* Move amcrest services to separate module

* Rename
2025-06-05 14:43:22 +02:00
Marc Mueller 4f99e54402 Update pandas to 2.3.0 (#146206) 2025-06-05 14:42:21 +02:00
epenet d6615e3d44 Move ffmpeg services to separate module (#146149)
* Move ffmpeg services to separate module

* Fix tests

* Rename
2025-06-05 14:39:44 +02:00
Willem-Jan van Rootselaar 9c23331ead Bump python-bsblan to version 2.0.1 (#146198)
* Bump python-bsblan to version 2.0.1

* Remove 'bsblan' exception for 'python-bsblan' from forbidden package exceptions
2025-06-05 13:07:16 +02:00
epenet 5fb2802bf4 Move zoneminder services to separate module (#146151) 2025-06-05 06:35:32 +02:00
epenet b4864e6a8a Move matrix services to separate module (#146161) 2025-06-05 06:35:10 +02:00
Raphael Hehl 04c34877f4 Bump uiprotect to 7.11.0 (#146171)
Bump uiprotect to version 7.11.0
2025-06-04 23:32:44 +03:00
Ludovic BOUÉ bdeb61fafc Matter Extractor hood fixture (#146174)
* Create extractor_hood.json

* Matter Extractor hood fixture

* Format document
2025-06-04 21:17:51 +02:00
J. Nick Koston 76d4257f51 Bump aiohttp to 3.12.9 (#146178) 2025-06-04 20:12:19 +02:00
143 changed files with 3976 additions and 1368 deletions
+4 -41
View File
@@ -125,7 +125,7 @@ jobs:
core:
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
if: false && github.repository_owner == 'home-assistant'
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ubuntu-latest
strategy:
@@ -176,26 +176,12 @@ jobs:
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
arch: amd64
abi: cp313
- os: ubuntu-latest
arch: i386
abi: cp313
- os: ubuntu-24.04-arm
arch: aarch64
abi: cp313
- os: ubuntu-24.04-arm
arch: armv7
abi: cp313
- os: ubuntu-latest
arch: armhf
abi: cp313
abi: ["cp313"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
@@ -232,31 +218,8 @@ jobs:
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt
- name: Create requirements file for custom build
run: |
touch requirements_custom.txt
echo "protobuf==6.30.2" >> requirements_custom.txt
echo "grpcio==1.72.1" >> requirements_custom.txt
echo "grpcio-status==1.72.1" >> requirements_custom.txt
echo "grpcio-reflection==1.72.1" >> requirements_custom.txt
- name: Build wheels (custom)
uses: cdce8p/wheels@master
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements: "requirements_custom.txt"
verbose: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
- name: Build wheels
uses: home-assistant/wheels@2025.03.0
if: false
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
+3 -73
View File
@@ -14,30 +14,24 @@ from jaraco.abode.exceptions import (
)
from jaraco.abode.helpers.timeline import Groups as GROUPS
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DATE,
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
ATTR_TIME,
CONF_PASSWORD,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.typing import ConfigType
from .const import CONF_POLLING, DOMAIN, LOGGER
SERVICE_SETTINGS = "change_setting"
SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
from .services import async_setup_services
ATTR_DEVICE_NAME = "device_name"
ATTR_DEVICE_TYPE = "device_type"
@@ -45,22 +39,12 @@ ATTR_EVENT_CODE = "event_code"
ATTR_EVENT_NAME = "event_name"
ATTR_EVENT_TYPE = "event_type"
ATTR_EVENT_UTC = "event_utc"
ATTR_SETTING = "setting"
ATTR_USER_NAME = "user_name"
ATTR_APP_TYPE = "app_type"
ATTR_EVENT_BY = "event_by"
ATTR_VALUE = "value"
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
CHANGE_SETTING_SCHEMA = vol.Schema(
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
)
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
@@ -85,7 +69,7 @@ class AbodeSystem:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Abode component."""
setup_hass_services(hass)
async_setup_services(hass)
return True
@@ -138,60 +122,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
def setup_hass_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
def change_setting(call: ServiceCall) -> None:
"""Change an Abode system setting."""
setting = call.data[ATTR_SETTING]
value = call.data[ATTR_VALUE]
try:
hass.data[DOMAIN].abode.set_setting(setting, value)
except AbodeException as ex:
LOGGER.warning(ex)
def capture_image(call: ServiceCall) -> None:
"""Capture a new image."""
entity_ids = call.data[ATTR_ENTITY_ID]
target_entities = [
entity_id
for entity_id in hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for entity_id in target_entities:
signal = f"abode_camera_capture_{entity_id}"
dispatcher_send(hass, signal)
def trigger_automation(call: ServiceCall) -> None:
"""Trigger an Abode automation."""
entity_ids = call.data[ATTR_ENTITY_ID]
target_entities = [
entity_id
for entity_id in hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for entity_id in target_entities:
signal = f"abode_trigger_automation_{entity_id}"
dispatcher_send(hass, signal)
hass.services.async_register(
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
)
async def setup_hass_events(hass: HomeAssistant) -> None:
"""Home Assistant start and stop callbacks."""
@@ -0,0 +1,89 @@
"""Support for the Abode Security System."""
from __future__ import annotations
from jaraco.abode.exceptions import Exception as AbodeException
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import DOMAIN, LOGGER
SERVICE_SETTINGS = "change_setting"
SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
ATTR_SETTING = "setting"
ATTR_VALUE = "value"
CHANGE_SETTING_SCHEMA = vol.Schema(
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
)
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
def _change_setting(call: ServiceCall) -> None:
"""Change an Abode system setting."""
setting = call.data[ATTR_SETTING]
value = call.data[ATTR_VALUE]
try:
call.hass.data[DOMAIN].abode.set_setting(setting, value)
except AbodeException as ex:
LOGGER.warning(ex)
def _capture_image(call: ServiceCall) -> None:
"""Capture a new image."""
entity_ids = call.data[ATTR_ENTITY_ID]
target_entities = [
entity_id
for entity_id in call.hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for entity_id in target_entities:
signal = f"abode_camera_capture_{entity_id}"
dispatcher_send(call.hass, signal)
def _trigger_automation(call: ServiceCall) -> None:
"""Trigger an Abode automation."""
entity_ids = call.data[ATTR_ENTITY_ID]
target_entities = [
entity_id
for entity_id in call.hass.data[DOMAIN].entity_ids
if entity_id in entity_ids
]
for entity_id in target_entities:
signal = f"abode_trigger_automation_{entity_id}"
dispatcher_send(call.hass, signal)
def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
hass.services.async_register(
DOMAIN, SERVICE_SETTINGS, _change_setting, schema=CHANGE_SETTING_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_CAPTURE_IMAGE, _capture_image, schema=CAPTURE_IMAGE_SCHEMA
)
hass.services.async_register(
DOMAIN,
SERVICE_TRIGGER_AUTOMATION,
_trigger_automation,
schema=AUTOMATION_SCHEMA,
)
+1 -1
View File
@@ -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
),
+4 -50
View File
@@ -16,10 +16,7 @@ from amcrest import AmcrestError, ApiWrapper, LoginError
import httpx
import voluptuous as vol
from homeassistant.auth.models import User
from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_AUTHENTICATION,
CONF_BINARY_SENSORS,
CONF_HOST,
@@ -30,21 +27,17 @@ from homeassistant.const import (
CONF_SENSORS,
CONF_SWITCHES,
CONF_USERNAME,
ENTITY_MATCH_ALL,
ENTITY_MATCH_NONE,
HTTP_BASIC_AUTHENTICATION,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.service import async_extract_entity_ids
from homeassistant.helpers.typing import ConfigType
from .binary_sensor import BINARY_SENSOR_KEYS, BINARY_SENSORS, check_binary_sensors
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
from .camera import STREAM_SOURCE_LIST
from .const import (
CAMERAS,
COMM_RETRIES,
@@ -58,6 +51,7 @@ from .const import (
)
from .helpers import service_signal
from .sensor import SENSOR_KEYS
from .services import async_setup_services
from .switch import SWITCH_KEYS
_LOGGER = logging.getLogger(__name__)
@@ -455,47 +449,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if not hass.data[DATA_AMCREST][DEVICES]:
return False
def have_permission(user: User | None, entity_id: str) -> bool:
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
async def async_extract_from_service(call: ServiceCall) -> list[str]:
if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(context=call.context)
else:
user = None
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
# Return all entity_ids user has permission to control.
return [
entity_id
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
if have_permission(user, entity_id)
]
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
return []
call_ids = await async_extract_entity_ids(hass, call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
if entity_id not in call_ids:
continue
if not have_permission(user, entity_id):
raise Unauthorized(
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
)
entity_ids.append(entity_id)
return entity_ids
async def async_service_handler(call: ServiceCall) -> None:
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
for entity_id in await async_extract_from_service(call):
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
for service, params in CAMERA_SERVICES.items():
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
async_setup_services(hass)
return True
@@ -0,0 +1,61 @@
"""Support for Amcrest IP cameras."""
from __future__ import annotations
from homeassistant.auth.models import User
from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.service import async_extract_entity_ids
from .camera import CAMERA_SERVICES
from .const import CAMERAS, DATA_AMCREST, DOMAIN
from .helpers import service_signal
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the Amcrest IP Camera services."""
def have_permission(user: User | None, entity_id: str) -> bool:
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
async def async_extract_from_service(call: ServiceCall) -> list[str]:
if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id)
if user is None:
raise UnknownUser(context=call.context)
else:
user = None
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
# Return all entity_ids user has permission to control.
return [
entity_id
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
if have_permission(user, entity_id)
]
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
return []
call_ids = await async_extract_entity_ids(hass, call)
entity_ids = []
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
if entity_id not in call_ids:
continue
if not have_permission(user, entity_id):
raise Unauthorized(
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
)
entity_ids.append(entity_id)
return entity_ids
async def async_service_handler(call: ServiceCall) -> None:
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
for entity_id in await async_extract_from_service(call):
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
for service, params in CAMERA_SERVICES.items():
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
@@ -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"]
}
+4 -3
View File
@@ -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,
+5 -65
View File
@@ -8,7 +8,7 @@ import re
from typing import Any
from elkm1_lib.elements import Element
from elkm1_lib.elk import Elk, Panel
from elkm1_lib.elk import Elk
from elkm1_lib.util import parse_url
import voluptuous as vol
@@ -26,12 +26,11 @@ from homeassistant.const import (
Platform,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.network import is_ip_address
from .const import (
@@ -62,6 +61,7 @@ from .discovery import (
async_update_entry_from_discovery,
)
from .models import ELKM1Data
from .services import async_setup_services
type ElkM1ConfigEntry = ConfigEntry[ELKM1Data]
@@ -79,19 +79,6 @@ PLATFORMS = [
Platform.SWITCH,
]
SPEAK_SERVICE_SCHEMA = vol.Schema(
{
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
vol.Optional("prefix", default=""): cv.string,
}
)
SET_TIME_SERVICE_SCHEMA = vol.Schema(
{
vol.Optional("prefix", default=""): cv.string,
}
)
def hostname_from_url(url: str) -> str:
"""Return the hostname from a url."""
@@ -179,7 +166,7 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Elk M1 platform."""
_create_elk_services(hass)
async_setup_services(hass)
async def _async_discovery(*_: Any) -> None:
async_trigger_discovery(
@@ -326,17 +313,6 @@ def _included(ranges: list[tuple[int, int]], set_to: bool, values: list[bool]) -
values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
"""Search all config entries for a given prefix."""
for entry in hass.config_entries.async_entries(DOMAIN):
if not entry.runtime_data:
continue
elk_data: ELKM1Data = entry.runtime_data
if elk_data.prefix == prefix:
return elk_data.elk
return None
async def async_unload_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -390,39 +366,3 @@ async def async_wait_for_elk_to_sync(
_LOGGER.debug("Received %s event", name)
return success
@callback
def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel:
"""Get the ElkM1 panel from a service call."""
prefix = service.data["prefix"]
elk = _find_elk_by_prefix(hass, prefix)
if elk is None:
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
return elk.panel
def _create_elk_services(hass: HomeAssistant) -> None:
"""Create ElkM1 services."""
@callback
def _speak_word_service(service: ServiceCall) -> None:
_async_get_elk_panel(hass, service).speak_word(service.data["number"])
@callback
def _speak_phrase_service(service: ServiceCall) -> None:
_async_get_elk_panel(hass, service).speak_phrase(service.data["number"])
@callback
def _set_time_service(service: ServiceCall) -> None:
_async_get_elk_panel(hass, service).set_time(dt_util.now())
hass.services.async_register(
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
)
@@ -0,0 +1,77 @@
"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels."""
from __future__ import annotations
from elkm1_lib.elk import Elk, Panel
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .models import ELKM1Data
SPEAK_SERVICE_SCHEMA = vol.Schema(
{
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
vol.Optional("prefix", default=""): cv.string,
}
)
SET_TIME_SERVICE_SCHEMA = vol.Schema(
{
vol.Optional("prefix", default=""): cv.string,
}
)
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
"""Search all config entries for a given prefix."""
for entry in hass.config_entries.async_entries(DOMAIN):
if not entry.runtime_data:
continue
elk_data: ELKM1Data = entry.runtime_data
if elk_data.prefix == prefix:
return elk_data.elk
return None
@callback
def _async_get_elk_panel(service: ServiceCall) -> Panel:
"""Get the ElkM1 panel from a service call."""
prefix = service.data["prefix"]
elk = _find_elk_by_prefix(service.hass, prefix)
if elk is None:
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
return elk.panel
@callback
def _speak_word_service(service: ServiceCall) -> None:
_async_get_elk_panel(service).speak_word(service.data["number"])
@callback
def _speak_phrase_service(service: ServiceCall) -> None:
_async_get_elk_panel(service).speak_phrase(service.data["number"])
@callback
def _set_time_service(service: ServiceCall) -> None:
_async_get_elk_panel(service).set_time(dt_util.now())
def async_setup_services(hass: HomeAssistant) -> None:
"""Create ElkM1 services."""
hass.services.async_register(
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
)
@@ -2,7 +2,6 @@
from __future__ import annotations
import httpx
from pyenphase import Envoy
from homeassistant.config_entries import ConfigEntry
@@ -10,14 +9,9 @@ from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import (
DOMAIN,
OPTION_DISABLE_KEEP_ALIVE,
OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE,
PLATFORMS,
)
from .const import DOMAIN, PLATFORMS
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
@@ -25,19 +19,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
"""Set up Enphase Envoy from a config entry."""
host = entry.data[CONF_HOST]
options = entry.options
envoy = (
Envoy(
host,
httpx.AsyncClient(
verify=False, limits=httpx.Limits(max_keepalive_connections=0)
),
)
if options.get(
OPTION_DISABLE_KEEP_ALIVE, OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE
)
else Envoy(host, get_async_client(hass, verify_ssl=False))
)
session = async_create_clientsession(hass, verify_ssl=False)
envoy = Envoy(host, session)
coordinator = EnphaseUpdateCoordinator(hass, envoy, entry)
await coordinator.async_config_entry_first_refresh()
@@ -24,7 +24,7 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.helpers.typing import VolDictType
@@ -63,7 +63,7 @@ async def validate_input(
description_placeholders: dict[str, str],
) -> Envoy:
"""Validate the user input allows us to connect."""
envoy = Envoy(host, get_async_client(hass, verify_ssl=False))
envoy = Envoy(host, async_get_clientsession(hass, verify_ssl=False))
try:
await envoy.setup()
await envoy.authenticate(username=username, password=password)
@@ -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."
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==32.0.0",
"aioesphomeapi==32.2.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0"
],
+10 -41
View File
@@ -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
View 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")
@@ -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,
)
@@ -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"]
}
@@ -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 -2
View File
@@ -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
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "silver",
"requirements": ["aioimmich==0.9.0"]
"requirements": ["aioimmich==0.9.1"]
}
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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"]
}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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."]
}
+4 -30
View File
@@ -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
+5
View File
@@ -6,3 +6,8 @@ SERVICE_SEND_MESSAGE = "send_message"
FORMAT_HTML = "html"
FORMAT_TEXT = "text"
ATTR_FORMAT = "format" # optional message format
ATTR_IMAGES = "images" # optional images
CONF_ROOMS_REGEX = "^[!|#][^:]*:.*"
@@ -0,0 +1,61 @@
"""The Matrix bot component."""
from __future__ import annotations
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from .const import (
ATTR_FORMAT,
ATTR_IMAGES,
CONF_ROOMS_REGEX,
DOMAIN,
FORMAT_HTML,
FORMAT_TEXT,
SERVICE_SEND_MESSAGE,
)
if TYPE_CHECKING:
from . import MatrixBot
MESSAGE_FORMATS = [FORMAT_HTML, FORMAT_TEXT]
DEFAULT_MESSAGE_FORMAT = FORMAT_TEXT
SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema(
{
vol.Required(ATTR_MESSAGE): cv.string,
vol.Optional(ATTR_DATA, default={}): {
vol.Optional(ATTR_FORMAT, default=DEFAULT_MESSAGE_FORMAT): vol.In(
MESSAGE_FORMATS
),
vol.Optional(ATTR_IMAGES): vol.All(cv.ensure_list, [cv.string]),
},
vol.Required(ATTR_TARGET): vol.All(
cv.ensure_list, [cv.matches_regex(CONF_ROOMS_REGEX)]
),
}
)
async def _handle_send_message(call: ServiceCall) -> None:
"""Handle the send_message service call."""
matrix_bot: MatrixBot = call.hass.data[DOMAIN]
await matrix_bot.handle_send_message(call)
def register_services(hass: HomeAssistant) -> None:
"""Set up the Matrix bot component."""
hass.services.async_register(
DOMAIN,
SERVICE_SEND_MESSAGE,
_handle_send_message,
schema=SERVICE_SCHEMA_SEND_MESSAGE,
)
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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({}))
@@ -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:
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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, {})
+3 -3
View File
@@ -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)
+9 -2
View File
@@ -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,
)
+3 -3
View File
@@ -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)
+25 -7
View File
@@ -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",
+1 -1
View File
@@ -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"]
}
+12 -290
View File
@@ -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)
@@ -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": {
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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):
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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(
+3 -3
View File
@@ -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)
+46 -34
View File
@@ -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
+1 -1
View File
@@ -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
View 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,7 +9,7 @@ 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
@@ -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,14 +70,21 @@ 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
# 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
):
@@ -41,5 +41,5 @@
"iot_class": "local_push",
"loggers": ["switchbot"],
"quality_scale": "gold",
"requirements": ["PySwitchbot==0.64.1"]
"requirements": ["PySwitchbot==0.65.0"]
}
+8 -1
View File
@@ -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",
@@ -8,7 +8,7 @@ from types import ModuleType
from typing import Any
from telegram import Bot
from telegram.error import InvalidToken
from telegram.error import InvalidToken, TelegramError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
@@ -26,7 +26,11 @@ from homeassistant.core import (
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ServiceValidationError
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -418,6 +422,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: TelegramBotConfigEntry)
await bot.get_me()
except InvalidToken as err:
raise ConfigEntryAuthFailed("Invalid API token for Telegram Bot.") from err
except TelegramError as err:
raise ConfigEntryNotReady from err
p_type: str = entry.data[CONF_PLATFORM]
@@ -135,7 +135,7 @@ OPTIONS_SCHEMA: vol.Schema = vol.Schema(
class OptionsFlowHandler(OptionsFlow):
"""Options flow for webhooks."""
"""Options flow."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/telegram_bot",
"iot_class": "cloud_push",
"loggers": ["telegram"],
"quality_scale": "legacy",
"quality_scale": "bronze",
"requirements": ["python-telegram-bot[socks]==21.5"]
}
@@ -0,0 +1,72 @@
rules:
# Bronze
action-setup: done
appropriate-polling:
status: exempt
comment: |
The integration provides webhooks (push based), polling (long polling) and broadcast (no data fetching) platforms which do not have interval polling.
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
The integration does not provide any entities.
entity-unique-id:
status: exempt
comment: |
The integration does not provide any entities.
has-entity-name:
status: exempt
comment: |
The integration does not provide any entities.
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: todo
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: done
test-coverage: todo
# Gold
devices: todo
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
@@ -10,7 +10,7 @@
"tensorflow==2.5.0",
"tf-models-official==2.5.0",
"pycocotools==2.0.6",
"numpy==2.2.6",
"numpy==2.3.0",
"Pillow==11.2.1"
]
}
@@ -32,7 +32,7 @@ from .coordinator import (
)
from .helpers import flatten
from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
from .services import async_register_services
from .services import async_setup_services
PLATFORMS: Final = [
Platform.BINARY_SENSOR,
@@ -56,7 +56,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Telemetry integration."""
async_register_services(hass)
async_setup_services(hass)
return True
@@ -441,6 +441,7 @@ class TeslemetryStreamingRearTrunkEntity(
"""Update the entity attributes."""
self._attr_is_closed = None if value is None else not value
self.async_write_ha_state()
class TeslemetrySunroofEntity(TeslemetryVehiclePollingEntity, CoverEntity):
@@ -309,6 +309,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfLength.MILES,
device_class=SensorDeviceClass.DISTANCE,
suggested_display_precision=1,
entity_registry_enabled_default=False,
),
TeslemetryVehicleSensorEntityDescription(
key="charge_state_est_battery_range",
@@ -320,7 +321,6 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfLength.MILES,
device_class=SensorDeviceClass.DISTANCE,
suggested_display_precision=1,
entity_registry_enabled_default=False,
),
TeslemetryVehicleSensorEntityDescription(
key="charge_state_ideal_battery_range",
@@ -332,7 +332,6 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfLength.MILES,
device_class=SensorDeviceClass.DISTANCE,
suggested_display_precision=1,
entity_registry_enabled_default=False,
),
TeslemetryVehicleSensorEntityDescription(
key="drive_state_speed",
@@ -98,7 +98,7 @@ def async_get_energy_site_for_entry(
return energy_data
def async_register_services(hass: HomeAssistant) -> None:
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the Teslemetry services."""
async def navigate_gps_request(call: ServiceCall) -> None:
+3 -1
View File
@@ -168,6 +168,8 @@ class TessieExportRuleSelectEntity(TessieEnergyEntity, SelectEntity):
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await handle_command(self.api.grid_import_export(option))
await handle_command(
self.api.grid_import_export(customer_preferred_export_rule=option)
)
self._attr_current_option = option
self.async_write_ha_state()
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "helper",
"iot_class": "calculated",
"quality_scale": "internal",
"requirements": ["numpy==2.2.6"]
"requirements": ["numpy==2.3.0"]
}
@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==7.10.1", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==7.12.0", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
@@ -17,7 +17,7 @@
"tariffs": "Supported tariffs"
},
"data_description": {
"always_available": "If activated, the sensor will always be show the last known value, even if the source entity is unavailable or unknown.",
"always_available": "If activated, the sensor will always show the last known value, even if the source entity is unavailable or unknown.",
"delta_values": "Enable if the source values are delta values since the last reading instead of absolute values.",
"net_consumption": "Enable if the source is a net meter, meaning it can both increase and decrease.",
"periodically_resetting": "Enable if the source may periodically reset to 0, for example at boot of the measuring device. If disabled, new readings are directly recorded after data inavailability.",
@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.73"]
"requirements": ["holidays==0.74"]
}
+2 -2
View File
@@ -29,7 +29,7 @@ from . import api
from .const import ATTR_LORA_INFO, DOMAIN, YOLINK_EVENT
from .coordinator import YoLinkCoordinator
from .device_trigger import CONF_LONG_PRESS, CONF_SHORT_PRESS
from .services import async_register_services
from .services import async_setup_services
SCAN_INTERVAL = timedelta(minutes=5)
@@ -111,7 +111,7 @@ class YoLinkHomeStore:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up YoLink."""
async_register_services(hass)
async_setup_services(hass)
return True
+1 -1
View File
@@ -25,7 +25,7 @@ _SPEAKER_HUB_PLAY_CALL_OPTIONAL_ATTRS = (
)
def async_register_services(hass: HomeAssistant) -> None:
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services for YoLink integration."""
async def handle_speaker_hub_play_call(service_call: ServiceCall) -> None:
+9 -1
View File
@@ -10,13 +10,14 @@ from zha.exceptions import ZHAException
from zigpy.application import ControllerApplication
from homeassistant.components.update import (
ATTR_LATEST_VERSION,
UpdateDeviceClass,
UpdateEntity,
UpdateEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -195,3 +196,10 @@ class ZHAFirmwareUpdateEntity(
"""Update the entity."""
await CoordinatorEntity.async_update(self)
await super().async_update()
@callback
def restore_external_state_attributes(self, state: State) -> None:
"""Restore entity state."""
self.entity_data.entity.restore_external_state_attributes(
latest_version=state.attributes.get(ATTR_LATEST_VERSION),
)
@@ -7,8 +7,6 @@ import voluptuous as vol
from zoneminder.zm import ZoneMinder
from homeassistant.const import (
ATTR_ID,
ATTR_NAME,
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
@@ -17,11 +15,14 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .services import register_services
_LOGGER = logging.getLogger(__name__)
CONF_PATH_ZMS = "path_zms"
@@ -31,7 +32,6 @@ DEFAULT_PATH_ZMS = "/zm/cgi-bin/nph-zms"
DEFAULT_SSL = False
DEFAULT_TIMEOUT = 10
DEFAULT_VERIFY_SSL = True
DOMAIN = "zoneminder"
HOST_CONFIG_SCHEMA = vol.Schema(
{
@@ -49,11 +49,6 @@ CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA])}, extra=vol.ALLOW_EXTRA
)
SERVICE_SET_RUN_STATE = "set_run_state"
SET_RUN_STATE_SCHEMA = vol.Schema(
{vol.Required(ATTR_ID): cv.string, vol.Required(ATTR_NAME): cv.string}
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the ZoneMinder component."""
@@ -86,22 +81,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
ex,
)
def set_active_state(call: ServiceCall) -> None:
"""Set the ZoneMinder run state to the given state name."""
zm_id = call.data[ATTR_ID]
state_name = call.data[ATTR_NAME]
if zm_id not in hass.data[DOMAIN]:
_LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id)
if not hass.data[DOMAIN][zm_id].set_active_state(state_name):
_LOGGER.error(
"Unable to change ZoneMinder state. Host: %s, state: %s",
zm_id,
state_name,
)
hass.services.async_register(
DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, schema=SET_RUN_STATE_SCHEMA
)
register_services(hass)
hass.async_create_task(
async_load_platform(hass, Platform.BINARY_SENSOR, DOMAIN, {}, config)
@@ -0,0 +1,3 @@
"""Support for ZoneMinder."""
DOMAIN = "zoneminder"
@@ -0,0 +1,40 @@
"""Support for ZoneMinder."""
import logging
import voluptuous as vol
from homeassistant.const import ATTR_ID, ATTR_NAME
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SERVICE_SET_RUN_STATE = "set_run_state"
SET_RUN_STATE_SCHEMA = vol.Schema(
{vol.Required(ATTR_ID): cv.string, vol.Required(ATTR_NAME): cv.string}
)
def _set_active_state(call: ServiceCall) -> None:
"""Set the ZoneMinder run state to the given state name."""
zm_id = call.data[ATTR_ID]
state_name = call.data[ATTR_NAME]
if zm_id not in call.hass.data[DOMAIN]:
_LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id)
if not call.hass.data[DOMAIN][zm_id].set_active_state(state_name):
_LOGGER.error(
"Unable to change ZoneMinder state. Host: %s, state: %s",
zm_id,
state_name,
)
def register_services(hass: HomeAssistant) -> None:
"""Register ZoneMinder services."""
hass.services.async_register(
DOMAIN, SERVICE_SET_RUN_STATE, _set_active_state, schema=SET_RUN_STATE_SCHEMA
)
@@ -133,7 +133,7 @@ from .helpers import (
get_valueless_base_unique_id,
)
from .migrate import async_migrate_discovered_value
from .services import ZWaveServices
from .services import async_setup_services
CONNECT_TIMEOUT = 10
DATA_DRIVER_EVENTS = "driver_events"
@@ -177,10 +177,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
entry, unique_id=str(entry.unique_id)
)
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)
services = ZWaveServices(hass, ent_reg, dev_reg)
services.async_register()
async_setup_services(hass)
return True
@@ -58,6 +58,12 @@ TARGET_VALIDATORS = {
}
def async_setup_services(hass: HomeAssistant) -> None:
"""Register integration services."""
services = ZWaveServices(hass, er.async_get(hass), dr.async_get(hass))
services.async_register()
def parameter_name_does_not_need_bitmask(
val: dict[str, int | str | list[str]],
) -> dict[str, int | str | list[str]]:

Some files were not shown because too many files have changed in this diff Show More