Compare commits

..

1 Commits

Author SHA1 Message Date
farmio 7aea90c301 Move KNXModule class to separate module 2025-06-03 09:38:52 +02:00
238 changed files with 2241 additions and 4547 deletions
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.19
uses: github/codeql-action/init@v3.28.18
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.19
uses: github/codeql-action/analyze@v3.28.18
with:
category: "/language:python"
+4 -44
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,34 +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 -n "cython==3.1.2" >> requirements_custom.txt
- name: Modify requirements file for custom build
if: contains(fromJSON('["armv7", "armhf"]'), matrix.arch)
id: modify-requirements
run: |
echo " # force update" >> requirements_custom.txt
echo "skip_binary=cython" >> $GITHUB_OUTPUT
- 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: ${{ steps.modify-requirements.outputs.skip_binary }}
constraints: "homeassistant/package_constraints.txt"
requirements: "requirements_custom.txt"
verbose: true
- name: Build wheels
uses: home-assistant/wheels@2025.03.0
if: false
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
+73 -3
View File
@@ -14,24 +14,30 @@ 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
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
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
from .services import async_setup_services
SERVICE_SETTINGS = "change_setting"
SERVICE_CAPTURE_IMAGE = "capture_image"
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
ATTR_DEVICE_NAME = "device_name"
ATTR_DEVICE_TYPE = "device_type"
@@ -39,12 +45,22 @@ 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,
@@ -69,7 +85,7 @@ class AbodeSystem:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Abode component."""
async_setup_services(hass)
setup_hass_services(hass)
return True
@@ -122,6 +138,60 @@ 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."""
@@ -1,89 +0,0 @@
"""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
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
from .const import CONNECTION_TYPE, LOCAL
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
PLATFORMS = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
+1 -24
View File
@@ -41,30 +41,7 @@ class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Fetch data from the Adax."""
try:
if hasattr(self.adax_data_handler, "fetch_rooms_info"):
rooms = await self.adax_data_handler.fetch_rooms_info() or []
_LOGGER.debug("fetch_rooms_info returned: %s", rooms)
else:
_LOGGER.debug("fetch_rooms_info method not available, using get_rooms")
rooms = []
if not rooms:
_LOGGER.debug(
"No rooms from fetch_rooms_info, trying get_rooms as fallback"
)
rooms = await self.adax_data_handler.get_rooms() or []
_LOGGER.debug("get_rooms fallback returned: %s", rooms)
if not rooms:
raise UpdateFailed("No rooms available from Adax API")
except OSError as e:
raise UpdateFailed(f"Error communicating with API: {e}") from e
for room in rooms:
room["energyWh"] = int(room.get("energyWh", 0))
rooms = await self.adax_data_handler.get_rooms() or []
return {r["id"]: r for r in rooms}
-77
View File
@@ -1,77 +0,0 @@
"""Support for Adax energy sensors."""
from __future__ import annotations
from typing import cast
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import UnitOfEnergy
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AdaxConfigEntry
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
from .coordinator import AdaxCloudCoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: AdaxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Adax energy sensors with config flow."""
if entry.data.get(CONNECTION_TYPE) != LOCAL:
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
# Create individual energy sensors for each device
async_add_entities(
AdaxEnergySensor(cloud_coordinator, device_id)
for device_id in cloud_coordinator.data
)
class AdaxEnergySensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
"""Representation of an Adax energy sensor."""
_attr_has_entity_name = True
_attr_translation_key = "energy"
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
_attr_suggested_display_precision = 3
def __init__(
self,
coordinator: AdaxCloudCoordinator,
device_id: str,
) -> None:
"""Initialize the energy sensor."""
super().__init__(coordinator)
self._device_id = device_id
room = coordinator.data[device_id]
self._attr_unique_id = f"{room['homeId']}_{device_id}_energy"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=room["name"],
manufacturer="Adax",
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available and "energyWh" in self.coordinator.data[self._device_id]
)
@property
def native_value(self) -> int:
"""Return the native value of the sensor."""
return int(self.coordinator.data[self._device_id]["energyWh"])
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairq"],
"requirements": ["aioairq==0.4.6"]
"requirements": ["aioairq==0.4.4"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
"iot_class": "local_push",
"loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.3.0"]
"requirements": ["airtouch5py==0.2.11"]
}
@@ -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) or {}
model = model_details.get("model")
model_details = coordinator.api.get_model_details(self.device)
model = model_details["model"] if model_details else None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=model,
model_id=self.device.device_type,
manufacturer=model_details.get("manufacturer", "Amazon"),
hw_version=model_details.get("hw_version"),
manufacturer="Amazon",
hw_version=model_details["hw_version"] if model_details else None,
sw_version=(
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
),
+50 -4
View File
@@ -16,7 +16,10 @@ 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,
@@ -27,17 +30,21 @@ 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, callback
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import Unauthorized, UnknownUser
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 STREAM_SOURCE_LIST
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
from .const import (
CAMERAS,
COMM_RETRIES,
@@ -51,7 +58,6 @@ 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__)
@@ -449,7 +455,47 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if not hass.data[DATA_AMCREST][DEVICES]:
return False
async_setup_services(hass)
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])
return True
@@ -1,61 +0,0 @@
"""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])
@@ -6,7 +6,6 @@ from homeassistant.components.water_heater import (
STATE_ECO,
STATE_PERFORMANCE,
WaterHeaterEntity,
WaterHeaterEntityFeature,
)
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant
@@ -33,7 +32,6 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
"""Representation of an ATAG water heater."""
_attr_operation_list = OPERATION_LIST
_attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
@property
@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.1",
"dbus-fast==2.43.0",
"habluetooth==3.49.0"
"habluetooth==3.48.2"
]
}
@@ -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, True, False, True)
super().__init__(panel, area_id, unique_id, False, 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==2.1.0"]
"requirements": ["python-bsblan==1.2.1"]
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/compensation",
"iot_class": "calculated",
"quality_scale": "legacy",
"requirements": ["numpy==2.3.0"]
"requirements": ["numpy==2.2.6"]
}
+3 -4
View File
@@ -5,7 +5,6 @@ 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
@@ -35,7 +34,7 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=120)
def sort_ips(ips: list, querytype: Literal["A", "AAAA"]) -> list:
def sort_ips(ips: list, querytype: str) -> list:
"""Join IPs into a single string."""
if querytype == "AAAA":
@@ -90,7 +89,7 @@ class WanIpSensor(SensorEntity):
self.hostname = hostname
self.resolver = aiodns.DNSResolver(tcp_port=port, udp_port=port)
self.resolver.nameservers = [resolver]
self.querytype: Literal["A", "AAAA"] = "AAAA" if ipv6 else "A"
self.querytype = "AAAA" if ipv6 else "A"
self._retries = DEFAULT_RETRIES
self._attr_extra_state_attributes = {
"resolver": resolver,
@@ -107,7 +106,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)
response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload]
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 async_setup_services
from .services import register_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
async_setup_services(hass)
register_services(hass)
return True
@@ -141,7 +141,7 @@ def download_file(service: ServiceCall) -> None:
threading.Thread(target=do_download).start()
def async_setup_services(hass: HomeAssistant) -> None:
def register_services(hass: HomeAssistant) -> None:
"""Register the services for the downloader component."""
async_register_admin_service(
hass,
@@ -1,6 +1 @@
"""The eddystone_temperature component."""
DOMAIN = "eddystone_temperature"
CONF_BEACONS = "beacons"
CONF_INSTANCE = "instance"
CONF_NAMESPACE = "namespace"
@@ -23,18 +23,17 @@ from homeassistant.const import (
STATE_UNKNOWN,
UnitOfTemperature,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_BEACONS = "beacons"
CONF_BT_DEVICE_ID = "bt_device_id"
CONF_INSTANCE = "instance"
CONF_NAMESPACE = "namespace"
BEACON_SCHEMA = vol.Schema(
{
@@ -59,21 +58,6 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Validate configuration, create devices and start monitoring thread."""
create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_yaml_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Eddystone",
},
)
bt_device_id: int = config[CONF_BT_DEVICE_ID]
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
+65 -5
View File
@@ -8,7 +8,7 @@ import re
from typing import Any
from elkm1_lib.elements import Element
from elkm1_lib.elk import Elk
from elkm1_lib.elk import Elk, Panel
from elkm1_lib.util import parse_url
import voluptuous as vol
@@ -26,11 +26,12 @@ from homeassistant.const import (
Platform,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
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 (
@@ -61,7 +62,6 @@ from .discovery import (
async_update_entry_from_discovery,
)
from .models import ELKM1Data
from .services import async_setup_services
type ElkM1ConfigEntry = ConfigEntry[ELKM1Data]
@@ -79,6 +79,19 @@ 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."""
@@ -166,7 +179,7 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Elk M1 platform."""
async_setup_services(hass)
_create_elk_services(hass)
async def _async_discovery(*_: Any) -> None:
async_trigger_discovery(
@@ -313,6 +326,17 @@ 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)
@@ -366,3 +390,39 @@ 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
)
@@ -1,77 +0,0 @@
"""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,6 +2,7 @@
from __future__ import annotations
import httpx
from pyenphase import Envoy
from homeassistant.config_entries import ConfigEntry
@@ -9,9 +10,14 @@ 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.aiohttp_client import async_create_clientsession
from homeassistant.helpers.httpx_client import get_async_client
from .const import DOMAIN, PLATFORMS
from .const import (
DOMAIN,
OPTION_DISABLE_KEEP_ALIVE,
OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE,
PLATFORMS,
)
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
@@ -19,8 +25,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
"""Set up Enphase Envoy from a config entry."""
host = entry.data[CONF_HOST]
session = async_create_clientsession(hass, verify_ssl=False)
envoy = Envoy(host, session)
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))
)
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.aiohttp_client import async_get_clientsession
from homeassistant.helpers.httpx_client import get_async_client
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, async_get_clientsession(hass, verify_ssl=False))
envoy = Envoy(host, get_async_client(hass, verify_ssl=False))
try:
await envoy.setup()
await envoy.authenticate(username=username, password=password)
@@ -6,7 +6,6 @@ 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
@@ -70,14 +69,14 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
for end_point in end_points:
try:
response: ClientResponse = await envoy.request(end_point)
fixture_data[end_point] = (
(await response.text()).replace("\n", "").replace(serial, CLEAN_TEXT)
response = await envoy.request(end_point)
fixture_data[end_point] = 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": response.status_code,
}
)
except EnvoyError as err:
@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from typing import Any, Concatenate
from aiohttp import ClientError
from httpx import HTTPError
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, ClientError)
ACTIONERRORS = (EnvoyError, HTTPError)
class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator]):
@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.0.1"],
"requirements": ["pyenphase==1.26.1"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"]
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
}
@@ -17,9 +17,9 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==32.2.0",
"aioesphomeapi==31.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0"
"bleak-esphome==2.15.1"
],
"zeroconf": ["_esphomelib._tcp.local."]
}
@@ -71,11 +71,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
_attr_name = "DHW controller"
_attr_icon = "mdi:thermometer-lines"
_attr_operation_list = list(HA_STATE_TO_EVO)
_attr_supported_features = (
WaterHeaterEntityFeature.AWAY_MODE
| WaterHeaterEntityFeature.ON_OFF
| WaterHeaterEntityFeature.OPERATION_MODE
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_evo_device: evo.HotWater
@@ -96,6 +91,9 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
self._attr_precision = (
PRECISION_TENTHS if coordinator.client_v1 else PRECISION_WHOLE
)
self._attr_supported_features = (
WaterHeaterEntityFeature.AWAY_MODE | WaterHeaterEntityFeature.OPERATION_MODE
)
@property
def current_operation(self) -> str | None:
+41 -10
View File
@@ -11,25 +11,32 @@ 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, callback
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
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
from .const import (
DOMAIN,
SIGNAL_FFMPEG_RESTART,
SIGNAL_FFMPEG_START,
SIGNAL_FFMPEG_STOP,
)
from .services import async_setup_services
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")
DATA_FFMPEG = "ffmpeg"
@@ -56,6 +63,8 @@ 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."""
@@ -65,7 +74,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await manager.async_get_version()
async_setup_services(hass)
# 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
)
hass.data[DATA_FFMPEG] = manager
return True
-9
View File
@@ -1,9 +0,0 @@
"""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")
@@ -1,51 +0,0 @@
"""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
)
@@ -84,7 +84,6 @@ async def async_setup_entry(
name=f"Freebox {sensor_name}",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
)
for sensor_name in router.sensors_temperature
@@ -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_setup_services
from .services import async_register_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_setup_services(hass)
async_register_services(hass)
return True
@@ -77,7 +77,7 @@ def _read_file_contents(
return results
def async_setup_services(hass: HomeAssistant) -> None:
def async_register_services(hass: HomeAssistant) -> None:
"""Register Google Photos services."""
async def async_handle_upload(call: ServiceCall) -> ServiceResponse:
@@ -2,33 +2,48 @@
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_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
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.typing import ConfigType
from homeassistant.helpers.selector import ConfigEntrySelector
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"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Activate the Google Sheets component."""
SERVICE_APPEND_SHEET = "append_sheet"
async_setup_services(hass)
return True
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 def async_setup_entry(
@@ -52,6 +67,8 @@ 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
@@ -64,4 +81,55 @@ 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,
)
@@ -1,87 +0,0 @@
"""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,
)
@@ -73,9 +73,7 @@ async def async_setup_entry(
class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
"""Hive Water Heater Device."""
_attr_supported_features = (
WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE
)
_attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_operation_list = SUPPORT_WATER_HEATER
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.74", "babel==2.15.0"]
"requirements": ["holidays==0.73", "babel==2.15.0"]
}
@@ -83,54 +83,3 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=AUTH_SCHEMA,
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the reconfigure flow."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input:
self.homee = Homee(
user_input[CONF_HOST],
reconfigure_entry.data[CONF_USERNAME],
reconfigure_entry.data[CONF_PASSWORD],
)
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = "cannot_connect"
except HomeeAuthenticationFailedException:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
self.hass.loop.create_task(self.homee.run())
await self.homee.wait_until_connected()
self.homee.disconnect()
await self.homee.wait_until_disconnected()
await self.async_set_unique_id(self.homee.settings.uid)
self._abort_if_unique_id_mismatch(reason="wrong_hub")
_LOGGER.debug("Updated homee entry with ID %s", self.homee.settings.uid)
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(), data_updates=user_input
)
return self.async_show_form(
data_schema=vol.Schema(
{
vol.Required(
CONF_HOST, default=reconfigure_entry.data[CONF_HOST]
): str
}
),
description_placeholders={
"name": reconfigure_entry.runtime_data.settings.uid
},
errors=errors,
)
+1 -13
View File
@@ -2,9 +2,7 @@
"config": {
"flow_title": "homee {name} ({host})",
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"wrong_hub": "Address belongs to a different homee."
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -24,16 +22,6 @@
"username": "The username for your homee.",
"password": "The password for your homee."
}
},
"reconfigure": {
"title": "Reconfigure homee {name}",
"description": "Reconfigure the IP address of your homee.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The IP address of your homee."
}
}
}
},
@@ -21,7 +21,7 @@ from .const import (
HMIPC_NAME,
)
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .services import async_setup_services
from .services import async_setup_services, async_unload_services
CONFIG_SCHEMA = vol.Schema(
{
@@ -116,6 +116,8 @@ async def async_unload_entry(
assert hap.reset_connection_listener is not None
hap.reset_connection_listener()
await async_unload_services(hass)
return await hap.async_reset()
@@ -123,29 +123,32 @@ SCHEMA_SET_HOME_COOLING_MODE = vol.Schema(
async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the HomematicIP Cloud services."""
if hass.services.async_services_for_domain(DOMAIN):
return
@verify_domain_control(hass, DOMAIN)
async def async_call_hmipc_service(service: ServiceCall) -> None:
"""Call correct HomematicIP Cloud service."""
service_name = service.service
if service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION:
await _async_activate_eco_mode_with_duration(service)
await _async_activate_eco_mode_with_duration(hass, service)
elif service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD:
await _async_activate_eco_mode_with_period(service)
await _async_activate_eco_mode_with_period(hass, service)
elif service_name == SERVICE_ACTIVATE_VACATION:
await _async_activate_vacation(service)
await _async_activate_vacation(hass, service)
elif service_name == SERVICE_DEACTIVATE_ECO_MODE:
await _async_deactivate_eco_mode(service)
await _async_deactivate_eco_mode(hass, service)
elif service_name == SERVICE_DEACTIVATE_VACATION:
await _async_deactivate_vacation(service)
await _async_deactivate_vacation(hass, service)
elif service_name == SERVICE_DUMP_HAP_CONFIG:
await _async_dump_hap_config(service)
await _async_dump_hap_config(hass, service)
elif service_name == SERVICE_RESET_ENERGY_COUNTER:
await _async_reset_energy_counter(service)
await _async_reset_energy_counter(hass, service)
elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE:
await _set_active_climate_profile(service)
await _set_active_climate_profile(hass, service)
elif service_name == SERVICE_SET_HOME_COOLING_MODE:
await _async_set_home_cooling_mode(service)
await _async_set_home_cooling_mode(hass, service)
hass.services.async_register(
domain=DOMAIN,
@@ -214,75 +217,90 @@ async def async_setup_services(hass: HomeAssistant) -> None:
)
async def _async_activate_eco_mode_with_duration(service: ServiceCall) -> None:
async def async_unload_services(hass: HomeAssistant):
"""Unload HomematicIP Cloud services."""
if hass.config_entries.async_loaded_entries(DOMAIN):
return
for hmipc_service in HMIPC_SERVICES:
hass.services.async_remove(domain=DOMAIN, service=hmipc_service)
async def _async_activate_eco_mode_with_duration(
hass: HomeAssistant, service: ServiceCall
) -> None:
"""Service to activate eco mode with duration."""
duration = service.data[ATTR_DURATION]
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(service.hass, hapid):
if home := _get_home(hass, hapid):
await home.activate_absence_with_duration_async(duration)
else:
entry: HomematicIPConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.activate_absence_with_duration_async(duration)
async def _async_activate_eco_mode_with_period(service: ServiceCall) -> None:
async def _async_activate_eco_mode_with_period(
hass: HomeAssistant, service: ServiceCall
) -> None:
"""Service to activate eco mode with period."""
endtime = service.data[ATTR_ENDTIME]
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(service.hass, hapid):
if home := _get_home(hass, hapid):
await home.activate_absence_with_period_async(endtime)
else:
entry: HomematicIPConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.activate_absence_with_period_async(endtime)
async def _async_activate_vacation(service: ServiceCall) -> None:
async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
"""Service to activate vacation."""
endtime = service.data[ATTR_ENDTIME]
temperature = service.data[ATTR_TEMPERATURE]
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(service.hass, hapid):
if home := _get_home(hass, hapid):
await home.activate_vacation_async(endtime, temperature)
else:
entry: HomematicIPConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.activate_vacation_async(endtime, temperature)
async def _async_deactivate_eco_mode(service: ServiceCall) -> None:
async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None:
"""Service to deactivate eco mode."""
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(service.hass, hapid):
if home := _get_home(hass, hapid):
await home.deactivate_absence_async()
else:
entry: HomematicIPConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.deactivate_absence_async()
async def _async_deactivate_vacation(service: ServiceCall) -> None:
async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
"""Service to deactivate vacation."""
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(service.hass, hapid):
if home := _get_home(hass, hapid):
await home.deactivate_vacation_async()
else:
entry: HomematicIPConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.deactivate_vacation_async()
async def _set_active_climate_profile(service: ServiceCall) -> None:
async def _set_active_climate_profile(
hass: HomeAssistant, service: ServiceCall
) -> None:
"""Service to set the active climate profile."""
entity_id_list = service.data[ATTR_ENTITY_ID]
climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1
entry: HomematicIPConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
if entity_id_list != "all":
for entity_id in entity_id_list:
group = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
@@ -294,16 +312,16 @@ async def _set_active_climate_profile(service: ServiceCall) -> None:
await group.set_active_profile_async(climate_profile_index)
async def _async_dump_hap_config(service: ServiceCall) -> None:
async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> None:
"""Service to dump the configuration of a Homematic IP Access Point."""
config_path: str = (
service.data.get(ATTR_CONFIG_OUTPUT_PATH) or service.hass.config.config_dir
service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir
)
config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX]
anonymize = service.data[ATTR_ANONYMIZE]
entry: HomematicIPConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
hap_sgtin = entry.unique_id
assert hap_sgtin is not None
@@ -320,12 +338,12 @@ async def _async_dump_hap_config(service: ServiceCall) -> None:
config_file.write_text(json_state, encoding="utf8")
async def _async_reset_energy_counter(service: ServiceCall):
async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall):
"""Service to reset the energy counter."""
entity_id_list = service.data[ATTR_ENTITY_ID]
entry: HomematicIPConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
if entity_id_list != "all":
for entity_id in entity_id_list:
device = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
@@ -337,16 +355,16 @@ async def _async_reset_energy_counter(service: ServiceCall):
await device.reset_energy_counter_async()
async def _async_set_home_cooling_mode(service: ServiceCall):
async def _async_set_home_cooling_mode(hass: HomeAssistant, service: ServiceCall):
"""Service to set the cooling mode."""
cooling = service.data[ATTR_COOLING]
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
if home := _get_home(service.hass, hapid):
if home := _get_home(hass, hapid):
await home.set_cooling_async(cooling)
else:
entry: HomematicIPConfigEntry
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
await entry.runtime_data.home.set_cooling_async(cooling)
+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_setup_services
from .services import async_register_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_setup_services(hass)
async_register_services(hass)
return True
+1 -1
View File
@@ -25,7 +25,7 @@ from .const import (
LOGGER = logging.getLogger(__name__)
def async_setup_services(hass: HomeAssistant) -> None:
def async_register_services(hass: HomeAssistant) -> None:
"""Register services for Hue integration."""
async def hue_activate_scene(call: ServiceCall, skip_reload=True) -> None:
+3 -14
View File
@@ -6,31 +6,18 @@ from typing import Any
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.storage import Store
from homeassistant.helpers.typing import ConfigType
from .account import IcloudAccount, IcloudConfigEntry
from .const import (
CONF_GPS_ACCURACY_THRESHOLD,
CONF_MAX_INTERVAL,
CONF_WITH_FAMILY,
DOMAIN,
PLATFORMS,
STORAGE_KEY,
STORAGE_VERSION,
)
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up iCloud integration."""
async_setup_services(hass)
return True
from .services import register_services
async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bool:
@@ -64,6 +51,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bo
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
register_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 async_setup_services(hass: HomeAssistant) -> None:
"""Register iCloud services."""
def register_services(hass: HomeAssistant) -> None:
"""Set up an iCloud account from a config entry."""
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.1"]
"requirements": ["aioimmich==0.8.0"]
}
+2 -2
View File
@@ -25,9 +25,9 @@ from .const import (
DOMAIN,
INSTEON_PLATFORMS,
)
from .services import async_setup_services
from .utils import (
add_insteon_events,
async_register_services,
get_device_platforms,
register_new_device_callback,
)
@@ -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_setup_services(hass)
async_register_services(hass)
create_insteon_device(hass, devices.modem, entry.entry_id)
@@ -1,291 +0,0 @@
"""Utilities used by insteon component."""
from __future__ import annotations
import asyncio
import logging
from pyinsteon import devices
from pyinsteon.address import Address
from pyinsteon.managers.link_manager import (
async_enter_linking_mode,
async_enter_unlinking_mode,
)
from pyinsteon.managers.scene_manager import (
async_trigger_scene_off,
async_trigger_scene_on,
)
from pyinsteon.managers.x10_manager import (
async_x10_all_lights_off,
async_x10_all_lights_on,
async_x10_all_units_off,
)
from pyinsteon.x10_address import create as create_x10_address
from homeassistant.const import (
CONF_ADDRESS,
CONF_ENTITY_ID,
CONF_PLATFORM,
ENTITY_MATCH_ALL,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
dispatcher_send,
)
from .const import (
CONF_CAT,
CONF_DIM_STEPS,
CONF_HOUSECODE,
CONF_SUBCAT,
CONF_UNITCODE,
DOMAIN,
SIGNAL_ADD_DEFAULT_LINKS,
SIGNAL_ADD_DEVICE_OVERRIDE,
SIGNAL_ADD_X10_DEVICE,
SIGNAL_LOAD_ALDB,
SIGNAL_PRINT_ALDB,
SIGNAL_REMOVE_DEVICE_OVERRIDE,
SIGNAL_REMOVE_ENTITY,
SIGNAL_REMOVE_HA_DEVICE,
SIGNAL_REMOVE_INSTEON_DEVICE,
SIGNAL_REMOVE_X10_DEVICE,
SIGNAL_SAVE_DEVICES,
SRV_ADD_ALL_LINK,
SRV_ADD_DEFAULT_LINKS,
SRV_ALL_LINK_GROUP,
SRV_ALL_LINK_MODE,
SRV_CONTROLLER,
SRV_DEL_ALL_LINK,
SRV_HOUSECODE,
SRV_LOAD_ALDB,
SRV_LOAD_DB_RELOAD,
SRV_PRINT_ALDB,
SRV_PRINT_IM_ALDB,
SRV_SCENE_OFF,
SRV_SCENE_ON,
SRV_X10_ALL_LIGHTS_OFF,
SRV_X10_ALL_LIGHTS_ON,
SRV_X10_ALL_UNITS_OFF,
)
from .schemas import (
ADD_ALL_LINK_SCHEMA,
ADD_DEFAULT_LINKS_SCHEMA,
DEL_ALL_LINK_SCHEMA,
LOAD_ALDB_SCHEMA,
PRINT_ALDB_SCHEMA,
TRIGGER_SCENE_SCHEMA,
X10_HOUSECODE_SCHEMA,
)
from .utils import print_aldb_to_log
_LOGGER = logging.getLogger(__name__)
@callback
def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
"""Register services used by insteon component."""
save_lock = asyncio.Lock()
async def async_srv_add_all_link(service: ServiceCall) -> None:
"""Add an INSTEON All-Link between two devices."""
group = service.data[SRV_ALL_LINK_GROUP]
mode = service.data[SRV_ALL_LINK_MODE]
link_mode = mode.lower() == SRV_CONTROLLER
await async_enter_linking_mode(link_mode, group)
async def async_srv_del_all_link(service: ServiceCall) -> None:
"""Delete an INSTEON All-Link between two devices."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_enter_unlinking_mode(group)
async def async_srv_load_aldb(service: ServiceCall) -> None:
"""Load the device All-Link database."""
entity_id = service.data[CONF_ENTITY_ID]
reload = service.data[SRV_LOAD_DB_RELOAD]
if entity_id.lower() == ENTITY_MATCH_ALL:
await async_srv_load_aldb_all(reload)
else:
signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
async_dispatcher_send(hass, signal, reload)
async def async_srv_load_aldb_all(reload):
"""Load the All-Link database for all devices."""
# Cannot be done concurrently due to issues with the underlying protocol.
for address in devices:
device = devices[address]
if device != devices.modem and device.cat != 0x03:
await device.aldb.async_load(refresh=reload)
await async_srv_save_devices()
async def async_srv_save_devices():
"""Write the Insteon device configuration to file."""
async with save_lock:
_LOGGER.debug("Saving Insteon devices")
await devices.async_save(hass.config.config_dir)
def print_aldb(service: ServiceCall) -> None:
"""Print the All-Link Database for a device."""
# For now this sends logs to the log file.
# Future direction is to create an INSTEON control panel.
entity_id = service.data[CONF_ENTITY_ID]
signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
dispatcher_send(hass, signal)
def print_im_aldb(service: ServiceCall) -> None:
"""Print the All-Link Database for a device."""
# For now this sends logs to the log file.
# Future direction is to create an INSTEON control panel.
print_aldb_to_log(devices.modem.aldb)
async def async_srv_x10_all_units_off(service: ServiceCall) -> None:
"""Send the X10 All Units Off command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_units_off(housecode)
async def async_srv_x10_all_lights_off(service: ServiceCall) -> None:
"""Send the X10 All Lights Off command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_lights_off(housecode)
async def async_srv_x10_all_lights_on(service: ServiceCall) -> None:
"""Send the X10 All Lights On command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_lights_on(housecode)
async def async_srv_scene_on(service: ServiceCall) -> None:
"""Trigger an INSTEON scene ON."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_trigger_scene_on(group)
async def async_srv_scene_off(service: ServiceCall) -> None:
"""Trigger an INSTEON scene ON."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_trigger_scene_off(group)
@callback
def async_add_default_links(service: ServiceCall) -> None:
"""Add the default All-Link entries to a device."""
entity_id = service.data[CONF_ENTITY_ID]
signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}"
async_dispatcher_send(hass, signal)
async def async_add_device_override(override):
"""Remove an Insten device and associated entities."""
address = Address(override[CONF_ADDRESS])
await async_remove_ha_device(address)
devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0)
await async_srv_save_devices()
async def async_remove_device_override(address):
"""Remove an Insten device and associated entities."""
address = Address(address)
await async_remove_ha_device(address)
devices.set_id(address, None, None, None)
await devices.async_identify_device(address)
await async_srv_save_devices()
@callback
def async_add_x10_device(x10_config):
"""Add X10 device."""
housecode = x10_config[CONF_HOUSECODE]
unitcode = x10_config[CONF_UNITCODE]
platform = x10_config[CONF_PLATFORM]
steps = x10_config.get(CONF_DIM_STEPS, 22)
x10_type = "on_off"
if platform == "light":
x10_type = "dimmable"
elif platform == "binary_sensor":
x10_type = "sensor"
_LOGGER.debug(
"Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type
)
# This must be run in the event loop
devices.add_x10_device(housecode, unitcode, x10_type, steps)
async def async_remove_x10_device(housecode, unitcode):
"""Remove an X10 device and associated entities."""
address = create_x10_address(housecode, unitcode)
devices.pop(address)
await async_remove_ha_device(address)
async def async_remove_ha_device(address: Address, remove_all_refs: bool = False):
"""Remove the device and all entities from hass."""
signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}"
async_dispatcher_send(hass, signal)
dev_registry = dr.async_get(hass)
device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))})
if device:
dev_registry.async_remove_device(device.id)
async def async_remove_insteon_device(
address: Address, remove_all_refs: bool = False
):
"""Remove the underlying Insteon device from the network."""
await devices.async_remove_device(
address=address, force=False, remove_all_refs=remove_all_refs
)
await async_srv_save_devices()
hass.services.async_register(
DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_LOAD_ALDB, async_srv_load_aldb, schema=LOAD_ALDB_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
)
hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_UNITS_OFF,
async_srv_x10_all_units_off,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_LIGHTS_OFF,
async_srv_x10_all_lights_off,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_LIGHTS_ON,
async_srv_x10_all_lights_on,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA
)
hass.services.async_register(
DOMAIN,
SRV_ADD_DEFAULT_LINKS,
async_add_default_links,
schema=ADD_DEFAULT_LINKS_SCHEMA,
)
async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices)
async_dispatcher_connect(
hass, SIGNAL_ADD_DEVICE_OVERRIDE, async_add_device_override
)
async_dispatcher_connect(
hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, async_remove_device_override
)
async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device)
async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device)
async_dispatcher_connect(hass, SIGNAL_REMOVE_HA_DEVICE, async_remove_ha_device)
async_dispatcher_connect(
hass, SIGNAL_REMOVE_INSTEON_DEVICE, async_remove_insteon_device
)
_LOGGER.debug("Insteon Services registered")
+276 -4
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
import logging
from typing import TYPE_CHECKING, Any
@@ -11,25 +12,90 @@ from pyinsteon.address import Address
from pyinsteon.constants import ALDBStatus, DeviceAction
from pyinsteon.device_types.device_base import Device
from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT, Event
from pyinsteon.managers.link_manager import (
async_enter_linking_mode,
async_enter_unlinking_mode,
)
from pyinsteon.managers.scene_manager import (
async_trigger_scene_off,
async_trigger_scene_on,
)
from pyinsteon.managers.x10_manager import (
async_x10_all_lights_off,
async_x10_all_lights_on,
async_x10_all_units_off,
)
from pyinsteon.x10_address import create as create_x10_address
from serial.tools import list_ports
from homeassistant.components import usb
from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import (
CONF_ADDRESS,
CONF_ENTITY_ID,
CONF_PLATFORM,
ENTITY_MATCH_ALL,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
CONF_CAT,
CONF_DIM_STEPS,
CONF_HOUSECODE,
CONF_SUBCAT,
CONF_UNITCODE,
DOMAIN,
EVENT_CONF_BUTTON,
EVENT_GROUP_OFF,
EVENT_GROUP_OFF_FAST,
EVENT_GROUP_ON,
EVENT_GROUP_ON_FAST,
SIGNAL_ADD_DEFAULT_LINKS,
SIGNAL_ADD_DEVICE_OVERRIDE,
SIGNAL_ADD_ENTITIES,
SIGNAL_ADD_X10_DEVICE,
SIGNAL_LOAD_ALDB,
SIGNAL_PRINT_ALDB,
SIGNAL_REMOVE_DEVICE_OVERRIDE,
SIGNAL_REMOVE_ENTITY,
SIGNAL_REMOVE_HA_DEVICE,
SIGNAL_REMOVE_INSTEON_DEVICE,
SIGNAL_REMOVE_X10_DEVICE,
SIGNAL_SAVE_DEVICES,
SRV_ADD_ALL_LINK,
SRV_ADD_DEFAULT_LINKS,
SRV_ALL_LINK_GROUP,
SRV_ALL_LINK_MODE,
SRV_CONTROLLER,
SRV_DEL_ALL_LINK,
SRV_HOUSECODE,
SRV_LOAD_ALDB,
SRV_LOAD_DB_RELOAD,
SRV_PRINT_ALDB,
SRV_PRINT_IM_ALDB,
SRV_SCENE_OFF,
SRV_SCENE_ON,
SRV_X10_ALL_LIGHTS_OFF,
SRV_X10_ALL_LIGHTS_ON,
SRV_X10_ALL_UNITS_OFF,
)
from .ipdb import get_device_platform_groups, get_device_platforms
from .schemas import (
ADD_ALL_LINK_SCHEMA,
ADD_DEFAULT_LINKS_SCHEMA,
DEL_ALL_LINK_SCHEMA,
LOAD_ALDB_SCHEMA,
PRINT_ALDB_SCHEMA,
TRIGGER_SCENE_SCHEMA,
X10_HOUSECODE_SCHEMA,
)
if TYPE_CHECKING:
from .entity import InsteonEntity
@@ -88,7 +154,7 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
_register_event(event, async_fire_insteon_event)
def register_new_device_callback(hass: HomeAssistant) -> None:
def register_new_device_callback(hass):
"""Register callback for new Insteon device."""
@callback
@@ -114,6 +180,212 @@ def register_new_device_callback(hass: HomeAssistant) -> None:
devices.subscribe(async_new_insteon_device, force_strong_ref=True)
@callback
def async_register_services(hass): # noqa: C901
"""Register services used by insteon component."""
save_lock = asyncio.Lock()
async def async_srv_add_all_link(service: ServiceCall) -> None:
"""Add an INSTEON All-Link between two devices."""
group = service.data[SRV_ALL_LINK_GROUP]
mode = service.data[SRV_ALL_LINK_MODE]
link_mode = mode.lower() == SRV_CONTROLLER
await async_enter_linking_mode(link_mode, group)
async def async_srv_del_all_link(service: ServiceCall) -> None:
"""Delete an INSTEON All-Link between two devices."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_enter_unlinking_mode(group)
async def async_srv_load_aldb(service: ServiceCall) -> None:
"""Load the device All-Link database."""
entity_id = service.data[CONF_ENTITY_ID]
reload = service.data[SRV_LOAD_DB_RELOAD]
if entity_id.lower() == ENTITY_MATCH_ALL:
await async_srv_load_aldb_all(reload)
else:
signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
async_dispatcher_send(hass, signal, reload)
async def async_srv_load_aldb_all(reload):
"""Load the All-Link database for all devices."""
# Cannot be done concurrently due to issues with the underlying protocol.
for address in devices:
device = devices[address]
if device != devices.modem and device.cat != 0x03:
await device.aldb.async_load(refresh=reload)
await async_srv_save_devices()
async def async_srv_save_devices():
"""Write the Insteon device configuration to file."""
async with save_lock:
_LOGGER.debug("Saving Insteon devices")
await devices.async_save(hass.config.config_dir)
def print_aldb(service: ServiceCall) -> None:
"""Print the All-Link Database for a device."""
# For now this sends logs to the log file.
# Future direction is to create an INSTEON control panel.
entity_id = service.data[CONF_ENTITY_ID]
signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
dispatcher_send(hass, signal)
def print_im_aldb(service: ServiceCall) -> None:
"""Print the All-Link Database for a device."""
# For now this sends logs to the log file.
# Future direction is to create an INSTEON control panel.
print_aldb_to_log(devices.modem.aldb)
async def async_srv_x10_all_units_off(service: ServiceCall) -> None:
"""Send the X10 All Units Off command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_units_off(housecode)
async def async_srv_x10_all_lights_off(service: ServiceCall) -> None:
"""Send the X10 All Lights Off command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_lights_off(housecode)
async def async_srv_x10_all_lights_on(service: ServiceCall) -> None:
"""Send the X10 All Lights On command."""
housecode = service.data.get(SRV_HOUSECODE)
await async_x10_all_lights_on(housecode)
async def async_srv_scene_on(service: ServiceCall) -> None:
"""Trigger an INSTEON scene ON."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_trigger_scene_on(group)
async def async_srv_scene_off(service: ServiceCall) -> None:
"""Trigger an INSTEON scene ON."""
group = service.data.get(SRV_ALL_LINK_GROUP)
await async_trigger_scene_off(group)
@callback
def async_add_default_links(service: ServiceCall) -> None:
"""Add the default All-Link entries to a device."""
entity_id = service.data[CONF_ENTITY_ID]
signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}"
async_dispatcher_send(hass, signal)
async def async_add_device_override(override):
"""Remove an Insten device and associated entities."""
address = Address(override[CONF_ADDRESS])
await async_remove_ha_device(address)
devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0)
await async_srv_save_devices()
async def async_remove_device_override(address):
"""Remove an Insten device and associated entities."""
address = Address(address)
await async_remove_ha_device(address)
devices.set_id(address, None, None, None)
await devices.async_identify_device(address)
await async_srv_save_devices()
@callback
def async_add_x10_device(x10_config):
"""Add X10 device."""
housecode = x10_config[CONF_HOUSECODE]
unitcode = x10_config[CONF_UNITCODE]
platform = x10_config[CONF_PLATFORM]
steps = x10_config.get(CONF_DIM_STEPS, 22)
x10_type = "on_off"
if platform == "light":
x10_type = "dimmable"
elif platform == "binary_sensor":
x10_type = "sensor"
_LOGGER.debug(
"Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type
)
# This must be run in the event loop
devices.add_x10_device(housecode, unitcode, x10_type, steps)
async def async_remove_x10_device(housecode, unitcode):
"""Remove an X10 device and associated entities."""
address = create_x10_address(housecode, unitcode)
devices.pop(address)
await async_remove_ha_device(address)
async def async_remove_ha_device(address: Address, remove_all_refs: bool = False):
"""Remove the device and all entities from hass."""
signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}"
async_dispatcher_send(hass, signal)
dev_registry = dr.async_get(hass)
device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))})
if device:
dev_registry.async_remove_device(device.id)
async def async_remove_insteon_device(
address: Address, remove_all_refs: bool = False
):
"""Remove the underlying Insteon device from the network."""
await devices.async_remove_device(
address=address, force=False, remove_all_refs=remove_all_refs
)
await async_srv_save_devices()
hass.services.async_register(
DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_LOAD_ALDB, async_srv_load_aldb, schema=LOAD_ALDB_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
)
hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_UNITS_OFF,
async_srv_x10_all_units_off,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_LIGHTS_OFF,
async_srv_x10_all_lights_off,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SRV_X10_ALL_LIGHTS_ON,
async_srv_x10_all_lights_on,
schema=X10_HOUSECODE_SCHEMA,
)
hass.services.async_register(
DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA
)
hass.services.async_register(
DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA
)
hass.services.async_register(
DOMAIN,
SRV_ADD_DEFAULT_LINKS,
async_add_default_links,
schema=ADD_DEFAULT_LINKS_SCHEMA,
)
async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices)
async_dispatcher_connect(
hass, SIGNAL_ADD_DEVICE_OVERRIDE, async_add_device_override
)
async_dispatcher_connect(
hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, async_remove_device_override
)
async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device)
async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device)
async_dispatcher_connect(hass, SIGNAL_REMOVE_HA_DEVICE, async_remove_ha_device)
async_dispatcher_connect(
hass, SIGNAL_REMOVE_INSTEON_DEVICE, async_remove_insteon_device
)
_LOGGER.debug("Insteon Services registered")
def print_aldb_to_log(aldb):
"""Print the All-Link Database to the log file."""
logger = logging.getLogger(f"{__name__}.links")
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyiqvia"],
"requirements": ["numpy==2.3.0", "pyiqvia==2022.04.0"]
"requirements": ["numpy==2.2.6", "pyiqvia==2022.04.0"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyiskra"],
"requirements": ["pyiskra==0.1.21"]
"requirements": ["pyiskra==0.1.19"]
}
+11 -291
View File
@@ -1,34 +1,17 @@
"""Support KNX devices."""
"""The KNX integration."""
from __future__ import annotations
import contextlib
import logging
from pathlib import Path
from typing import Final
import voluptuous as vol
from xknx import XKNX
from xknx.core import XknxConnectionState
from xknx.core.state_updater import StateTrackerType, TrackerOptions
from xknx.core.telegram_queue import TelegramQueue
from xknx.dpt import DPTBase
from xknx.exceptions import ConversionError, CouldNotParseTelegram, XKNXException
from xknx.io import ConnectionConfig, ConnectionType, SecureConfig
from xknx.telegram import AddressFilter, Telegram
from xknx.telegram.address import DeviceGroupAddress, GroupAddress, InternalGroupAddress
from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
from xknx.exceptions import XKNXException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_EVENT,
CONF_HOST,
CONF_PORT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.reload import async_integration_yaml_config
@@ -36,40 +19,17 @@ from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_EXPOSE,
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_KNXKEY_FILENAME,
CONF_KNX_KNXKEY_PASSWORD,
CONF_KNX_LOCAL_IP,
CONF_KNX_MCAST_GRP,
CONF_KNX_MCAST_PORT,
CONF_KNX_RATE_LIMIT,
CONF_KNX_ROUTE_BACK,
CONF_KNX_ROUTING,
CONF_KNX_ROUTING_BACKBONE_KEY,
CONF_KNX_ROUTING_SECURE,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
CONF_KNX_SECURE_USER_ID,
CONF_KNX_SECURE_USER_PASSWORD,
CONF_KNX_STATE_UPDATER,
CONF_KNX_TELEGRAM_LOG_SIZE,
CONF_KNX_TUNNEL_ENDPOINT_IA,
CONF_KNX_TUNNELING,
CONF_KNX_TUNNELING_TCP,
CONF_KNX_TUNNELING_TCP_SECURE,
DATA_HASS_CONFIG,
DOMAIN,
KNX_ADDRESS,
KNX_MODULE_KEY,
SUPPORTED_PLATFORMS_UI,
SUPPORTED_PLATFORMS_YAML,
TELEGRAM_LOG_DEFAULT,
)
from .device import KNXInterfaceDevice
from .expose import KNXExposeSensor, KNXExposeTime, create_knx_exposure
from .project import STORAGE_KEY as PROJECT_STORAGE_KEY, KNXProject
from .expose import create_knx_exposure
from .knx_module import KNXModule
from .project import STORAGE_KEY as PROJECT_STORAGE_KEY
from .schema import (
BinarySensorSchema,
ButtonSchema,
@@ -92,12 +52,10 @@ from .schema import (
WeatherSchema,
)
from .services import register_knx_services
from .storage.config_store import STORAGE_KEY as CONFIG_STORAGE_KEY, KNXConfigStore
from .telegrams import STORAGE_KEY as TELEGRAMS_STORAGE_KEY, Telegrams
from .storage.config_store import STORAGE_KEY as CONFIG_STORAGE_KEY
from .telegrams import STORAGE_KEY as TELEGRAMS_STORAGE_KEY
from .websocket import register_panel
_LOGGER = logging.getLogger(__name__)
_KNX_YAML_CONFIG: Final = "knx_yaml_config"
CONFIG_SCHEMA = vol.Schema(
@@ -162,6 +120,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[KNX_MODULE_KEY] = knx_module
entry.async_on_unload(entry.add_update_listener(async_update_entry))
if CONF_KNX_EXPOSE in config:
for expose_config in config[CONF_KNX_EXPOSE]:
knx_module.exposures.append(
@@ -255,243 +215,3 @@ async def async_remove_config_entry_device(
if entity.device_id == device_entry.id:
await knx_module.config_store.delete_entity(entity.entity_id)
return True
class KNXModule:
"""Representation of KNX Object."""
def __init__(
self, hass: HomeAssistant, config: ConfigType, entry: ConfigEntry
) -> None:
"""Initialize KNX module."""
self.hass = hass
self.config_yaml = config
self.connected = False
self.exposures: list[KNXExposeSensor | KNXExposeTime] = []
self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
self.entry = entry
self.project = KNXProject(hass=hass, entry=entry)
self.config_store = KNXConfigStore(hass=hass, config_entry=entry)
default_state_updater = (
TrackerOptions(tracker_type=StateTrackerType.EXPIRE, update_interval_min=60)
if self.entry.data[CONF_KNX_STATE_UPDATER]
else TrackerOptions(
tracker_type=StateTrackerType.INIT, update_interval_min=60
)
)
self.xknx = XKNX(
address_format=self.project.get_address_format(),
connection_config=self.connection_config(),
rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT],
state_updater=default_state_updater,
)
self.xknx.connection_manager.register_connection_state_changed_cb(
self.connection_state_changed_cb
)
self.telegrams = Telegrams(
hass=hass,
xknx=self.xknx,
project=self.project,
log_size=entry.data.get(CONF_KNX_TELEGRAM_LOG_SIZE, TELEGRAM_LOG_DEFAULT),
)
self.interface_device = KNXInterfaceDevice(
hass=hass, entry=entry, xknx=self.xknx
)
self._address_filter_transcoder: dict[AddressFilter, type[DPTBase]] = {}
self.group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {}
self.knx_event_callback: TelegramQueue.Callback = self.register_event_callback()
self.entry.async_on_unload(
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
)
self.entry.async_on_unload(self.entry.add_update_listener(async_update_entry))
async def start(self) -> None:
"""Start XKNX object. Connect to tunneling or Routing device."""
await self.project.load_project(self.xknx)
await self.config_store.load_data()
await self.telegrams.load_history()
await self.xknx.start()
async def stop(self, event: Event | None = None) -> None:
"""Stop XKNX object. Disconnect from tunneling or Routing device."""
await self.xknx.stop()
await self.telegrams.save_history()
def connection_config(self) -> ConnectionConfig:
"""Return the connection_config."""
_conn_type: str = self.entry.data[CONF_KNX_CONNECTION_TYPE]
_knxkeys_file: str | None = (
self.hass.config.path(
STORAGE_DIR,
self.entry.data[CONF_KNX_KNXKEY_FILENAME],
)
if self.entry.data.get(CONF_KNX_KNXKEY_FILENAME) is not None
else None
)
if _conn_type == CONF_KNX_ROUTING:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING,
individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
multicast_group=self.entry.data[CONF_KNX_MCAST_GRP],
multicast_port=self.entry.data[CONF_KNX_MCAST_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
route_back=self.entry.data.get(CONF_KNX_ROUTE_BACK, False),
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING_TCP:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP_SECURE,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
secure_config=SecureConfig(
user_id=self.entry.data.get(CONF_KNX_SECURE_USER_ID),
user_password=self.entry.data.get(CONF_KNX_SECURE_USER_PASSWORD),
device_authentication_password=self.entry.data.get(
CONF_KNX_SECURE_DEVICE_AUTHENTICATION
),
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
auto_reconnect=True,
threaded=True,
)
if _conn_type == CONF_KNX_ROUTING_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING_SECURE,
individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
multicast_group=self.entry.data[CONF_KNX_MCAST_GRP],
multicast_port=self.entry.data[CONF_KNX_MCAST_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
secure_config=SecureConfig(
backbone_key=self.entry.data.get(CONF_KNX_ROUTING_BACKBONE_KEY),
latency_ms=self.entry.data.get(
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE
),
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
auto_reconnect=True,
threaded=True,
)
return ConnectionConfig(
auto_reconnect=True,
individual_address=self.entry.data.get(
CONF_KNX_TUNNEL_ENDPOINT_IA, # may be configured at knxkey upload
),
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
"""Call invoked after a KNX connection state change was received."""
self.connected = state == XknxConnectionState.CONNECTED
for device in self.xknx.devices:
device.after_update()
def telegram_received_cb(self, telegram: Telegram) -> None:
"""Call invoked after a KNX telegram was received."""
# Not all telegrams have serializable data.
data: int | tuple[int, ...] | None = None
value = None
if (
isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse))
and telegram.payload.value is not None
and isinstance(
telegram.destination_address, (GroupAddress, InternalGroupAddress)
)
):
data = telegram.payload.value.value
if transcoder := (
self.group_address_transcoder.get(telegram.destination_address)
or next(
(
_transcoder
for _filter, _transcoder in self._address_filter_transcoder.items()
if _filter.match(telegram.destination_address)
),
None,
)
):
try:
value = transcoder.from_knx(telegram.payload.value)
except (ConversionError, CouldNotParseTelegram) as err:
_LOGGER.warning(
(
"Error in `knx_event` at decoding type '%s' from"
" telegram %s\n%s"
),
transcoder.__name__,
telegram,
err,
)
self.hass.bus.async_fire(
"knx_event",
{
"data": data,
"destination": str(telegram.destination_address),
"direction": telegram.direction.value,
"value": value,
"source": str(telegram.source_address),
"telegramtype": telegram.payload.__class__.__name__,
},
)
def register_event_callback(self) -> TelegramQueue.Callback:
"""Register callback for knx_event within XKNX TelegramQueue."""
address_filters = []
for filter_set in self.config_yaml[CONF_EVENT]:
_filters = list(map(AddressFilter, filter_set[KNX_ADDRESS]))
address_filters.extend(_filters)
if (dpt := filter_set.get(CONF_TYPE)) and (
transcoder := DPTBase.parse_transcoder(dpt)
):
self._address_filter_transcoder.update(
dict.fromkeys(_filters, transcoder)
)
return self.xknx.telegram_queue.register_telegram_received_cb(
self.telegram_received_cb,
address_filters=address_filters,
group_addresses=[],
match_for_outgoing=True,
)
@@ -1,4 +1,4 @@
"""Support for KNX/IP binary sensors."""
"""Support for KNX binary sensor entities."""
from __future__ import annotations
@@ -25,7 +25,6 @@ from homeassistant.helpers.entity_platform import (
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
ATTR_COUNTER,
ATTR_SOURCE,
@@ -39,6 +38,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .storage.const import CONF_ENTITY, CONF_GA_PASSIVE, CONF_GA_SENSOR, CONF_GA_STATE
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP buttons."""
"""Support for KNX button entities."""
from __future__ import annotations
@@ -11,9 +11,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONF_PAYLOAD_LENGTH, KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP climate devices."""
"""Support for KNX climate entities."""
from __future__ import annotations
@@ -37,9 +37,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import ClimateSchema
ATTR_COMMAND_VALUE = "command_value"
+1 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.const import Platform
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import KNXModule
from .knx_module import KNXModule
DOMAIN: Final = "knx"
KNX_MODULE_KEY: HassKey[KNXModule] = HassKey(DOMAIN)
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP covers."""
"""Support for KNX cover entities."""
from __future__ import annotations
@@ -28,9 +28,9 @@ from homeassistant.helpers.entity_platform import (
)
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONF_SYNC_STATE, DOMAIN, KNX_MODULE_KEY, CoverConf
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .schema import CoverSchema
from .storage.const import (
CONF_ENTITY,
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP date."""
"""Support for KNX date entities."""
from __future__ import annotations
@@ -22,7 +22,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
@@ -31,6 +30,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP datetime."""
"""Support for KNX datetime entities."""
from __future__ import annotations
@@ -23,7 +23,6 @@ from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from . import KNXModule
from .const import (
CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
@@ -32,6 +31,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(
+1 -1
View File
@@ -1,4 +1,4 @@
"""Handle KNX Devices."""
"""Handle Home Assistant Devices for the KNX integration."""
from __future__ import annotations
@@ -1,4 +1,4 @@
"""Provides device triggers for KNX."""
"""Provide device triggers for KNX."""
from __future__ import annotations
+1 -1
View File
@@ -1,4 +1,4 @@
"""Diagnostics support for KNX."""
"""Diagnostics support for the KNX integration."""
from __future__ import annotations
+2 -2
View File
@@ -1,4 +1,4 @@
"""Base class for KNX devices."""
"""Base classes for KNX entities."""
from __future__ import annotations
@@ -17,7 +17,7 @@ from .storage.config_store import PlatformControllerBase
from .storage.const import CONF_DEVICE_INFO
if TYPE_CHECKING:
from . import KNXModule
from .knx_module import KNXModule
class KnxUiEntityPlatformController(PlatformControllerBase):
+1 -1
View File
@@ -1,4 +1,4 @@
"""Exposures to KNX bus."""
"""Expose Home Assistant entity states to KNX."""
from __future__ import annotations
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP fans."""
"""Support for KNX fan entities."""
from __future__ import annotations
@@ -19,9 +19,9 @@ from homeassistant.util.percentage import (
)
from homeassistant.util.scaling import int_states_in_range
from . import KNXModule
from .const import KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import FanSchema
DEFAULT_PERCENTAGE: Final = 50
+301
View File
@@ -0,0 +1,301 @@
"""Base module for the KNX integration."""
from __future__ import annotations
import logging
from xknx import XKNX
from xknx.core import XknxConnectionState
from xknx.core.state_updater import StateTrackerType, TrackerOptions
from xknx.core.telegram_queue import TelegramQueue
from xknx.dpt import DPTBase
from xknx.exceptions import ConversionError, CouldNotParseTelegram
from xknx.io import ConnectionConfig, ConnectionType, SecureConfig
from xknx.telegram import AddressFilter, Telegram
from xknx.telegram.address import DeviceGroupAddress, GroupAddress, InternalGroupAddress
from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_EVENT,
CONF_HOST,
CONF_PORT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_KNX_CONNECTION_TYPE,
CONF_KNX_INDIVIDUAL_ADDRESS,
CONF_KNX_KNXKEY_FILENAME,
CONF_KNX_KNXKEY_PASSWORD,
CONF_KNX_LOCAL_IP,
CONF_KNX_MCAST_GRP,
CONF_KNX_MCAST_PORT,
CONF_KNX_RATE_LIMIT,
CONF_KNX_ROUTE_BACK,
CONF_KNX_ROUTING,
CONF_KNX_ROUTING_BACKBONE_KEY,
CONF_KNX_ROUTING_SECURE,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
CONF_KNX_SECURE_USER_ID,
CONF_KNX_SECURE_USER_PASSWORD,
CONF_KNX_STATE_UPDATER,
CONF_KNX_TELEGRAM_LOG_SIZE,
CONF_KNX_TUNNEL_ENDPOINT_IA,
CONF_KNX_TUNNELING,
CONF_KNX_TUNNELING_TCP,
CONF_KNX_TUNNELING_TCP_SECURE,
KNX_ADDRESS,
TELEGRAM_LOG_DEFAULT,
)
from .device import KNXInterfaceDevice
from .expose import KNXExposeSensor, KNXExposeTime
from .project import KNXProject
from .storage.config_store import KNXConfigStore
from .telegrams import Telegrams
_LOGGER = logging.getLogger(__name__)
class KNXModule:
"""Representation of KNX Object."""
def __init__(
self, hass: HomeAssistant, config: ConfigType, entry: ConfigEntry
) -> None:
"""Initialize KNX module."""
self.hass = hass
self.config_yaml = config
self.connected = False
self.exposures: list[KNXExposeSensor | KNXExposeTime] = []
self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
self.entry = entry
self.project = KNXProject(hass=hass, entry=entry)
self.config_store = KNXConfigStore(hass=hass, config_entry=entry)
default_state_updater = (
TrackerOptions(tracker_type=StateTrackerType.EXPIRE, update_interval_min=60)
if self.entry.data[CONF_KNX_STATE_UPDATER]
else TrackerOptions(
tracker_type=StateTrackerType.INIT, update_interval_min=60
)
)
self.xknx = XKNX(
address_format=self.project.get_address_format(),
connection_config=self.connection_config(),
rate_limit=self.entry.data[CONF_KNX_RATE_LIMIT],
state_updater=default_state_updater,
)
self.xknx.connection_manager.register_connection_state_changed_cb(
self.connection_state_changed_cb
)
self.telegrams = Telegrams(
hass=hass,
xknx=self.xknx,
project=self.project,
log_size=entry.data.get(CONF_KNX_TELEGRAM_LOG_SIZE, TELEGRAM_LOG_DEFAULT),
)
self.interface_device = KNXInterfaceDevice(
hass=hass, entry=entry, xknx=self.xknx
)
self._address_filter_transcoder: dict[AddressFilter, type[DPTBase]] = {}
self.group_address_transcoder: dict[DeviceGroupAddress, type[DPTBase]] = {}
self.knx_event_callback: TelegramQueue.Callback = self.register_event_callback()
self.entry.async_on_unload(
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
)
async def start(self) -> None:
"""Start XKNX object. Connect to tunneling or Routing device."""
await self.project.load_project(self.xknx)
await self.config_store.load_data()
await self.telegrams.load_history()
await self.xknx.start()
async def stop(self, event: Event | None = None) -> None:
"""Stop XKNX object. Disconnect from tunneling or Routing device."""
await self.xknx.stop()
await self.telegrams.save_history()
def connection_config(self) -> ConnectionConfig:
"""Return the connection_config."""
_conn_type: str = self.entry.data[CONF_KNX_CONNECTION_TYPE]
_knxkeys_file: str | None = (
self.hass.config.path(
STORAGE_DIR,
self.entry.data[CONF_KNX_KNXKEY_FILENAME],
)
if self.entry.data.get(CONF_KNX_KNXKEY_FILENAME) is not None
else None
)
if _conn_type == CONF_KNX_ROUTING:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING,
individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
multicast_group=self.entry.data[CONF_KNX_MCAST_GRP],
multicast_port=self.entry.data[CONF_KNX_MCAST_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING,
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
route_back=self.entry.data.get(CONF_KNX_ROUTE_BACK, False),
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING_TCP:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
auto_reconnect=True,
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.TUNNELING_TCP_SECURE,
individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
gateway_ip=self.entry.data[CONF_HOST],
gateway_port=self.entry.data[CONF_PORT],
secure_config=SecureConfig(
user_id=self.entry.data.get(CONF_KNX_SECURE_USER_ID),
user_password=self.entry.data.get(CONF_KNX_SECURE_USER_PASSWORD),
device_authentication_password=self.entry.data.get(
CONF_KNX_SECURE_DEVICE_AUTHENTICATION
),
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
auto_reconnect=True,
threaded=True,
)
if _conn_type == CONF_KNX_ROUTING_SECURE:
return ConnectionConfig(
connection_type=ConnectionType.ROUTING_SECURE,
individual_address=self.entry.data[CONF_KNX_INDIVIDUAL_ADDRESS],
multicast_group=self.entry.data[CONF_KNX_MCAST_GRP],
multicast_port=self.entry.data[CONF_KNX_MCAST_PORT],
local_ip=self.entry.data.get(CONF_KNX_LOCAL_IP),
secure_config=SecureConfig(
backbone_key=self.entry.data.get(CONF_KNX_ROUTING_BACKBONE_KEY),
latency_ms=self.entry.data.get(
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE
),
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
auto_reconnect=True,
threaded=True,
)
return ConnectionConfig(
auto_reconnect=True,
individual_address=self.entry.data.get(
CONF_KNX_TUNNEL_ENDPOINT_IA, # may be configured at knxkey upload
),
secure_config=SecureConfig(
knxkeys_password=self.entry.data.get(CONF_KNX_KNXKEY_PASSWORD),
knxkeys_file_path=_knxkeys_file,
),
threaded=True,
)
def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
"""Call invoked after a KNX connection state change was received."""
self.connected = state == XknxConnectionState.CONNECTED
for device in self.xknx.devices:
device.after_update()
def telegram_received_cb(self, telegram: Telegram) -> None:
"""Call invoked after a KNX telegram was received."""
# Not all telegrams have serializable data.
data: int | tuple[int, ...] | None = None
value = None
if (
isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse))
and telegram.payload.value is not None
and isinstance(
telegram.destination_address, (GroupAddress, InternalGroupAddress)
)
):
data = telegram.payload.value.value
if transcoder := (
self.group_address_transcoder.get(telegram.destination_address)
or next(
(
_transcoder
for _filter, _transcoder in self._address_filter_transcoder.items()
if _filter.match(telegram.destination_address)
),
None,
)
):
try:
value = transcoder.from_knx(telegram.payload.value)
except (ConversionError, CouldNotParseTelegram) as err:
_LOGGER.warning(
(
"Error in `knx_event` at decoding type '%s' from"
" telegram %s\n%s"
),
transcoder.__name__,
telegram,
err,
)
self.hass.bus.async_fire(
"knx_event",
{
"data": data,
"destination": str(telegram.destination_address),
"direction": telegram.direction.value,
"value": value,
"source": str(telegram.source_address),
"telegramtype": telegram.payload.__class__.__name__,
},
)
def register_event_callback(self) -> TelegramQueue.Callback:
"""Register callback for knx_event within XKNX TelegramQueue."""
address_filters = []
for filter_set in self.config_yaml[CONF_EVENT]:
_filters = list(map(AddressFilter, filter_set[KNX_ADDRESS]))
address_filters.extend(_filters)
if (dpt := filter_set.get(CONF_TYPE)) and (
transcoder := DPTBase.parse_transcoder(dpt)
):
self._address_filter_transcoder.update(
dict.fromkeys(_filters, transcoder)
)
return self.xknx.telegram_queue.register_telegram_received_cb(
self.telegram_received_cb,
address_filters=address_filters,
group_addresses=[],
match_for_outgoing=True,
)
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP lights."""
"""Support for KNX light entities."""
from __future__ import annotations
@@ -28,9 +28,9 @@ from homeassistant.helpers.entity_platform import (
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import color as color_util
from . import KNXModule
from .const import CONF_SYNC_STATE, DOMAIN, KNX_ADDRESS, KNX_MODULE_KEY, ColorTempModes
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .schema import LightSchema
from .storage.const import (
CONF_COLOR_TEMP_MAX,
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP notifications."""
"""Support for KNX notify entities."""
from __future__ import annotations
@@ -12,9 +12,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP numeric values."""
"""Support for KNX number entities."""
from __future__ import annotations
@@ -22,9 +22,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import NumberSchema
@@ -99,7 +99,7 @@ rules:
status: exempt
comment: |
Since all entities are configured manually, names are user-defined.
exception-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX scenes."""
"""Support for KNX scene entities."""
from __future__ import annotations
@@ -13,9 +13,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import SceneSchema
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP select entities."""
"""Support for KNX select entities."""
from __future__ import annotations
@@ -20,7 +20,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
CONF_PAYLOAD_LENGTH,
CONF_RESPOND_TO_READ,
@@ -30,6 +29,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import SelectSchema
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP sensors."""
"""Support for KNX sensor entities."""
from __future__ import annotations
@@ -33,9 +33,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.util.enum import try_parse_enum
from . import KNXModule
from .const import ATTR_SOURCE, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import SensorSchema
SCAN_INTERVAL = timedelta(seconds=10)
+5 -15
View File
@@ -35,7 +35,7 @@ from .expose import create_knx_exposure
from .schema import ExposeSchema, dpt_base_type_validator, ga_validator
if TYPE_CHECKING:
from . import KNXModule
from .knx_module import KNXModule
_LOGGER = logging.getLogger(__name__)
@@ -87,9 +87,7 @@ def get_knx_module(hass: HomeAssistant) -> KNXModule:
try:
return hass.data[KNX_MODULE_KEY]
except KeyError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="integration_not_loaded"
) from err
raise HomeAssistantError("KNX entry not loaded") from err
SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema(
@@ -168,11 +166,7 @@ async def service_exposure_register_modify(call: ServiceCall) -> None:
removed_exposure = knx_module.service_exposures.pop(group_address)
except KeyError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="service_exposure_remove_not_found",
translation_placeholders={
"group_address": group_address,
},
f"Could not find exposure for '{group_address}' to remove."
) from err
removed_exposure.async_remove()
@@ -240,17 +234,13 @@ async def service_send_to_knx_bus(call: ServiceCall) -> None:
transcoder = DPTBase.parse_transcoder(attr_type)
if transcoder is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="service_send_invalid_type",
translation_placeholders={"type": attr_type},
f"Invalid type for knx.send service: {attr_type}"
)
try:
payload = transcoder.to_knx(attr_payload)
except ConversionError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="service_send_invalid_payload",
translation_placeholders={"error": str(err)},
f"Invalid payload for knx.send service: {err}"
) from err
elif isinstance(attr_payload, int):
payload = DPTBinary(attr_payload)
@@ -1 +1 @@
"""Helpers for KNX."""
"""Handle persistent storage for the KNX integration."""
@@ -1,4 +1,4 @@
"""KNX Entity Store Validation."""
"""KNX entity store validation."""
from typing import Literal, TypedDict
+1 -15
View File
@@ -131,7 +131,7 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal digits expected.",
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.",
"invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'",
"invalid_ip_address": "Invalid IPv4 address.",
"keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
@@ -143,20 +143,6 @@
"unsupported_tunnel_type": "Selected tunneling type not supported by gateway."
}
},
"exceptions": {
"integration_not_loaded": {
"message": "KNX integration is not loaded."
},
"service_exposure_remove_not_found": {
"message": "Could not find exposure for `{group_address}` to remove."
},
"service_send_invalid_payload": {
"message": "Invalid payload for `knx.send` service. {error}"
},
"service_send_invalid_type": {
"message": "Invalid type for `knx.send` service: {type}"
}
},
"options": {
"step": {
"init": {
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP switches."""
"""Support for KNX switch entities."""
from __future__ import annotations
@@ -25,7 +25,6 @@ from homeassistant.helpers.entity_platform import (
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
CONF_INVERT,
CONF_RESPOND_TO_READ,
@@ -35,6 +34,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule
from .schema import SwitchSchema
from .storage.const import (
CONF_ENTITY,
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP text."""
"""Support for KNX text entities."""
from __future__ import annotations
@@ -22,9 +22,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP time."""
"""Support for KNX time entities."""
from __future__ import annotations
@@ -22,7 +22,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import (
CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
@@ -31,6 +30,7 @@ from .const import (
KNX_MODULE_KEY,
)
from .entity import KnxYamlEntity
from .knx_module import KNXModule
async def async_setup_entry(
+1 -1
View File
@@ -1,4 +1,4 @@
"""Offer knx telegram automation triggers."""
"""Provide KNX automation triggers."""
from typing import Final
+2 -2
View File
@@ -1,4 +1,4 @@
"""Support for KNX/IP weather station."""
"""Support for KNX weather entities."""
from __future__ import annotations
@@ -19,9 +19,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .knx_module import KNXModule
from .schema import WeatherSchema
+1 -1
View File
@@ -36,7 +36,7 @@ from .storage.entity_store_validation import (
from .telegrams import SIGNAL_KNX_TELEGRAM, TelegramDict
if TYPE_CHECKING:
from . import KNXModule
from .knx_module import KNXModule
URL_BASE: Final = "/knx_static"
+2 -2
View File
@@ -59,7 +59,7 @@ from .helpers import (
register_lcn_address_devices,
register_lcn_host_device,
)
from .services import async_setup_services
from .services import register_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, {})
async_setup_services(hass)
await register_services(hass)
await register_panel_and_ws_api(hass)
return True
+1 -1
View File
@@ -438,7 +438,7 @@ SERVICES = (
)
def async_setup_services(hass: HomeAssistant) -> None:
async def register_services(hass: HomeAssistant) -> None:
"""Register services for LCN."""
for service_name, service in SERVICES:
hass.services.async_register(
@@ -31,9 +31,6 @@ 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.10"],
"requirements": ["python-linkplay==0.2.9"],
"zeroconf": ["_linkplay._tcp.local."]
}
+30 -4
View File
@@ -44,8 +44,7 @@ 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 ATTR_FORMAT, ATTR_IMAGES, CONF_ROOMS_REGEX, DOMAIN, FORMAT_HTML
from .services import register_services
from .const import DOMAIN, FORMAT_HTML, FORMAT_TEXT, SERVICE_SEND_MESSAGE
_LOGGER = logging.getLogger(__name__)
@@ -58,11 +57,17 @@ 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)
@@ -112,12 +117,27 @@ 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]
hass.data[DOMAIN] = MatrixBot(
matrix_bot = MatrixBot(
hass,
os.path.join(hass.config.path(), SESSION_FILE),
config[CONF_HOMESERVER],
@@ -127,8 +147,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
config[CONF_ROOMS],
config[CONF_COMMANDS],
)
hass.data[DOMAIN] = matrix_bot
register_services(hass)
hass.services.async_register(
DOMAIN,
SERVICE_SEND_MESSAGE,
matrix_bot.handle_send_message,
schema=SERVICE_SCHEMA_SEND_MESSAGE,
)
return True
-5
View File
@@ -6,8 +6,3 @@ 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 = "^[!|#][^:]*:.*"
@@ -1,61 +0,0 @@
"""The Matrix bot component."""
from __future__ import annotations
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from .const import (
ATTR_FORMAT,
ATTR_IMAGES,
CONF_ROOMS_REGEX,
DOMAIN,
FORMAT_HTML,
FORMAT_TEXT,
SERVICE_SEND_MESSAGE,
)
if TYPE_CHECKING:
from . import MatrixBot
MESSAGE_FORMATS = [FORMAT_HTML, FORMAT_TEXT]
DEFAULT_MESSAGE_FORMAT = FORMAT_TEXT
SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema(
{
vol.Required(ATTR_MESSAGE): cv.string,
vol.Optional(ATTR_DATA, default={}): {
vol.Optional(ATTR_FORMAT, default=DEFAULT_MESSAGE_FORMAT): vol.In(
MESSAGE_FORMATS
),
vol.Optional(ATTR_IMAGES): vol.All(cv.ensure_list, [cv.string]),
},
vol.Required(ATTR_TARGET): vol.All(
cv.ensure_list, [cv.matches_regex(CONF_ROOMS_REGEX)]
),
}
)
async def _handle_send_message(call: ServiceCall) -> None:
"""Handle the send_message service call."""
matrix_bot: MatrixBot = call.hass.data[DOMAIN]
await matrix_bot.handle_send_message(call)
def register_services(hass: HomeAssistant) -> None:
"""Set up the Matrix bot component."""
hass.services.async_register(
DOMAIN,
SERVICE_SEND_MESSAGE,
_handle_send_message,
schema=SERVICE_SCHEMA_SEND_MESSAGE,
)
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/modbus",
"iot_class": "local_polling",
"loggers": ["pymodbus"],
"requirements": ["pymodbus==3.9.2"]
"requirements": ["pymodbus==3.8.3"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nextbus",
"iot_class": "cloud_polling",
"loggers": ["py_nextbus"],
"requirements": ["py-nextbusnext==2.2.0"]
"requirements": ["py-nextbusnext==2.1.2"]
}
+43 -11
View File
@@ -1,25 +1,30 @@
"""The NZBGet integration."""
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DATA_COORDINATOR, DATA_UNDO_UPDATE_LISTENER, DOMAIN
from .const import (
ATTR_SPEED,
DATA_COORDINATOR,
DATA_UNDO_UPDATE_LISTENER,
DEFAULT_SPEED_LIMIT,
DOMAIN,
SERVICE_PAUSE,
SERVICE_RESUME,
SERVICE_SET_SPEED,
)
from .coordinator import NZBGetDataUpdateCoordinator
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up NZBGet integration."""
async_setup_services(hass)
return True
SPEED_LIMIT_SCHEMA = vol.Schema(
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int}
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -39,6 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
_async_register_services(hass, coordinator)
return True
@@ -53,6 +60,31 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
def _async_register_services(
hass: HomeAssistant,
coordinator: NZBGetDataUpdateCoordinator,
) -> None:
"""Register integration-level services."""
def pause(call: ServiceCall) -> None:
"""Service call to pause downloads in NZBGet."""
coordinator.nzbget.pausedownload()
def resume(call: ServiceCall) -> None:
"""Service call to resume downloads in NZBGet."""
coordinator.nzbget.resumedownload()
def set_speed(call: ServiceCall) -> None:
"""Service call to rate limit speeds in NZBGet."""
coordinator.nzbget.rate(call.data[ATTR_SPEED])
hass.services.async_register(DOMAIN, SERVICE_PAUSE, pause, schema=vol.Schema({}))
hass.services.async_register(DOMAIN, SERVICE_RESUME, resume, schema=vol.Schema({}))
hass.services.async_register(
DOMAIN, SERVICE_SET_SPEED, set_speed, schema=SPEED_LIMIT_SCHEMA
)
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
@@ -1,58 +0,0 @@
"""The NZBGet integration."""
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from .const import (
ATTR_SPEED,
DATA_COORDINATOR,
DEFAULT_SPEED_LIMIT,
DOMAIN,
SERVICE_PAUSE,
SERVICE_RESUME,
SERVICE_SET_SPEED,
)
from .coordinator import NZBGetDataUpdateCoordinator
SPEED_LIMIT_SCHEMA = vol.Schema(
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int}
)
def _get_coordinator(call: ServiceCall) -> NZBGetDataUpdateCoordinator:
"""Service call to pause downloads in NZBGet."""
entries = call.hass.config_entries.async_loaded_entries(DOMAIN)
if not entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_config_entry",
)
return call.hass.data[DOMAIN][entries[0].entry_id][DATA_COORDINATOR]
def pause(call: ServiceCall) -> None:
"""Service call to pause downloads in NZBGet."""
_get_coordinator(call).nzbget.pausedownload()
def resume(call: ServiceCall) -> None:
"""Service call to resume downloads in NZBGet."""
_get_coordinator(call).nzbget.resumedownload()
def set_speed(call: ServiceCall) -> None:
"""Service call to rate limit speeds in NZBGet."""
_get_coordinator(call).nzbget.rate(call.data[ATTR_SPEED])
def async_setup_services(hass: HomeAssistant) -> None:
"""Register integration-level services."""
hass.services.async_register(DOMAIN, SERVICE_PAUSE, pause, schema=vol.Schema({}))
hass.services.async_register(DOMAIN, SERVICE_RESUME, resume, schema=vol.Schema({}))
hass.services.async_register(
DOMAIN, SERVICE_SET_SPEED, set_speed, schema=SPEED_LIMIT_SCHEMA
)
@@ -64,11 +64,6 @@
}
}
},
"exceptions": {
"invalid_config_entry": {
"message": "Config entry not found or not loaded!"
}
},
"services": {
"pause": {
"name": "[%key:common::action::pause%]",

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