forked from home-assistant/core
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d19e012664 | |||
| f54d0ccbd7 | |||
| fd500346bf | |||
| c6c7e7eae1 | |||
| 07557e27b0 | |||
| f211da60e0 | |||
| 64b74d00f7 | |||
| 96cb645644 | |||
| 9b0db3bd51 | |||
| ffdefd1e0f | |||
| 59ad0268a9 | |||
| f28851e76f | |||
| 4f5c1d544b | |||
| a8ccf1c6fc | |||
| e3f7e5706b | |||
| 7ad1e756e7 | |||
| 8868f214f3 | |||
| 3ecff19a45 | |||
| 74421db747 | |||
| 1cccfac3dc | |||
| c254548a64 | |||
| 7f8b782e95 | |||
| cd518d4a46 | |||
| c5db07e84d | |||
| d1e0225520 | |||
| d439bb68eb | |||
| 980dbf364d | |||
| 842e7ce171 | |||
| 8afec8ada9 | |||
| 7b699f7733 | |||
| d448ef9f16 | |||
| 03912a1704 | |||
| 54c20d5d5a | |||
| 2dbf24e798 | |||
| 791654a420 | |||
| 5fe07e49e4 | |||
| 0bd287788c | |||
| 40e0c0f98d | |||
| 85b608912b | |||
| 987753dd1c | |||
| 5df05fb6dd | |||
| f295ca27af | |||
| 8f75cc6a33 | |||
| 19c71f0f49 | |||
| 22c2028c00 | |||
| 39f687e3a3 | |||
| 6692b9b71f | |||
| 2f5787e7be | |||
| bbda1761bf | |||
| ecc10e9793 | |||
| 9e1e889fd7 | |||
| eefe1e6f0f | |||
| 397ed87f2d | |||
| 15830f383e | |||
| 87395efc6e | |||
| 27d79bb10a | |||
| 7427db70aa | |||
| 77d5bffa85 | |||
| ab7c7b8d89 | |||
| 93b8cc38d8 | |||
| e5f95b3aff | |||
| 613728ad3b | |||
| cb1bfe6ebe | |||
| 434179ab3f | |||
| eb53277fcc | |||
| 850ddb3667 | |||
| 5a727a4fa3 | |||
| 33fc700952 | |||
| ad493e077e | |||
| a2b2f6f20a | |||
| ee57fd413a | |||
| f5d585e0f0 | |||
| 1899388f35 | |||
| 4d833e9b1c | |||
| 6d827cd412 | |||
| ebfbea39ff | |||
| 89a40f1c48 | |||
| 664eb7af10 | |||
| 33b99b6627 | |||
| 0cf2ee0bcb | |||
| 85a86c3f11 | |||
| de4a5fa30b | |||
| 43ac550ca0 | |||
| c3c4d224b2 | |||
| 6f865beacd | |||
| de25195383 | |||
| 0139d2cabf | |||
| 17542614b5 | |||
| 885367e690 | |||
| f8c44aad25 | |||
| 2323cc2869 | |||
| 7f0249bbf7 | |||
| 7a23b778a4 | |||
| d910924032 | |||
| 0b93a8c2f2 | |||
| 5e377b89fc | |||
| dd85a1e5f0 | |||
| b96a7aebcd | |||
| 3cfcf382da |
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents-package
|
||||
|
||||
@@ -360,7 +360,7 @@ jobs:
|
||||
- name: Run ruff
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure
|
||||
pre-commit run --hook-stage manual ruff-check --all-files --show-diff-on-failure
|
||||
env:
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.18
|
||||
uses: github/codeql-action/init@v3.28.19
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.18
|
||||
uses: github/codeql-action/analyze@v3.28.19
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
|
||||
core:
|
||||
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
if: false && github.repository_owner == 'home-assistant'
|
||||
needs: init
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -176,12 +176,26 @@ jobs:
|
||||
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: init
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp313"]
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
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
|
||||
arch: armv7
|
||||
abi: cp313
|
||||
- os: ubuntu-latest
|
||||
arch: armhf
|
||||
abi: cp313
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
@@ -218,8 +232,28 @@ 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 "rpds_py==0.25.1" >> requirements_custom.txt
|
||||
|
||||
- name: Build wheels (custom)
|
||||
uses: cdce8p/wheels@master
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||
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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.0
|
||||
rev: v0.11.12
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-check
|
||||
args:
|
||||
- --fix
|
||||
- id: ruff-format
|
||||
@@ -30,7 +30,7 @@ repos:
|
||||
- --branch=master
|
||||
- --branch=rc
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.35.1
|
||||
rev: v1.37.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
|
||||
Vendored
+1
-1
@@ -45,7 +45,7 @@
|
||||
{
|
||||
"label": "Ruff",
|
||||
"type": "shell",
|
||||
"command": "pre-commit run ruff --all-files",
|
||||
"command": "pre-commit run ruff-check --all-files",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
|
||||
from .const import CONNECTION_TYPE, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
|
||||
|
||||
@@ -41,7 +41,30 @@ class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||
"""Fetch data from the Adax."""
|
||||
rooms = await self.adax_data_handler.get_rooms() or []
|
||||
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))
|
||||
|
||||
return {r["id"]: r for r in rooms}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""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"])
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Diagnostics support for Amazon Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
devices: list[dict[str, dict[str, Any]]] = [
|
||||
build_device_data(device) for device in coordinator.data.values()
|
||||
]
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"device_info": {
|
||||
"last_update success": coordinator.last_update_success,
|
||||
"last_exception": repr(coordinator.last_exception),
|
||||
"devices": devices,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
assert device_entry.serial_number
|
||||
|
||||
return build_device_data(coordinator.data[device_entry.serial_number])
|
||||
|
||||
|
||||
def build_device_data(device: AmazonDevice) -> dict[str, Any]:
|
||||
"""Build device data for diagnostics."""
|
||||
return {
|
||||
"account name": device.account_name,
|
||||
"capabilities": device.capabilities,
|
||||
"device family": device.device_family,
|
||||
"device type": device.device_type,
|
||||
"device cluster members": device.device_cluster_members,
|
||||
"online": device.online,
|
||||
"serial number": device.serial_number,
|
||||
"software version": device.software_version,
|
||||
"do not disturb": device.do_not_disturb,
|
||||
"response style": device.response_style,
|
||||
"bluetooth state": device.bluetooth_state,
|
||||
}
|
||||
@@ -118,5 +118,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==2.1.1"]
|
||||
"requirements": ["aioamazondevices==3.0.5"]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.components.recorder import (
|
||||
get_instance as get_recorder_instance,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -225,7 +225,8 @@ class Analytics:
|
||||
LOGGER.error(err)
|
||||
return
|
||||
|
||||
configuration_set = set(yaml_configuration)
|
||||
configuration_set = _domains_from_yaml_config(yaml_configuration)
|
||||
|
||||
er_platforms = {
|
||||
entity.platform
|
||||
for entity in ent_reg.entities.values()
|
||||
@@ -370,3 +371,13 @@ class Analytics:
|
||||
for entry in entries
|
||||
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
||||
)
|
||||
|
||||
|
||||
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
||||
"""Extract domains from the YAML configuration."""
|
||||
domains = set(yaml_configuration)
|
||||
for platforms in conf_util.extract_platform_integrations(
|
||||
yaml_configuration, BASE_PLATFORMS
|
||||
).values():
|
||||
domains.update(platforms)
|
||||
return domains
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -32,6 +33,7 @@ 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.48.2"
|
||||
"habluetooth==3.49.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/camera",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.7.5"]
|
||||
"requirements": ["PyTurboJPEG==1.8.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["numpy==2.2.2"]
|
||||
"requirements": ["numpy==2.2.6"]
|
||||
}
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
"""The eddystone_temperature component."""
|
||||
|
||||
DOMAIN = "eddystone_temperature"
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
@@ -23,17 +23,18 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_BT_DEVICE_ID = "bt_device_id"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
|
||||
BEACON_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -58,6 +59,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Validate configuration, create devices and start monitoring thread."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Eddystone",
|
||||
},
|
||||
)
|
||||
|
||||
bt_device_id: int = config[CONF_BT_DEVICE_ID]
|
||||
|
||||
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"]
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==31.1.0",
|
||||
"aioesphomeapi==32.0.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.15.1"
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -71,6 +71,11 @@ 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
|
||||
@@ -91,9 +96,6 @@ 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:
|
||||
|
||||
@@ -84,6 +84,7 @@ 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
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["go2rtc-client==0.1.3b0"],
|
||||
"requirements": ["go2rtc-client==0.2.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Google Mail platform."""
|
||||
"""Set up the Google Mail integration."""
|
||||
hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -52,8 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -
|
||||
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
|
||||
)
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -7,17 +7,26 @@ from google_photos_library_api.api import GooglePhotosLibraryApi
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import api
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GooglePhotosConfigEntry, GooglePhotosUpdateCoordinator
|
||||
from .services import async_register_services
|
||||
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
]
|
||||
__all__ = ["DOMAIN"]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Google Photos integration."""
|
||||
|
||||
async_register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -48,8 +57,6 @@ async def async_setup_entry(
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
async_register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -152,11 +152,10 @@ def async_register_services(hass: HomeAssistant) -> None:
|
||||
}
|
||||
return None
|
||||
|
||||
if not hass.services.has_service(DOMAIN, UPLOAD_SERVICE):
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The hddtemp component."""
|
||||
|
||||
DOMAIN = "hddtemp"
|
||||
|
||||
@@ -22,11 +22,14 @@ from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, 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 DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DEVICE = "device"
|
||||
@@ -56,6 +59,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the HDDTemp sensor."""
|
||||
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": "hddtemp",
|
||||
},
|
||||
)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
@@ -73,7 +73,9 @@ async def async_setup_entry(
|
||||
class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
|
||||
"""Hive Water Heater Device."""
|
||||
|
||||
_attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_operation_list = SUPPORT_WATER_HEATER
|
||||
|
||||
|
||||
@@ -83,3 +83,54 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the reconfigure flow."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input:
|
||||
self.homee = Homee(
|
||||
user_input[CONF_HOST],
|
||||
reconfigure_entry.data[CONF_USERNAME],
|
||||
reconfigure_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
await self.homee.wait_until_connected()
|
||||
self.homee.disconnect()
|
||||
await self.homee.wait_until_disconnected()
|
||||
|
||||
await self.async_set_unique_id(self.homee.settings.uid)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_hub")
|
||||
|
||||
_LOGGER.debug("Updated homee entry with ID %s", self.homee.settings.uid)
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(), data_updates=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_HOST, default=reconfigure_entry.data[CONF_HOST]
|
||||
): str
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"name": reconfigure_entry.runtime_data.settings.uid
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"config": {
|
||||
"flow_title": "homee {name} ({host})",
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"wrong_hub": "Address belongs to a different homee."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -22,6 +24,16 @@
|
||||
"username": "The username for your homee.",
|
||||
"password": "The password for your homee."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure homee {name}",
|
||||
"description": "Reconfigure the IP address of your homee.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of your homee."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.14"],
|
||||
"requirements": ["aiohomekit==3.2.15"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ from .const import (
|
||||
HMIPC_NAME,
|
||||
)
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .services import async_setup_services, async_unload_services
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -63,6 +63,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -83,7 +85,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
|
||||
if not await hap.async_setup():
|
||||
return False
|
||||
|
||||
await async_setup_services(hass)
|
||||
_async_remove_obsolete_entities(hass, entry, hap)
|
||||
|
||||
# Register on HA stop event to gracefully shutdown HomematicIP Cloud connection
|
||||
@@ -115,8 +116,6 @@ 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,32 +123,29 @@ 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(hass, service)
|
||||
await _async_activate_eco_mode_with_duration(service)
|
||||
elif service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD:
|
||||
await _async_activate_eco_mode_with_period(hass, service)
|
||||
await _async_activate_eco_mode_with_period(service)
|
||||
elif service_name == SERVICE_ACTIVATE_VACATION:
|
||||
await _async_activate_vacation(hass, service)
|
||||
await _async_activate_vacation(service)
|
||||
elif service_name == SERVICE_DEACTIVATE_ECO_MODE:
|
||||
await _async_deactivate_eco_mode(hass, service)
|
||||
await _async_deactivate_eco_mode(service)
|
||||
elif service_name == SERVICE_DEACTIVATE_VACATION:
|
||||
await _async_deactivate_vacation(hass, service)
|
||||
await _async_deactivate_vacation(service)
|
||||
elif service_name == SERVICE_DUMP_HAP_CONFIG:
|
||||
await _async_dump_hap_config(hass, service)
|
||||
await _async_dump_hap_config(service)
|
||||
elif service_name == SERVICE_RESET_ENERGY_COUNTER:
|
||||
await _async_reset_energy_counter(hass, service)
|
||||
await _async_reset_energy_counter(service)
|
||||
elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE:
|
||||
await _set_active_climate_profile(hass, service)
|
||||
await _set_active_climate_profile(service)
|
||||
elif service_name == SERVICE_SET_HOME_COOLING_MODE:
|
||||
await _async_set_home_cooling_mode(hass, service)
|
||||
await _async_set_home_cooling_mode(service)
|
||||
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
@@ -217,90 +214,75 @@ async def async_setup_services(hass: HomeAssistant) -> 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:
|
||||
async def _async_activate_eco_mode_with_duration(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(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.activate_absence_with_duration_async(duration)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.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(
|
||||
hass: HomeAssistant, service: ServiceCall
|
||||
) -> None:
|
||||
async def _async_activate_eco_mode_with_period(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(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.activate_absence_with_period_async(endtime)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.activate_absence_with_period_async(endtime)
|
||||
|
||||
|
||||
async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
async def _async_activate_vacation(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(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.activate_vacation_async(endtime, temperature)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.activate_vacation_async(endtime, temperature)
|
||||
|
||||
|
||||
async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
async def _async_deactivate_eco_mode(service: ServiceCall) -> None:
|
||||
"""Service to deactivate eco mode."""
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.deactivate_absence_async()
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.deactivate_absence_async()
|
||||
|
||||
|
||||
async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
async def _async_deactivate_vacation(service: ServiceCall) -> None:
|
||||
"""Service to deactivate vacation."""
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.deactivate_vacation_async()
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.deactivate_vacation_async()
|
||||
|
||||
|
||||
async def _set_active_climate_profile(
|
||||
hass: HomeAssistant, service: ServiceCall
|
||||
) -> None:
|
||||
async def _set_active_climate_profile(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 hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.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)
|
||||
@@ -312,16 +294,16 @@ async def _set_active_climate_profile(
|
||||
await group.set_active_profile_async(climate_profile_index)
|
||||
|
||||
|
||||
async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
async def _async_dump_hap_config(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 hass.config.config_dir
|
||||
service.data.get(ATTR_CONFIG_OUTPUT_PATH) or service.hass.config.config_dir
|
||||
)
|
||||
config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX]
|
||||
anonymize = service.data[ATTR_ANONYMIZE]
|
||||
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
hap_sgtin = entry.unique_id
|
||||
assert hap_sgtin is not None
|
||||
|
||||
@@ -338,12 +320,12 @@ async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> N
|
||||
config_file.write_text(json_state, encoding="utf8")
|
||||
|
||||
|
||||
async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall):
|
||||
async def _async_reset_energy_counter(service: ServiceCall):
|
||||
"""Service to reset the energy counter."""
|
||||
entity_id_list = service.data[ATTR_ENTITY_ID]
|
||||
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.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)
|
||||
@@ -355,16 +337,16 @@ async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall)
|
||||
await device.reset_energy_counter_async()
|
||||
|
||||
|
||||
async def _async_set_home_cooling_mode(hass: HomeAssistant, service: ServiceCall):
|
||||
async def _async_set_home_cooling_mode(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(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.set_cooling_async(cooling)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.set_cooling_async(cooling)
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["homewizard_energy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-homewizard-energy==v8.3.2"],
|
||||
"requirements": ["python-homewizard-energy==8.3.3"],
|
||||
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ def setup_cors(app: Application, origins: list[str]) -> None:
|
||||
cors = aiohttp_cors.setup(
|
||||
app,
|
||||
defaults={
|
||||
host: aiohttp_cors.ResourceOptions(
|
||||
host: aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
|
||||
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
|
||||
)
|
||||
for host in origins
|
||||
},
|
||||
)
|
||||
|
||||
cors_added = set()
|
||||
cors_added: set[str] = set()
|
||||
|
||||
def _allow_cors(
|
||||
route: AbstractRoute | AbstractResource,
|
||||
@@ -69,13 +69,13 @@ def setup_cors(app: Application, origins: list[str]) -> None:
|
||||
if path_str in cors_added:
|
||||
return
|
||||
|
||||
cors.add(route, config)
|
||||
cors.add(route, config) # type: ignore[arg-type]
|
||||
cors_added.add(path_str)
|
||||
|
||||
app[KEY_ALLOW_ALL_CORS] = lambda route: _allow_cors(
|
||||
route,
|
||||
{
|
||||
"*": aiohttp_cors.ResourceOptions(
|
||||
"*": aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
|
||||
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
|
||||
)
|
||||
},
|
||||
|
||||
@@ -5,13 +5,24 @@ from aiohue.util import normalize_bridge_id
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .bridge import HueBridge, HueConfigEntry
|
||||
from .const import DOMAIN, SERVICE_HUE_ACTIVATE_SCENE
|
||||
from .const import DOMAIN
|
||||
from .migration import check_migration
|
||||
from .services import async_register_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Hue integration."""
|
||||
|
||||
async_register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
"""Set up a bridge from a config entry."""
|
||||
@@ -23,9 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
if not await bridge.async_initialize_bridge():
|
||||
return False
|
||||
|
||||
# register Hue domain services
|
||||
async_register_services(hass)
|
||||
|
||||
api = bridge.api
|
||||
|
||||
# For backwards compat
|
||||
@@ -106,7 +114,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_success = await entry.runtime_data.async_reset()
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
hass.services.async_remove(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE)
|
||||
return unload_success
|
||||
return await entry.runtime_data.async_reset()
|
||||
|
||||
@@ -59,21 +59,20 @@ def async_register_services(hass: HomeAssistant) -> None:
|
||||
group_name,
|
||||
)
|
||||
|
||||
if not hass.services.has_service(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE):
|
||||
# Register a local handler for scene activation
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_HUE_ACTIVATE_SCENE,
|
||||
verify_domain_control(hass, DOMAIN)(hue_activate_scene),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GROUP_NAME): cv.string,
|
||||
vol.Required(ATTR_SCENE_NAME): cv.string,
|
||||
vol.Optional(ATTR_TRANSITION): cv.positive_int,
|
||||
vol.Optional(ATTR_DYNAMIC): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
# Register a local handler for scene activation
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_HUE_ACTIVATE_SCENE,
|
||||
verify_domain_control(hass, DOMAIN)(hue_activate_scene),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GROUP_NAME): cv.string,
|
||||
vol.Required(ATTR_SCENE_NAME): cv.string,
|
||||
vol.Optional(ATTR_TRANSITION): cv.positive_int,
|
||||
vol.Optional(ATTR_DYNAMIC): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def hue_activate_scene_v1(
|
||||
|
||||
@@ -6,19 +6,32 @@ 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 register_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up iCloud integration."""
|
||||
|
||||
register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bool:
|
||||
"""Set up an iCloud account from a config entry."""
|
||||
@@ -51,8 +64,6 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioimmich==0.7.0"]
|
||||
"requirements": ["aioimmich==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -133,10 +133,10 @@ class ImmichMediaSource(MediaSource):
|
||||
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaClass.IMAGE,
|
||||
title=album.name,
|
||||
title=album.album_name,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.album_thumbnail_asset_id}/thumbnail/image/jpg",
|
||||
)
|
||||
for album in albums
|
||||
]
|
||||
@@ -153,47 +153,40 @@ class ImmichMediaSource(MediaSource):
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
ret = [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}|albums|"
|
||||
f"{identifier.collection_id}|"
|
||||
f"{asset.asset_id}|"
|
||||
f"{asset.file_name}|"
|
||||
f"{asset.mime_type}"
|
||||
),
|
||||
media_class=MediaClass.IMAGE,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=False,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{asset.mime_type}",
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if asset.mime_type.startswith("image/")
|
||||
]
|
||||
ret: list[BrowseMediaSource] = []
|
||||
for asset in album_info.assets:
|
||||
if not (mime_type := asset.original_mime_type) or not mime_type.startswith(
|
||||
("image/", "video/")
|
||||
):
|
||||
continue
|
||||
|
||||
ret.extend(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}|albums|"
|
||||
f"{identifier.collection_id}|"
|
||||
f"{asset.asset_id}|"
|
||||
f"{asset.file_name}|"
|
||||
f"{asset.mime_type}"
|
||||
),
|
||||
media_class=MediaClass.VIDEO,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg",
|
||||
if mime_type.startswith("image/"):
|
||||
media_class = MediaClass.IMAGE
|
||||
can_play = False
|
||||
thumb_mime_type = mime_type
|
||||
else:
|
||||
media_class = MediaClass.VIDEO
|
||||
can_play = True
|
||||
thumb_mime_type = "image/jpeg"
|
||||
|
||||
ret.append(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}|albums|"
|
||||
f"{identifier.collection_id}|"
|
||||
f"{asset.asset_id}|"
|
||||
f"{asset.original_file_name}|"
|
||||
f"{mime_type}"
|
||||
),
|
||||
media_class=media_class,
|
||||
media_content_type=mime_type,
|
||||
title=asset.original_file_name,
|
||||
can_play=can_play,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{thumb_mime_type}",
|
||||
)
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if asset.mime_type.startswith("video/")
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ from .const import (
|
||||
DOMAIN,
|
||||
INSTEON_PLATFORMS,
|
||||
)
|
||||
from .services import async_register_services
|
||||
from .utils import (
|
||||
add_insteon_events,
|
||||
async_register_services,
|
||||
get_device_platforms,
|
||||
register_new_device_callback,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
"""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_register_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")
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -12,90 +11,25 @@ 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,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
ENTITY_MATCH_ALL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import 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
|
||||
@@ -154,7 +88,7 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
|
||||
_register_event(event, async_fire_insteon_event)
|
||||
|
||||
|
||||
def register_new_device_callback(hass):
|
||||
def register_new_device_callback(hass: HomeAssistant) -> None:
|
||||
"""Register callback for new Insteon device."""
|
||||
|
||||
@callback
|
||||
@@ -180,212 +114,6 @@ def register_new_device_callback(hass):
|
||||
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")
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyiqvia"],
|
||||
"requirements": ["numpy==2.2.2", "pyiqvia==2022.04.0"]
|
||||
"requirements": ["numpy==2.2.6", "pyiqvia==2022.04.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyiskra"],
|
||||
"requirements": ["pyiskra==0.1.19"]
|
||||
"requirements": ["pyiskra==0.1.21"]
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
@@ -46,7 +47,7 @@ from .const import (
|
||||
)
|
||||
from .helpers import _categorize_nodes, _categorize_programs
|
||||
from .models import IsyConfigEntry, IsyData
|
||||
from .services import async_setup_services, async_unload_services
|
||||
from .services import async_setup_services
|
||||
from .util import _async_cleanup_registry_entries
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@@ -55,6 +56,14 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the ISY 994 integration."""
|
||||
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool:
|
||||
"""Set up the ISY 994 integration."""
|
||||
isy_config = entry.data
|
||||
@@ -167,9 +176,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool:
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update)
|
||||
)
|
||||
|
||||
# Register Integration-wide Services:
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -221,9 +227,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool
|
||||
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
|
||||
entry.runtime_data.root.websocket.stop()
|
||||
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
async_unload_services(hass)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
|
||||
@@ -137,10 +137,6 @@ def async_get_entities(hass: HomeAssistant) -> dict[str, Entity]:
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Create and register services for the ISY integration."""
|
||||
existing_services = hass.services.async_services_for_domain(DOMAIN)
|
||||
if existing_services and SERVICE_SEND_PROGRAM_COMMAND in existing_services:
|
||||
# Integration-level services have already been added. Return.
|
||||
return
|
||||
|
||||
async def async_send_program_command_service_handler(service: ServiceCall) -> None:
|
||||
"""Handle a send program command service call."""
|
||||
@@ -230,18 +226,3 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
schema=cv.make_entity_service_schema(SERVICE_RENAME_NODE_SCHEMA),
|
||||
service_func=_async_rename_node,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_unload_services(hass: HomeAssistant) -> None:
|
||||
"""Unload services for the ISY integration."""
|
||||
existing_services = hass.services.async_services_for_domain(DOMAIN)
|
||||
if not existing_services or SERVICE_SEND_PROGRAM_COMMAND not in existing_services:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Unloading ISY994 Services")
|
||||
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_PROGRAM_COMMAND)
|
||||
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_RAW_NODE_COMMAND)
|
||||
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_NODE_COMMAND)
|
||||
hass.services.async_remove(domain=DOMAIN, service=SERVICE_GET_ZWAVE_PARAMETER)
|
||||
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SET_ZWAVE_PARAMETER)
|
||||
|
||||
@@ -30,7 +30,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
from .entity import JewishCalendarConfigEntry, JewishCalendarData
|
||||
from .service import async_setup_services
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
@@ -99,7 +99,7 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
Since all entities are configured manually, names are user-defined.
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
|
||||
@@ -87,7 +87,9 @@ def get_knx_module(hass: HomeAssistant) -> KNXModule:
|
||||
try:
|
||||
return hass.data[KNX_MODULE_KEY]
|
||||
except KeyError as err:
|
||||
raise HomeAssistantError("KNX entry not loaded") from err
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="integration_not_loaded"
|
||||
) from err
|
||||
|
||||
|
||||
SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema(
|
||||
@@ -166,7 +168,11 @@ async def service_exposure_register_modify(call: ServiceCall) -> None:
|
||||
removed_exposure = knx_module.service_exposures.pop(group_address)
|
||||
except KeyError as err:
|
||||
raise ServiceValidationError(
|
||||
f"Could not find exposure for '{group_address}' to remove."
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_exposure_remove_not_found",
|
||||
translation_placeholders={
|
||||
"group_address": group_address,
|
||||
},
|
||||
) from err
|
||||
|
||||
removed_exposure.async_remove()
|
||||
@@ -234,13 +240,17 @@ async def service_send_to_knx_bus(call: ServiceCall) -> None:
|
||||
transcoder = DPTBase.parse_transcoder(attr_type)
|
||||
if transcoder is None:
|
||||
raise ServiceValidationError(
|
||||
f"Invalid type for knx.send service: {attr_type}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_send_invalid_type",
|
||||
translation_placeholders={"type": attr_type},
|
||||
)
|
||||
try:
|
||||
payload = transcoder.to_knx(attr_payload)
|
||||
except ConversionError as err:
|
||||
raise ServiceValidationError(
|
||||
f"Invalid payload for knx.send service: {err}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_send_invalid_payload",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
elif isinstance(attr_payload, int):
|
||||
payload = DPTBinary(attr_payload)
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.",
|
||||
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal digits expected.",
|
||||
"invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'",
|
||||
"invalid_ip_address": "Invalid IPv4 address.",
|
||||
"keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
|
||||
@@ -143,6 +143,20 @@
|
||||
"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": {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==9.2.5"]
|
||||
"requirements": ["ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==9.2.5"]
|
||||
"requirements": ["ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ class MatterLight(MatterEntity, LightEntity):
|
||||
|
||||
assert level_control is not None
|
||||
|
||||
level = round( # type: ignore[unreachable]
|
||||
level = round(
|
||||
renormalize(
|
||||
brightness,
|
||||
(0, 255),
|
||||
@@ -249,7 +249,7 @@ class MatterLight(MatterEntity, LightEntity):
|
||||
# We should not get here if brightness is not supported.
|
||||
assert level_control is not None
|
||||
|
||||
LOGGER.debug( # type: ignore[unreachable]
|
||||
LOGGER.debug(
|
||||
"Got brightness %s for %s",
|
||||
level_control.currentLevel,
|
||||
self.entity_id,
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/modbus",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymodbus"],
|
||||
"requirements": ["pymodbus==3.8.3"]
|
||||
"requirements": ["pymodbus==3.9.2"]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyatmo"],
|
||||
"requirements": ["pyatmo==9.2.0"]
|
||||
"requirements": ["pyatmo==9.2.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["py_nextbus"],
|
||||
"requirements": ["py-nextbusnext==2.1.2"]
|
||||
"requirements": ["py-nextbusnext==2.2.0"]
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ from pyrail.models import StationDetails
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
@@ -22,7 +20,6 @@ from .const import (
|
||||
CONF_EXCLUDE_VIAS,
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATION_FROM,
|
||||
CONF_STATION_LIVE,
|
||||
CONF_STATION_TO,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -115,68 +112,6 @@ class NMBSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import configuration from yaml."""
|
||||
try:
|
||||
self.stations = await self._fetch_stations()
|
||||
except CannotConnect:
|
||||
return self.async_abort(reason="api_unavailable")
|
||||
|
||||
station_from = None
|
||||
station_to = None
|
||||
station_live = None
|
||||
for station in self.stations:
|
||||
if user_input[CONF_STATION_FROM] in (
|
||||
station.standard_name,
|
||||
station.name,
|
||||
):
|
||||
station_from = station
|
||||
if user_input[CONF_STATION_TO] in (
|
||||
station.standard_name,
|
||||
station.name,
|
||||
):
|
||||
station_to = station
|
||||
if CONF_STATION_LIVE in user_input and user_input[CONF_STATION_LIVE] in (
|
||||
station.standard_name,
|
||||
station.name,
|
||||
):
|
||||
station_live = station
|
||||
|
||||
if station_from is None or station_to is None:
|
||||
return self.async_abort(reason="invalid_station")
|
||||
if station_from == station_to:
|
||||
return self.async_abort(reason="same_station")
|
||||
|
||||
# config flow uses id and not the standard name
|
||||
user_input[CONF_STATION_FROM] = station_from.id
|
||||
user_input[CONF_STATION_TO] = station_to.id
|
||||
|
||||
if station_live:
|
||||
user_input[CONF_STATION_LIVE] = station_live.id
|
||||
entity_registry = er.async_get(self.hass)
|
||||
prefix = "live"
|
||||
vias = "_excl_vias" if user_input.get(CONF_EXCLUDE_VIAS, False) else ""
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
f"{prefix}_{station_live.standard_name}_{station_from.standard_name}_{station_to.standard_name}",
|
||||
):
|
||||
new_unique_id = f"{DOMAIN}_{prefix}_{station_live.id}_{station_from.id}_{station_to.id}{vias}"
|
||||
entity_registry.async_update_entity(
|
||||
entity_id, new_unique_id=new_unique_id
|
||||
)
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
f"{prefix}_{station_live.name}_{station_from.name}_{station_to.name}",
|
||||
):
|
||||
new_unique_id = f"{DOMAIN}_{prefix}_{station_live.id}_{station_from.id}_{station_to.id}{vias}"
|
||||
entity_registry.async_update_entity(
|
||||
entity_id, new_unique_id=new_unique_id
|
||||
)
|
||||
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
|
||||
class CannotConnect(Exception):
|
||||
"""Error to indicate we cannot connect to NMBS."""
|
||||
|
||||
@@ -8,30 +8,19 @@ from typing import Any
|
||||
|
||||
from pyrail import iRail
|
||||
from pyrail.models import ConnectionDetails, LiveboardDeparture, StationDetails
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_SHOW_ON_MAP,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
@@ -47,22 +36,9 @@ from .const import ( # noqa: F401
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "NMBS"
|
||||
|
||||
DEFAULT_ICON = "mdi:train"
|
||||
DEFAULT_ICON_ALERT = "mdi:alert-octagon"
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_STATION_FROM): cv.string,
|
||||
vol.Required(CONF_STATION_TO): cv.string,
|
||||
vol.Optional(CONF_STATION_LIVE): cv.string,
|
||||
vol.Optional(CONF_EXCLUDE_VIAS, default=False): cv.boolean,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_time_until(departure_time: datetime | None = None):
|
||||
"""Calculate the time between now and a train's departure time."""
|
||||
@@ -85,71 +61,6 @@ def get_ride_duration(departure_time: datetime, arrival_time: datetime, delay=0)
|
||||
return duration_time + get_delay_in_minutes(delay)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the NMBS sensor with iRail API."""
|
||||
|
||||
if config[CONF_PLATFORM] == DOMAIN:
|
||||
if CONF_SHOW_ON_MAP not in config:
|
||||
config[CONF_SHOW_ON_MAP] = False
|
||||
if CONF_EXCLUDE_VIAS not in config:
|
||||
config[CONF_EXCLUDE_VIAS] = False
|
||||
|
||||
station_types = [CONF_STATION_FROM, CONF_STATION_TO, CONF_STATION_LIVE]
|
||||
|
||||
for station_type in station_types:
|
||||
station = (
|
||||
find_station_by_name(hass, config[station_type])
|
||||
if station_type in config
|
||||
else None
|
||||
)
|
||||
if station is None and station_type in config:
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_import_issue_station_not_found",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_import_issue_station_not_found",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "NMBS",
|
||||
"station_name": config[station_type],
|
||||
"url": "/config/integrations/dashboard/add?domain=nmbs",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "NMBS",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -336,7 +247,6 @@ class NMBSSensor(SensorEntity):
|
||||
|
||||
delay = get_delay_in_minutes(self._attrs.departure.delay)
|
||||
departure = get_time_until(self._attrs.departure.time)
|
||||
canceled = self._attrs.departure.canceled
|
||||
|
||||
attrs = {
|
||||
"destination": self._attrs.departure.station,
|
||||
@@ -346,14 +256,13 @@ class NMBSSensor(SensorEntity):
|
||||
"vehicle_id": self._attrs.departure.vehicle,
|
||||
}
|
||||
|
||||
if not canceled:
|
||||
attrs["departure"] = f"In {departure} minutes"
|
||||
attrs["departure_minutes"] = departure
|
||||
attrs["canceled"] = False
|
||||
else:
|
||||
attrs["canceled"] = self._attrs.departure.canceled
|
||||
if attrs["canceled"]:
|
||||
attrs["departure"] = None
|
||||
attrs["departure_minutes"] = None
|
||||
attrs["canceled"] = True
|
||||
else:
|
||||
attrs["departure"] = f"In {departure} minutes"
|
||||
attrs["departure_minutes"] = departure
|
||||
|
||||
if self._show_on_map and self.station_coordinates:
|
||||
attrs[ATTR_LATITUDE] = self.station_coordinates[0]
|
||||
@@ -369,9 +278,8 @@ class NMBSSensor(SensorEntity):
|
||||
via.timebetween
|
||||
) + get_delay_in_minutes(via.departure.delay)
|
||||
|
||||
if delay > 0:
|
||||
attrs["delay"] = f"{delay} minutes"
|
||||
attrs["delay_minutes"] = delay
|
||||
attrs["delay"] = f"{delay} minutes"
|
||||
attrs["delay_minutes"] = delay
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
@@ -25,11 +25,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_station_not_found": {
|
||||
"title": "The {integration_title} YAML configuration import failed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was a problem importing your YAML configuration.\n\nThe used station \"{station_name}\" could not be found. Fix it or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
"""The NZBGet integration."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
ATTR_SPEED,
|
||||
DATA_COORDINATOR,
|
||||
DATA_UNDO_UPDATE_LISTENER,
|
||||
DEFAULT_SPEED_LIMIT,
|
||||
DOMAIN,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RESUME,
|
||||
SERVICE_SET_SPEED,
|
||||
)
|
||||
from .const import DATA_COORDINATOR, DATA_UNDO_UPDATE_LISTENER, DOMAIN
|
||||
from .coordinator import NZBGetDataUpdateCoordinator
|
||||
from .services import async_register_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
SPEED_LIMIT_SCHEMA = vol.Schema(
|
||||
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int}
|
||||
)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up NZBGet integration."""
|
||||
|
||||
async_register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -44,8 +39,6 @@ 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
|
||||
|
||||
|
||||
@@ -60,31 +53,6 @@ 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)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""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_register_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,6 +64,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_config_entry": {
|
||||
"message": "Config entry not found or not loaded!"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"pause": {
|
||||
"name": "[%key:common::action::pause%]",
|
||||
|
||||
@@ -21,6 +21,7 @@ from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_NUM_CTX,
|
||||
CONF_PROMPT,
|
||||
CONF_THINK,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -33,6 +34,7 @@ __all__ = [
|
||||
"CONF_MODEL",
|
||||
"CONF_NUM_CTX",
|
||||
"CONF_PROMPT",
|
||||
"CONF_THINK",
|
||||
"CONF_URL",
|
||||
"DOMAIN",
|
||||
]
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.const import CONF_LLM_HASS_API, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
NumberSelector,
|
||||
NumberSelectorConfig,
|
||||
NumberSelectorMode,
|
||||
@@ -41,10 +42,12 @@ from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_NUM_CTX,
|
||||
CONF_PROMPT,
|
||||
CONF_THINK,
|
||||
DEFAULT_KEEP_ALIVE,
|
||||
DEFAULT_MAX_HISTORY,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_NUM_CTX,
|
||||
DEFAULT_THINK,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
MAX_NUM_CTX,
|
||||
@@ -280,6 +283,12 @@ def ollama_config_option_schema(
|
||||
min=-1, max=sys.maxsize, step=1, mode=NumberSelectorMode.BOX
|
||||
)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_THINK,
|
||||
description={
|
||||
"suggested_value": options.get("think", DEFAULT_THINK),
|
||||
},
|
||||
): BooleanSelector(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ DOMAIN = "ollama"
|
||||
|
||||
CONF_MODEL = "model"
|
||||
CONF_PROMPT = "prompt"
|
||||
CONF_THINK = "think"
|
||||
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
DEFAULT_KEEP_ALIVE = -1 # seconds. -1 = indefinite, 0 = never
|
||||
@@ -15,6 +16,7 @@ CONF_NUM_CTX = "num_ctx"
|
||||
DEFAULT_NUM_CTX = 8192
|
||||
MIN_NUM_CTX = 2048
|
||||
MAX_NUM_CTX = 131072
|
||||
DEFAULT_THINK = False
|
||||
|
||||
CONF_MAX_HISTORY = "max_history"
|
||||
DEFAULT_MAX_HISTORY = 20
|
||||
|
||||
@@ -24,6 +24,7 @@ from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_NUM_CTX,
|
||||
CONF_PROMPT,
|
||||
CONF_THINK,
|
||||
DEFAULT_KEEP_ALIVE,
|
||||
DEFAULT_MAX_HISTORY,
|
||||
DEFAULT_NUM_CTX,
|
||||
@@ -256,6 +257,7 @@ class OllamaConversationEntity(
|
||||
# keep_alive requires specifying unit. In this case, seconds
|
||||
keep_alive=f"{settings.get(CONF_KEEP_ALIVE, DEFAULT_KEEP_ALIVE)}s",
|
||||
options={CONF_NUM_CTX: settings.get(CONF_NUM_CTX, DEFAULT_NUM_CTX)},
|
||||
think=settings.get(CONF_THINK),
|
||||
)
|
||||
except (ollama.RequestError, ollama.ResponseError) as err:
|
||||
_LOGGER.error("Unexpected error talking to Ollama server: %s", err)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ollama",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ollama==0.4.7"]
|
||||
"requirements": ["ollama==0.5.1"]
|
||||
}
|
||||
|
||||
@@ -30,12 +30,14 @@
|
||||
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
|
||||
"max_history": "Max history messages",
|
||||
"num_ctx": "Context window size",
|
||||
"keep_alive": "Keep alive"
|
||||
"keep_alive": "Keep alive",
|
||||
"think": "Think before responding"
|
||||
},
|
||||
"data_description": {
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||
"keep_alive": "Duration in seconds for Ollama to keep model in memory. -1 = indefinite, 0 = never.",
|
||||
"num_ctx": "Maximum number of text tokens the model can process. Lower to reduce Ollama RAM, or increase for a large number of exposed entities."
|
||||
"num_ctx": "Maximum number of text tokens the model can process. Lower to reduce Ollama RAM, or increase for a large number of exposed entities.",
|
||||
"think": "If enabled, the LLM will think before responding. This can improve response quality but may increase latency."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,11 +121,10 @@ def async_register_services(hass: HomeAssistant) -> None:
|
||||
return {"files": [asdict(item_result) for item_result in upload_results]}
|
||||
return None
|
||||
|
||||
if not hass.services.has_service(DOMAIN, UPLOAD_SERVICE):
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.17.1"],
|
||||
"requirements": ["pyoverkiz==1.17.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
||||
@@ -5,14 +5,25 @@ from python_picnic_api2 import PicnicAPI
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY_CODE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_API, CONF_COORDINATOR, DOMAIN
|
||||
from .coordinator import PicnicUpdateCoordinator
|
||||
from .services import async_register_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.SENSOR, Platform.TODO]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Picnic integration."""
|
||||
|
||||
await async_register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_picnic_client(entry: ConfigEntry):
|
||||
"""Create an instance of the PicnicAPI client."""
|
||||
return PicnicAPI(
|
||||
@@ -37,9 +48,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Register the services
|
||||
await async_register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/picnic",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["python_picnic_api2"],
|
||||
"requirements": ["python-picnic-api2==1.2.4"]
|
||||
"requirements": ["python-picnic-api2==1.3.1"]
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ class PicnicServiceException(Exception):
|
||||
async def async_register_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for the Picnic integration, if not registered yet."""
|
||||
|
||||
if hass.services.has_service(DOMAIN, SERVICE_ADD_PRODUCT_TO_CART):
|
||||
return
|
||||
|
||||
async def async_add_product_service(call: ServiceCall):
|
||||
api_client = await get_api_client(hass, call.data[ATTR_CONFIG_ENTRY_ID])
|
||||
await handle_add_product(hass, api_client, call)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.const import ATTR_SW_VERSION
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
@@ -40,7 +38,5 @@ class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator[T]]):
|
||||
name=self.coordinator.config_entry.title,
|
||||
)
|
||||
if isinstance(self.coordinator, StatusDataUpdateCoordinator):
|
||||
device_info[ATTR_SW_VERSION] = cast(
|
||||
StatusDataUpdateCoordinator, self.coordinator
|
||||
).data.version
|
||||
device_info[ATTR_SW_VERSION] = self.coordinator.data.version
|
||||
return device_info
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiokem"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiokem==0.5.12"]
|
||||
"requirements": ["aiokem==1.0.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==9.2.5"]
|
||||
"requirements": ["ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -150,6 +150,10 @@ async def async_setup_entry(
|
||||
|
||||
if host.api.new_devices and config_entry.state == ConfigEntryState.LOADED:
|
||||
# Their are new cameras/chimes connected, reload to add them.
|
||||
_LOGGER.debug(
|
||||
"Reloading Reolink %s to add new device (capabilities)",
|
||||
host.api.nvr_name,
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_reload(config_entry.entry_id)
|
||||
)
|
||||
|
||||
@@ -194,6 +194,13 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
raise AbortFlow("already_configured")
|
||||
|
||||
if existing_entry and existing_entry.data[CONF_HOST] != discovery_info.ip:
|
||||
_LOGGER.debug(
|
||||
"Reolink DHCP reported new IP '%s', updating from old IP '%s'",
|
||||
discovery_info.ip,
|
||||
existing_entry.data[CONF_HOST],
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.13.4"]
|
||||
"requirements": ["reolink-aio==0.13.5"]
|
||||
}
|
||||
|
||||
@@ -636,14 +636,21 @@ class SamsungTVWSBridge(
|
||||
)
|
||||
self._remote = None
|
||||
except ConnectionFailure as err:
|
||||
LOGGER.warning(
|
||||
(
|
||||
error_details = err.args[0]
|
||||
if "ms.channel.timeOut" in (error_details := repr(err)):
|
||||
# The websocket was connected, but the TV is probably asleep
|
||||
LOGGER.debug(
|
||||
"Channel timeout occurred trying to get remote for %s: %s",
|
||||
self.host,
|
||||
error_details,
|
||||
)
|
||||
else:
|
||||
LOGGER.warning(
|
||||
"Unexpected ConnectionFailure trying to get remote for %s, "
|
||||
"please report this issue: %s"
|
||||
),
|
||||
self.host,
|
||||
repr(err),
|
||||
)
|
||||
"please report this issue: %s",
|
||||
self.host,
|
||||
error_details,
|
||||
)
|
||||
self._remote = None
|
||||
except (WebSocketException, AsyncioTimeoutError, OSError) as err:
|
||||
LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err))
|
||||
|
||||
@@ -39,7 +39,7 @@ class SamsungTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
|
||||
self.bridge = bridge
|
||||
self.is_on: bool | None = False
|
||||
self.is_on: bool | None = None
|
||||
self.async_extra_update: Callable[[], Coroutine[Any, Any, None]] | None = None
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
@@ -52,7 +52,12 @@ class SamsungTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
else:
|
||||
self.is_on = await self.bridge.async_is_on()
|
||||
if self.is_on != old_state:
|
||||
LOGGER.debug("TV %s state updated to %s", self.bridge.host, self.is_on)
|
||||
LOGGER.debug(
|
||||
"TV %s state updated from %s to %s",
|
||||
self.bridge.host,
|
||||
old_state,
|
||||
self.is_on,
|
||||
)
|
||||
|
||||
if self.async_extra_update:
|
||||
await self.async_extra_update()
|
||||
|
||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY, RPC_GENERATIONS
|
||||
from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY_G3, RPC_GENERATIONS
|
||||
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
|
||||
|
||||
from homeassistant.components.button import (
|
||||
@@ -62,7 +62,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
translation_key="self_test",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
press_action="trigger_shelly_gas_self_test",
|
||||
supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS,
|
||||
),
|
||||
ShellyButtonDescription[ShellyBlockCoordinator](
|
||||
key="mute",
|
||||
@@ -70,7 +70,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
translation_key="mute",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_shelly_gas_mute",
|
||||
supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS,
|
||||
),
|
||||
ShellyButtonDescription[ShellyBlockCoordinator](
|
||||
key="unmute",
|
||||
@@ -78,7 +78,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
translation_key="unmute",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_shelly_gas_unmute",
|
||||
supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -89,7 +89,7 @@ BLU_TRV_BUTTONS: Final[list[ShellyButtonDescription]] = [
|
||||
translation_key="calibrate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_blu_trv_calibration",
|
||||
supported=lambda coordinator: coordinator.device.model == MODEL_BLU_GATEWAY,
|
||||
supported=lambda coordinator: coordinator.model == MODEL_BLU_GATEWAY_G3,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -160,6 +160,7 @@ async def async_setup_entry(
|
||||
ShellyBluTrvButton(coordinator, button, id_)
|
||||
for id_ in blutrv_key_ids
|
||||
for button in BLU_TRV_BUTTONS
|
||||
if button.supported(coordinator)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -218,5 +218,6 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
}
|
||||
),
|
||||
description_placeholders={CONF_HOST: self._data[CONF_HOST]},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
},
|
||||
"description": "Enter your SMA device information.",
|
||||
"title": "Set up SMA Solar"
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"title": "[%key:component::sma::config::step::user::title%]",
|
||||
"description": "Do you want to set up the discovered SMA device ({host})?",
|
||||
"data": {
|
||||
"group": "[%key:component::sma::config::step::user::data::group%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pysmlight==0.2.5"],
|
||||
"requirements": ["pysmlight==0.2.6"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -130,11 +130,11 @@ async def async_generate_speaker_info(
|
||||
value = getattr(speaker, attrib)
|
||||
payload[attrib] = get_contents(value)
|
||||
|
||||
payload["enabled_entities"] = {
|
||||
payload["enabled_entities"] = sorted(
|
||||
entity_id
|
||||
for entity_id, s in hass.data[DATA_SONOS].entity_id_mappings.items()
|
||||
if s is speaker
|
||||
}
|
||||
)
|
||||
payload["media"] = await async_generate_media_info(hass, speaker)
|
||||
payload["activity_stats"] = speaker.activity_stats.report()
|
||||
payload["event_stats"] = speaker.event_stats.report()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.7.5", "av==13.1.0", "numpy==2.2.2"]
|
||||
"requirements": ["PyTurboJPEG==1.8.0", "av==13.1.0", "numpy==2.2.6"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ from homeassistant.helpers.typing import ConfigType
|
||||
# as we will always load it and we do not want to have
|
||||
# to wait for the import executor when its busy later
|
||||
# in the startup process.
|
||||
from . import sensor as sensor_pre_import # noqa: F401
|
||||
from . import (
|
||||
binary_sensor as binary_sensor_pre_import, # noqa: F401
|
||||
sensor as sensor_pre_import, # noqa: F401
|
||||
)
|
||||
from .const import ( # noqa: F401 # noqa: F401
|
||||
DOMAIN,
|
||||
STATE_ABOVE_HORIZON,
|
||||
@@ -24,6 +27,8 @@ from .const import ( # noqa: F401 # noqa: F401
|
||||
)
|
||||
from .entity import Sun, SunConfigEntry
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -52,14 +57,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool:
|
||||
await component.async_add_entities([sun])
|
||||
entry.runtime_data = sun
|
||||
entry.async_on_unload(sun.remove_listeners)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(
|
||||
entry, [Platform.SENSOR]
|
||||
):
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await entry.runtime_data.async_remove()
|
||||
return unload_ok
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Binary Sensor platform for Sun integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, SIGNAL_EVENTS_CHANGED
|
||||
from .entity import Sun, SunConfigEntry
|
||||
|
||||
ENTITY_ID_BINARY_SENSOR_FORMAT = BINARY_SENSOR_DOMAIN + ".sun_{}"
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class SunBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes a Sun binary sensor entity."""
|
||||
|
||||
value_fn: Callable[[Sun], bool | None]
|
||||
signal: str
|
||||
|
||||
|
||||
BINARY_SENSOR_TYPES: tuple[SunBinarySensorEntityDescription, ...] = (
|
||||
SunBinarySensorEntityDescription(
|
||||
key="solar_rising",
|
||||
translation_key="solar_rising",
|
||||
value_fn=lambda data: data.rising,
|
||||
entity_registry_enabled_default=False,
|
||||
signal=SIGNAL_EVENTS_CHANGED,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: SunConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sun binary sensor platform."""
|
||||
|
||||
sun = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
SunBinarySensor(sun, description, entry.entry_id)
|
||||
for description in BINARY_SENSOR_TYPES
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SunBinarySensor(BinarySensorEntity):
|
||||
"""Representation of a Sun binary sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
entity_description: SunBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sun: Sun,
|
||||
entity_description: SunBinarySensorEntityDescription,
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
"""Initiate Sun Binary Sensor."""
|
||||
self.entity_description = entity_description
|
||||
self.entity_id = ENTITY_ID_BINARY_SENSOR_FORMAT.format(entity_description.key)
|
||||
self._attr_unique_id = f"{entry_id}-{entity_description.key}"
|
||||
self.sun = sun
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name="Sun",
|
||||
identifiers={(DOMAIN, entry_id)},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return value of binary sensor."""
|
||||
return self.entity_description.value_fn(self.sun)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register signal listener when added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
self.entity_description.signal,
|
||||
self.async_write_ha_state,
|
||||
)
|
||||
)
|
||||
@@ -28,6 +28,15 @@
|
||||
"solar_rising": {
|
||||
"default": "mdi:sun-clock"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"solar_rising": {
|
||||
"default": "mdi:weather-sunny-off",
|
||||
"state": {
|
||||
"on": "mdi:weather-sunset-up",
|
||||
"off": "mdi:weather-sunset-down"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,15 @@
|
||||
"solar_azimuth": { "name": "Solar azimuth" },
|
||||
"solar_elevation": { "name": "Solar elevation" },
|
||||
"solar_rising": { "name": "Solar rising" }
|
||||
},
|
||||
"binary_sensor": {
|
||||
"solar_rising": {
|
||||
"name": "Solar rising",
|
||||
"state": {
|
||||
"on": "Rising",
|
||||
"off": "Setting"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +367,9 @@ class SwitchbotOptionsFlowHandler(OptionsFlow):
|
||||
),
|
||||
): int
|
||||
}
|
||||
if self.config_entry.data.get(CONF_SENSOR_TYPE) == SupportedModels.LOCK_PRO:
|
||||
if self.config_entry.data.get(CONF_SENSOR_TYPE, "").startswith(
|
||||
SupportedModels.LOCK
|
||||
):
|
||||
options.update(
|
||||
{
|
||||
vol.Optional(
|
||||
|
||||
@@ -12,7 +12,8 @@ from synology_dsm.exceptions import SynologyDSMNotLoggedInException
|
||||
from homeassistant.const import CONF_MAC, CONF_SCAN_INTERVAL, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .common import SynoApi, raise_config_entry_auth_error
|
||||
from .const import (
|
||||
@@ -34,10 +35,20 @@ from .coordinator import (
|
||||
SynologyDSMData,
|
||||
SynologyDSMSwitchUpdateCoordinator,
|
||||
)
|
||||
from .service import async_setup_services
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Synology DSM component."""
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SynologyDSMConfigEntry) -> bool:
|
||||
"""Set up Synology DSM sensors."""
|
||||
@@ -89,9 +100,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: SynologyDSMConfigEntry)
|
||||
details = EXCEPTION_UNKNOWN
|
||||
raise ConfigEntryNotReady(details) from err
|
||||
|
||||
# Services
|
||||
await async_setup_services(hass)
|
||||
|
||||
# For SSDP compat
|
||||
if not entry.data.get(CONF_MAC):
|
||||
hass.config_entries.async_update_entry(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,924 @@
|
||||
"""Telegram bot classes and utilities."""
|
||||
|
||||
from abc import abstractmethod
|
||||
import asyncio
|
||||
import io
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from telegram import (
|
||||
Bot,
|
||||
CallbackQuery,
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
Message,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
Update,
|
||||
User,
|
||||
)
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext import CallbackContext, filters
|
||||
from telegram.request import HTTPXRequest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_COMMAND,
|
||||
CONF_API_KEY,
|
||||
HTTP_BEARER_AUTHENTICATION,
|
||||
HTTP_DIGEST_AUTHENTICATION,
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
|
||||
|
||||
from .const import (
|
||||
ATTR_ARGS,
|
||||
ATTR_AUTHENTICATION,
|
||||
ATTR_CAPTION,
|
||||
ATTR_CHAT_ID,
|
||||
ATTR_CHAT_INSTANCE,
|
||||
ATTR_DATA,
|
||||
ATTR_DATE,
|
||||
ATTR_DISABLE_NOTIF,
|
||||
ATTR_DISABLE_WEB_PREV,
|
||||
ATTR_FILE,
|
||||
ATTR_FROM_FIRST,
|
||||
ATTR_FROM_LAST,
|
||||
ATTR_KEYBOARD,
|
||||
ATTR_KEYBOARD_INLINE,
|
||||
ATTR_MESSAGE,
|
||||
ATTR_MESSAGE_TAG,
|
||||
ATTR_MESSAGE_THREAD_ID,
|
||||
ATTR_MESSAGEID,
|
||||
ATTR_MSG,
|
||||
ATTR_MSGID,
|
||||
ATTR_ONE_TIME_KEYBOARD,
|
||||
ATTR_OPEN_PERIOD,
|
||||
ATTR_PARSER,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_REPLY_TO_MSGID,
|
||||
ATTR_REPLYMARKUP,
|
||||
ATTR_RESIZE_KEYBOARD,
|
||||
ATTR_STICKER_ID,
|
||||
ATTR_TEXT,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TITLE,
|
||||
ATTR_URL,
|
||||
ATTR_USER_ID,
|
||||
ATTR_USERNAME,
|
||||
ATTR_VERIFY_SSL,
|
||||
CONF_CHAT_ID,
|
||||
CONF_PROXY_PARAMS,
|
||||
CONF_PROXY_URL,
|
||||
DOMAIN,
|
||||
EVENT_TELEGRAM_CALLBACK,
|
||||
EVENT_TELEGRAM_COMMAND,
|
||||
EVENT_TELEGRAM_SENT,
|
||||
EVENT_TELEGRAM_TEXT,
|
||||
PARSER_HTML,
|
||||
PARSER_MD,
|
||||
PARSER_MD2,
|
||||
PARSER_PLAIN_TEXT,
|
||||
SERVICE_EDIT_CAPTION,
|
||||
SERVICE_EDIT_MESSAGE,
|
||||
SERVICE_SEND_ANIMATION,
|
||||
SERVICE_SEND_DOCUMENT,
|
||||
SERVICE_SEND_PHOTO,
|
||||
SERVICE_SEND_STICKER,
|
||||
SERVICE_SEND_VIDEO,
|
||||
SERVICE_SEND_VOICE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type TelegramBotConfigEntry = ConfigEntry[TelegramNotificationService]
|
||||
|
||||
|
||||
class BaseTelegramBot:
|
||||
"""The base class for the telegram bot."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TelegramBotConfigEntry) -> None:
|
||||
"""Initialize the bot base class."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the bot application."""
|
||||
|
||||
async def handle_update(self, update: Update, context: CallbackContext) -> bool:
|
||||
"""Handle updates from bot application set up by the respective platform."""
|
||||
_LOGGER.debug("Handling update %s", update)
|
||||
if not self.authorize_update(update):
|
||||
return False
|
||||
|
||||
# establish event type: text, command or callback_query
|
||||
if update.callback_query:
|
||||
# NOTE: Check for callback query first since effective message will be populated with the message
|
||||
# in .callback_query (python-telegram-bot docs are wrong)
|
||||
event_type, event_data = self._get_callback_query_event_data(
|
||||
update.callback_query
|
||||
)
|
||||
elif update.effective_message:
|
||||
event_type, event_data = self._get_message_event_data(
|
||||
update.effective_message
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning("Unhandled update: %s", update)
|
||||
return True
|
||||
|
||||
event_context = Context()
|
||||
|
||||
_LOGGER.debug("Firing event %s: %s", event_type, event_data)
|
||||
self.hass.bus.async_fire(event_type, event_data, context=event_context)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _get_command_event_data(command_text: str | None) -> dict[str, str | list]:
|
||||
if not command_text or not command_text.startswith("/"):
|
||||
return {}
|
||||
command_parts = command_text.split()
|
||||
command = command_parts[0]
|
||||
args = command_parts[1:]
|
||||
return {ATTR_COMMAND: command, ATTR_ARGS: args}
|
||||
|
||||
def _get_message_event_data(self, message: Message) -> tuple[str, dict[str, Any]]:
|
||||
event_data: dict[str, Any] = {
|
||||
ATTR_MSGID: message.message_id,
|
||||
ATTR_CHAT_ID: message.chat.id,
|
||||
ATTR_DATE: message.date,
|
||||
ATTR_MESSAGE_THREAD_ID: message.message_thread_id,
|
||||
}
|
||||
if filters.COMMAND.filter(message):
|
||||
# This is a command message - set event type to command and split data into command and args
|
||||
event_type = EVENT_TELEGRAM_COMMAND
|
||||
event_data.update(self._get_command_event_data(message.text))
|
||||
else:
|
||||
event_type = EVENT_TELEGRAM_TEXT
|
||||
event_data[ATTR_TEXT] = message.text
|
||||
|
||||
if message.from_user:
|
||||
event_data.update(self._get_user_event_data(message.from_user))
|
||||
|
||||
return event_type, event_data
|
||||
|
||||
def _get_user_event_data(self, user: User) -> dict[str, Any]:
|
||||
return {
|
||||
ATTR_USER_ID: user.id,
|
||||
ATTR_FROM_FIRST: user.first_name,
|
||||
ATTR_FROM_LAST: user.last_name,
|
||||
}
|
||||
|
||||
def _get_callback_query_event_data(
|
||||
self, callback_query: CallbackQuery
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
event_type = EVENT_TELEGRAM_CALLBACK
|
||||
event_data: dict[str, Any] = {
|
||||
ATTR_MSGID: callback_query.id,
|
||||
ATTR_CHAT_INSTANCE: callback_query.chat_instance,
|
||||
ATTR_DATA: callback_query.data,
|
||||
ATTR_MSG: None,
|
||||
ATTR_CHAT_ID: None,
|
||||
}
|
||||
if callback_query.message:
|
||||
event_data[ATTR_MSG] = callback_query.message.to_dict()
|
||||
event_data[ATTR_CHAT_ID] = callback_query.message.chat.id
|
||||
|
||||
if callback_query.from_user:
|
||||
event_data.update(self._get_user_event_data(callback_query.from_user))
|
||||
|
||||
# Split data into command and args if possible
|
||||
event_data.update(self._get_command_event_data(callback_query.data))
|
||||
|
||||
return event_type, event_data
|
||||
|
||||
def authorize_update(self, update: Update) -> bool:
|
||||
"""Make sure either user or chat is in allowed_chat_ids."""
|
||||
from_user = update.effective_user.id if update.effective_user else None
|
||||
from_chat = update.effective_chat.id if update.effective_chat else None
|
||||
allowed_chat_ids: list[int] = [
|
||||
subentry.data[CONF_CHAT_ID] for subentry in self.config.subentries.values()
|
||||
]
|
||||
if from_user in allowed_chat_ids or from_chat in allowed_chat_ids:
|
||||
return True
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Unauthorized update - neither user id %s nor chat id %s is in allowed"
|
||||
" chats: %s"
|
||||
),
|
||||
from_user,
|
||||
from_chat,
|
||||
allowed_chat_ids,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class TelegramNotificationService:
|
||||
"""Implement the notification services for the Telegram Bot domain."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
app: BaseTelegramBot,
|
||||
bot: Bot,
|
||||
config: TelegramBotConfigEntry,
|
||||
parser: str,
|
||||
) -> None:
|
||||
"""Initialize the service."""
|
||||
self.app = app
|
||||
self.config = config
|
||||
self._parsers = {
|
||||
PARSER_HTML: ParseMode.HTML,
|
||||
PARSER_MD: ParseMode.MARKDOWN,
|
||||
PARSER_MD2: ParseMode.MARKDOWN_V2,
|
||||
PARSER_PLAIN_TEXT: None,
|
||||
}
|
||||
self._parse_mode = self._parsers.get(parser)
|
||||
self.bot = bot
|
||||
self.hass = hass
|
||||
|
||||
def _get_allowed_chat_ids(self) -> list[int]:
|
||||
allowed_chat_ids: list[int] = [
|
||||
subentry.data[CONF_CHAT_ID] for subentry in self.config.subentries.values()
|
||||
]
|
||||
|
||||
if not allowed_chat_ids:
|
||||
bot_name: str = self.config.title
|
||||
raise ServiceValidationError(
|
||||
"No allowed chat IDs found for bot",
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_allowed_chat_ids",
|
||||
translation_placeholders={
|
||||
"bot_name": bot_name,
|
||||
},
|
||||
)
|
||||
|
||||
return allowed_chat_ids
|
||||
|
||||
def _get_last_message_id(self):
|
||||
return dict.fromkeys(self._get_allowed_chat_ids())
|
||||
|
||||
def _get_msg_ids(self, msg_data, chat_id):
|
||||
"""Get the message id to edit.
|
||||
|
||||
This can be one of (message_id, inline_message_id) from a msg dict,
|
||||
returning a tuple.
|
||||
**You can use 'last' as message_id** to edit
|
||||
the message last sent in the chat_id.
|
||||
"""
|
||||
message_id = inline_message_id = None
|
||||
if ATTR_MESSAGEID in msg_data:
|
||||
message_id = msg_data[ATTR_MESSAGEID]
|
||||
if (
|
||||
isinstance(message_id, str)
|
||||
and (message_id == "last")
|
||||
and (self._get_last_message_id()[chat_id] is not None)
|
||||
):
|
||||
message_id = self._get_last_message_id()[chat_id]
|
||||
else:
|
||||
inline_message_id = msg_data["inline_message_id"]
|
||||
return message_id, inline_message_id
|
||||
|
||||
def _get_target_chat_ids(self, target):
|
||||
"""Validate chat_id targets or return default target (first).
|
||||
|
||||
:param target: optional list of integers ([12234, -12345])
|
||||
:return list of chat_id targets (integers)
|
||||
"""
|
||||
allowed_chat_ids: list[int] = self._get_allowed_chat_ids()
|
||||
default_user: int = allowed_chat_ids[0]
|
||||
if target is not None:
|
||||
if isinstance(target, int):
|
||||
target = [target]
|
||||
chat_ids = [t for t in target if t in allowed_chat_ids]
|
||||
if chat_ids:
|
||||
return chat_ids
|
||||
_LOGGER.warning(
|
||||
"Disallowed targets: %s, using default: %s", target, default_user
|
||||
)
|
||||
return [default_user]
|
||||
|
||||
def _get_msg_kwargs(self, data):
|
||||
"""Get parameters in message data kwargs."""
|
||||
|
||||
def _make_row_inline_keyboard(row_keyboard):
|
||||
"""Make a list of InlineKeyboardButtons.
|
||||
|
||||
It can accept:
|
||||
- a list of tuples like:
|
||||
`[(text_b1, data_callback_b1),
|
||||
(text_b2, data_callback_b2), ...]
|
||||
- a string like: `/cmd1, /cmd2, /cmd3`
|
||||
- or a string like: `text_b1:/cmd1, text_b2:/cmd2`
|
||||
- also supports urls instead of callback commands
|
||||
"""
|
||||
buttons = []
|
||||
if isinstance(row_keyboard, str):
|
||||
for key in row_keyboard.split(","):
|
||||
if ":/" in key:
|
||||
# check if command or URL
|
||||
if key.startswith("https://"):
|
||||
label = key.split(",")[0]
|
||||
url = key[len(label) + 1 :]
|
||||
buttons.append(InlineKeyboardButton(label, url=url))
|
||||
else:
|
||||
# commands like: 'Label:/cmd' become ('Label', '/cmd')
|
||||
label = key.split(":/")[0]
|
||||
command = key[len(label) + 1 :]
|
||||
buttons.append(
|
||||
InlineKeyboardButton(label, callback_data=command)
|
||||
)
|
||||
else:
|
||||
# commands like: '/cmd' become ('CMD', '/cmd')
|
||||
label = key.strip()[1:].upper()
|
||||
buttons.append(InlineKeyboardButton(label, callback_data=key))
|
||||
elif isinstance(row_keyboard, list):
|
||||
for entry in row_keyboard:
|
||||
text_btn, data_btn = entry
|
||||
if data_btn.startswith("https://"):
|
||||
buttons.append(InlineKeyboardButton(text_btn, url=data_btn))
|
||||
else:
|
||||
buttons.append(
|
||||
InlineKeyboardButton(text_btn, callback_data=data_btn)
|
||||
)
|
||||
else:
|
||||
raise TypeError(str(row_keyboard))
|
||||
return buttons
|
||||
|
||||
# Defaults
|
||||
params = {
|
||||
ATTR_PARSER: self._parse_mode,
|
||||
ATTR_DISABLE_NOTIF: False,
|
||||
ATTR_DISABLE_WEB_PREV: None,
|
||||
ATTR_REPLY_TO_MSGID: None,
|
||||
ATTR_REPLYMARKUP: None,
|
||||
ATTR_TIMEOUT: None,
|
||||
ATTR_MESSAGE_TAG: None,
|
||||
ATTR_MESSAGE_THREAD_ID: None,
|
||||
}
|
||||
if data is not None:
|
||||
if ATTR_PARSER in data:
|
||||
params[ATTR_PARSER] = self._parsers.get(
|
||||
data[ATTR_PARSER], self._parse_mode
|
||||
)
|
||||
if ATTR_TIMEOUT in data:
|
||||
params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT]
|
||||
if ATTR_DISABLE_NOTIF in data:
|
||||
params[ATTR_DISABLE_NOTIF] = data[ATTR_DISABLE_NOTIF]
|
||||
if ATTR_DISABLE_WEB_PREV in data:
|
||||
params[ATTR_DISABLE_WEB_PREV] = data[ATTR_DISABLE_WEB_PREV]
|
||||
if ATTR_REPLY_TO_MSGID in data:
|
||||
params[ATTR_REPLY_TO_MSGID] = data[ATTR_REPLY_TO_MSGID]
|
||||
if ATTR_MESSAGE_TAG in data:
|
||||
params[ATTR_MESSAGE_TAG] = data[ATTR_MESSAGE_TAG]
|
||||
if ATTR_MESSAGE_THREAD_ID in data:
|
||||
params[ATTR_MESSAGE_THREAD_ID] = data[ATTR_MESSAGE_THREAD_ID]
|
||||
# Keyboards:
|
||||
if ATTR_KEYBOARD in data:
|
||||
keys = data.get(ATTR_KEYBOARD)
|
||||
keys = keys if isinstance(keys, list) else [keys]
|
||||
if keys:
|
||||
params[ATTR_REPLYMARKUP] = ReplyKeyboardMarkup(
|
||||
[[key.strip() for key in row.split(",")] for row in keys],
|
||||
resize_keyboard=data.get(ATTR_RESIZE_KEYBOARD, False),
|
||||
one_time_keyboard=data.get(ATTR_ONE_TIME_KEYBOARD, False),
|
||||
)
|
||||
else:
|
||||
params[ATTR_REPLYMARKUP] = ReplyKeyboardRemove(True)
|
||||
|
||||
elif ATTR_KEYBOARD_INLINE in data:
|
||||
keys = data.get(ATTR_KEYBOARD_INLINE)
|
||||
keys = keys if isinstance(keys, list) else [keys]
|
||||
params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup(
|
||||
[_make_row_inline_keyboard(row) for row in keys]
|
||||
)
|
||||
return params
|
||||
|
||||
async def _send_msg(
|
||||
self, func_send, msg_error, message_tag, *args_msg, context=None, **kwargs_msg
|
||||
):
|
||||
"""Send one message."""
|
||||
try:
|
||||
out = await func_send(*args_msg, **kwargs_msg)
|
||||
if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID):
|
||||
chat_id = out.chat_id
|
||||
message_id = out[ATTR_MESSAGEID]
|
||||
self._get_last_message_id()[chat_id] = message_id
|
||||
_LOGGER.debug(
|
||||
"Last message ID: %s (from chat_id %s)",
|
||||
self._get_last_message_id(),
|
||||
chat_id,
|
||||
)
|
||||
|
||||
event_data = {
|
||||
ATTR_CHAT_ID: chat_id,
|
||||
ATTR_MESSAGEID: message_id,
|
||||
}
|
||||
if message_tag is not None:
|
||||
event_data[ATTR_MESSAGE_TAG] = message_tag
|
||||
if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None:
|
||||
event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[
|
||||
ATTR_MESSAGE_THREAD_ID
|
||||
]
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_TELEGRAM_SENT, event_data, context=context
|
||||
)
|
||||
elif not isinstance(out, bool):
|
||||
_LOGGER.warning(
|
||||
"Update last message: out_type:%s, out=%s", type(out), out
|
||||
)
|
||||
except TelegramError as exc:
|
||||
_LOGGER.error(
|
||||
"%s: %s. Args: %s, kwargs: %s", msg_error, exc, args_msg, kwargs_msg
|
||||
)
|
||||
return None
|
||||
return out
|
||||
|
||||
async def send_message(self, message="", target=None, context=None, **kwargs):
|
||||
"""Send a message to one or multiple pre-allowed chat IDs."""
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
text = f"{title}\n{message}" if title else message
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
msg_ids = {}
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params)
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_message,
|
||||
"Error sending message",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id,
|
||||
text,
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV],
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
if msg is not None:
|
||||
msg_ids[chat_id] = msg.id
|
||||
return msg_ids
|
||||
|
||||
async def delete_message(self, chat_id=None, context=None, **kwargs):
|
||||
"""Delete a previously sent message."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
message_id, _ = self._get_msg_ids(kwargs, chat_id)
|
||||
_LOGGER.debug("Delete message %s in chat ID %s", message_id, chat_id)
|
||||
deleted = await self._send_msg(
|
||||
self.bot.delete_message,
|
||||
"Error deleting message",
|
||||
None,
|
||||
chat_id,
|
||||
message_id,
|
||||
context=context,
|
||||
)
|
||||
# reduce message_id anyway:
|
||||
if self._get_last_message_id()[chat_id] is not None:
|
||||
# change last msg_id for deque(n_msgs)?
|
||||
self._get_last_message_id()[chat_id] -= 1
|
||||
return deleted
|
||||
|
||||
async def edit_message(self, type_edit, chat_id=None, context=None, **kwargs):
|
||||
"""Edit a previously sent message."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
message_id, inline_message_id = self._get_msg_ids(kwargs, chat_id)
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
_LOGGER.debug(
|
||||
"Edit message %s in chat ID %s with params: %s",
|
||||
message_id or inline_message_id,
|
||||
chat_id,
|
||||
params,
|
||||
)
|
||||
if type_edit == SERVICE_EDIT_MESSAGE:
|
||||
message = kwargs.get(ATTR_MESSAGE)
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
text = f"{title}\n{message}" if title else message
|
||||
_LOGGER.debug("Editing message with ID %s", message_id or inline_message_id)
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_text,
|
||||
"Error editing text message",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
text,
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
inline_message_id=inline_message_id,
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
context=context,
|
||||
)
|
||||
if type_edit == SERVICE_EDIT_CAPTION:
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_caption,
|
||||
"Error editing message attributes",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
inline_message_id=inline_message_id,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
context=context,
|
||||
)
|
||||
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_reply_markup,
|
||||
"Error editing message attributes",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
inline_message_id=inline_message_id,
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
context=context,
|
||||
)
|
||||
|
||||
async def answer_callback_query(
|
||||
self, message, callback_query_id, show_alert=False, context=None, **kwargs
|
||||
):
|
||||
"""Answer a callback originated with a press in an inline keyboard."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
_LOGGER.debug(
|
||||
"Answer callback query with callback ID %s: %s, alert: %s",
|
||||
callback_query_id,
|
||||
message,
|
||||
show_alert,
|
||||
)
|
||||
await self._send_msg(
|
||||
self.bot.answer_callback_query,
|
||||
"Error sending answer callback query",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
callback_query_id,
|
||||
text=message,
|
||||
show_alert=show_alert,
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
context=context,
|
||||
)
|
||||
|
||||
async def send_file(
|
||||
self, file_type=SERVICE_SEND_PHOTO, target=None, context=None, **kwargs
|
||||
):
|
||||
"""Send a photo, sticker, video, or document."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
file_content = await load_data(
|
||||
self.hass,
|
||||
url=kwargs.get(ATTR_URL),
|
||||
filepath=kwargs.get(ATTR_FILE),
|
||||
username=kwargs.get(ATTR_USERNAME),
|
||||
password=kwargs.get(ATTR_PASSWORD),
|
||||
authentication=kwargs.get(ATTR_AUTHENTICATION),
|
||||
verify_ssl=(
|
||||
get_default_context()
|
||||
if kwargs.get(ATTR_VERIFY_SSL, False)
|
||||
else get_default_no_verify_context()
|
||||
),
|
||||
)
|
||||
|
||||
msg_ids = {}
|
||||
if file_content:
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug("Sending file to chat ID %s", chat_id)
|
||||
|
||||
if file_type == SERVICE_SEND_PHOTO:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_photo,
|
||||
"Error sending photo",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
photo=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
|
||||
elif file_type == SERVICE_SEND_STICKER:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_sticker,
|
||||
"Error sending sticker",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
sticker=file_content,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
|
||||
elif file_type == SERVICE_SEND_VIDEO:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_video,
|
||||
"Error sending video",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
video=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
elif file_type == SERVICE_SEND_DOCUMENT:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_document,
|
||||
"Error sending document",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
document=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
elif file_type == SERVICE_SEND_VOICE:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_voice,
|
||||
"Error sending voice",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
voice=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
elif file_type == SERVICE_SEND_ANIMATION:
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_animation,
|
||||
"Error sending animation",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
animation=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
|
||||
msg_ids[chat_id] = msg.id
|
||||
file_content.seek(0)
|
||||
else:
|
||||
_LOGGER.error("Can't send file with kwargs: %s", kwargs)
|
||||
|
||||
return msg_ids
|
||||
|
||||
async def send_sticker(self, target=None, context=None, **kwargs) -> dict:
|
||||
"""Send a sticker from a telegram sticker pack."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
stickerid = kwargs.get(ATTR_STICKER_ID)
|
||||
|
||||
msg_ids = {}
|
||||
if stickerid:
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_sticker,
|
||||
"Error sending sticker",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
sticker=stickerid,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
msg_ids[chat_id] = msg.id
|
||||
return msg_ids
|
||||
return await self.send_file(SERVICE_SEND_STICKER, target, **kwargs)
|
||||
|
||||
async def send_location(
|
||||
self, latitude, longitude, target=None, context=None, **kwargs
|
||||
):
|
||||
"""Send a location."""
|
||||
latitude = float(latitude)
|
||||
longitude = float(longitude)
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
msg_ids = {}
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug(
|
||||
"Send location %s/%s to chat ID %s", latitude, longitude, chat_id
|
||||
)
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_location,
|
||||
"Error sending location",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
msg_ids[chat_id] = msg.id
|
||||
return msg_ids
|
||||
|
||||
async def send_poll(
|
||||
self,
|
||||
question,
|
||||
options,
|
||||
is_anonymous,
|
||||
allows_multiple_answers,
|
||||
target=None,
|
||||
context=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Send a poll."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
openperiod = kwargs.get(ATTR_OPEN_PERIOD)
|
||||
msg_ids = {}
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug("Send poll '%s' to chat ID %s", question, chat_id)
|
||||
msg = await self._send_msg(
|
||||
self.bot.send_poll,
|
||||
"Error sending poll",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
question=question,
|
||||
options=options,
|
||||
is_anonymous=is_anonymous,
|
||||
allows_multiple_answers=allows_multiple_answers,
|
||||
open_period=openperiod,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
|
||||
context=context,
|
||||
)
|
||||
msg_ids[chat_id] = msg.id
|
||||
return msg_ids
|
||||
|
||||
async def leave_chat(self, chat_id=None, context=None, **kwargs):
|
||||
"""Remove bot from chat."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
_LOGGER.debug("Leave from chat ID %s", chat_id)
|
||||
return await self._send_msg(
|
||||
self.bot.leave_chat, "Error leaving chat", None, chat_id, context=context
|
||||
)
|
||||
|
||||
|
||||
def initialize_bot(hass: HomeAssistant, p_config: MappingProxyType[str, Any]) -> Bot:
|
||||
"""Initialize telegram bot with proxy support."""
|
||||
api_key: str = p_config[CONF_API_KEY]
|
||||
proxy_url: str | None = p_config.get(CONF_PROXY_URL)
|
||||
proxy_params: dict | None = p_config.get(CONF_PROXY_PARAMS)
|
||||
|
||||
if proxy_url is not None:
|
||||
auth = None
|
||||
if proxy_params is None:
|
||||
# CONF_PROXY_PARAMS has been kept for backwards compatibility.
|
||||
proxy_params = {}
|
||||
elif "username" in proxy_params and "password" in proxy_params:
|
||||
# Auth can actually be stuffed into the URL, but the docs have previously
|
||||
# indicated to put them here.
|
||||
auth = proxy_params.pop("username"), proxy_params.pop("password")
|
||||
ir.create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"proxy_params_auth_deprecation",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_persistent=False,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_placeholders={
|
||||
"proxy_params": CONF_PROXY_PARAMS,
|
||||
"proxy_url": CONF_PROXY_URL,
|
||||
"telegram_bot": "Telegram bot",
|
||||
},
|
||||
translation_key="proxy_params_auth_deprecation",
|
||||
learn_more_url="https://github.com/home-assistant/core/pull/112778",
|
||||
)
|
||||
else:
|
||||
ir.create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"proxy_params_deprecation",
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
is_persistent=False,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_placeholders={
|
||||
"proxy_params": CONF_PROXY_PARAMS,
|
||||
"proxy_url": CONF_PROXY_URL,
|
||||
"httpx": "httpx",
|
||||
"telegram_bot": "Telegram bot",
|
||||
},
|
||||
translation_key="proxy_params_deprecation",
|
||||
learn_more_url="https://github.com/home-assistant/core/pull/112778",
|
||||
)
|
||||
proxy = httpx.Proxy(proxy_url, auth=auth, **proxy_params)
|
||||
request = HTTPXRequest(connection_pool_size=8, proxy=proxy)
|
||||
else:
|
||||
request = HTTPXRequest(connection_pool_size=8)
|
||||
return Bot(token=api_key, request=request)
|
||||
|
||||
|
||||
async def load_data(
|
||||
hass: HomeAssistant,
|
||||
url=None,
|
||||
filepath=None,
|
||||
username=None,
|
||||
password=None,
|
||||
authentication=None,
|
||||
num_retries=5,
|
||||
verify_ssl=None,
|
||||
):
|
||||
"""Load data into ByteIO/File container from a source."""
|
||||
try:
|
||||
if url is not None:
|
||||
# Load data from URL
|
||||
params: dict[str, Any] = {}
|
||||
headers = {}
|
||||
if authentication == HTTP_BEARER_AUTHENTICATION and password is not None:
|
||||
headers = {"Authorization": f"Bearer {password}"}
|
||||
elif username is not None and password is not None:
|
||||
if authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
params["auth"] = httpx.DigestAuth(username, password)
|
||||
else:
|
||||
params["auth"] = httpx.BasicAuth(username, password)
|
||||
if verify_ssl is not None:
|
||||
params["verify"] = verify_ssl
|
||||
|
||||
retry_num = 0
|
||||
async with httpx.AsyncClient(
|
||||
timeout=15, headers=headers, **params
|
||||
) as client:
|
||||
while retry_num < num_retries:
|
||||
req = await client.get(url)
|
||||
if req.status_code != 200:
|
||||
_LOGGER.warning(
|
||||
"Status code %s (retry #%s) loading %s",
|
||||
req.status_code,
|
||||
retry_num + 1,
|
||||
url,
|
||||
)
|
||||
else:
|
||||
data = io.BytesIO(req.content)
|
||||
if data.read():
|
||||
data.seek(0)
|
||||
data.name = url
|
||||
return data
|
||||
_LOGGER.warning(
|
||||
"Empty data (retry #%s) in %s)", retry_num + 1, url
|
||||
)
|
||||
retry_num += 1
|
||||
if retry_num < num_retries:
|
||||
await asyncio.sleep(
|
||||
1
|
||||
) # Add a sleep to allow other async operations to proceed
|
||||
_LOGGER.warning(
|
||||
"Can't load data in %s after %s retries", url, retry_num
|
||||
)
|
||||
elif filepath is not None:
|
||||
if hass.config.is_allowed_path(filepath):
|
||||
return await hass.async_add_executor_job(
|
||||
_read_file_as_bytesio, filepath
|
||||
)
|
||||
|
||||
_LOGGER.warning("'%s' are not secure to load data from!", filepath)
|
||||
else:
|
||||
_LOGGER.warning("Can't load data. No data found in params!")
|
||||
|
||||
except (OSError, TypeError) as error:
|
||||
_LOGGER.error("Can't load data into ByteIO: %s", error)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _read_file_as_bytesio(file_path: str) -> io.BytesIO:
|
||||
"""Read a file and return it as a BytesIO object."""
|
||||
with open(file_path, "rb") as file:
|
||||
data = io.BytesIO(file.read())
|
||||
data.name = file_path
|
||||
return data
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user