forked from home-assistant/core
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8aafc6702 | |||
| a53d010dc2 | |||
| 22b5b67a25 | |||
| e3fe99e130 | |||
| 6f718c2de0 | |||
| 012cea9f37 | |||
| 335621a7a9 | |||
| 1c4c52e46a | |||
| dca353c306 | |||
| a9e39c9354 | |||
| a49b584955 | |||
| 3a116ea4ad | |||
| 4e02f5406e | |||
| e3ae289ee4 | |||
| f0906ac5c0 | |||
| e1739ccd87 | |||
| 83106c9e9d | |||
| 55da0d1d4f | |||
| 6fa4190441 | |||
| 9202b4d746 | |||
| 9f00dfdbbf | |||
| d7086d5c7c | |||
| b3074d272a |
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
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@v10
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
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-check --all-files --show-diff-on-failure
|
||||
pre-commit run --hook-stage manual ruff --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.19
|
||||
uses: github/codeql-action/init@v3.28.18
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.19
|
||||
uses: github/codeql-action/analyze@v3.28.18
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
|
||||
core:
|
||||
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
|
||||
if: false && github.repository_owner == 'home-assistant'
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: init
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -176,26 +176,12 @@ jobs:
|
||||
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: init
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
abi: cp313
|
||||
- os: ubuntu-latest
|
||||
arch: i386
|
||||
abi: cp313
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: aarch64
|
||||
abi: cp313
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: armv7
|
||||
abi: cp313
|
||||
- os: ubuntu-latest
|
||||
arch: armhf
|
||||
abi: cp313
|
||||
abi: ["cp313"]
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
@@ -232,31 +218,8 @@ jobs:
|
||||
sed -i "/uv/d" requirements.txt
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Create requirements file for custom build
|
||||
run: |
|
||||
touch requirements_custom.txt
|
||||
echo "protobuf==6.30.2" >> requirements_custom.txt
|
||||
echo "grpcio==1.72.1" >> requirements_custom.txt
|
||||
echo "grpcio-status==1.72.1" >> requirements_custom.txt
|
||||
echo "grpcio-reflection==1.72.1" >> requirements_custom.txt
|
||||
|
||||
- name: Build wheels (custom)
|
||||
uses: cdce8p/wheels@master
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements: "requirements_custom.txt"
|
||||
verbose: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
if: false
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.12
|
||||
rev: v0.11.0
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- id: ruff-format
|
||||
@@ -30,7 +30,7 @@ repos:
|
||||
- --branch=master
|
||||
- --branch=rc
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.37.1
|
||||
rev: v1.35.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-check --all-files",
|
||||
"command": "pre-commit run ruff --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, Platform.SENSOR]
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
|
||||
|
||||
@@ -41,30 +41,7 @@ class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||
"""Fetch data from the Adax."""
|
||||
try:
|
||||
if hasattr(self.adax_data_handler, "fetch_rooms_info"):
|
||||
rooms = await self.adax_data_handler.fetch_rooms_info() or []
|
||||
_LOGGER.debug("fetch_rooms_info returned: %s", rooms)
|
||||
else:
|
||||
_LOGGER.debug("fetch_rooms_info method not available, using get_rooms")
|
||||
rooms = []
|
||||
|
||||
if not rooms:
|
||||
_LOGGER.debug(
|
||||
"No rooms from fetch_rooms_info, trying get_rooms as fallback"
|
||||
)
|
||||
rooms = await self.adax_data_handler.get_rooms() or []
|
||||
_LOGGER.debug("get_rooms fallback returned: %s", rooms)
|
||||
|
||||
if not rooms:
|
||||
raise UpdateFailed("No rooms available from Adax API")
|
||||
|
||||
except OSError as e:
|
||||
raise UpdateFailed(f"Error communicating with API: {e}") from e
|
||||
|
||||
for room in rooms:
|
||||
room["energyWh"] = int(room.get("energyWh", 0))
|
||||
|
||||
rooms = await self.adax_data_handler.get_rooms() or []
|
||||
return {r["id"]: r for r in rooms}
|
||||
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
"""Support for Adax energy sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AdaxConfigEntry
|
||||
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AdaxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Adax energy sensors with config flow."""
|
||||
if entry.data.get(CONNECTION_TYPE) != LOCAL:
|
||||
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
|
||||
|
||||
# Create individual energy sensors for each device
|
||||
async_add_entities(
|
||||
AdaxEnergySensor(cloud_coordinator, device_id)
|
||||
for device_id in cloud_coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AdaxEnergySensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
|
||||
"""Representation of an Adax energy sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "energy"
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
|
||||
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
_attr_suggested_display_precision = 3
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AdaxCloudCoordinator,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize the energy sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._device_id = device_id
|
||||
room = coordinator.data[device_id]
|
||||
|
||||
self._attr_unique_id = f"{room['homeId']}_{device_id}_energy"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=room["name"],
|
||||
manufacturer="Adax",
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available and "energyWh" in self.coordinator.data[self._device_id]
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
"""Return the native value of the sensor."""
|
||||
return int(self.coordinator.data[self._device_id]["energyWh"])
|
||||
@@ -61,7 +61,7 @@
|
||||
"display_pm_standard": {
|
||||
"name": "Display PM standard",
|
||||
"state": {
|
||||
"ugm3": "µg/m³",
|
||||
"ugm3": "μg/m³",
|
||||
"us_aqi": "US AQI"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
"""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==3.0.5"]
|
||||
"requirements": ["aioamazondevices==2.1.1"]
|
||||
}
|
||||
|
||||
@@ -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, BASE_PLATFORMS, __version__ as HA_VERSION
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -225,8 +225,7 @@ class Analytics:
|
||||
LOGGER.error(err)
|
||||
return
|
||||
|
||||
configuration_set = _domains_from_yaml_config(yaml_configuration)
|
||||
|
||||
configuration_set = set(yaml_configuration)
|
||||
er_platforms = {
|
||||
entity.platform
|
||||
for entity in ent_reg.entities.values()
|
||||
@@ -371,13 +370,3 @@ 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
|
||||
|
||||
@@ -92,7 +92,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
key="radiation_rate",
|
||||
translation_key="radiation_rate",
|
||||
name="Radiation Dose Rate",
|
||||
native_unit_of_measurement="μSv/h",
|
||||
native_unit_of_measurement="μSv/h", # "μ" == "\u03bc"
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
scale=0.001,
|
||||
|
||||
@@ -6,7 +6,6 @@ from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
STATE_PERFORMANCE,
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, Platform, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -33,7 +32,6 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
|
||||
"""Representation of an ATAG water heater."""
|
||||
|
||||
_attr_operation_list = OPERATION_LIST
|
||||
_attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
@property
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.49.0"
|
||||
"habluetooth==3.48.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
key=str(BTHomeExtendedSensorDeviceClass.CHANNEL),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Conductivity (µS/cm)
|
||||
# Conductivity (μS/cm)
|
||||
(
|
||||
BTHomeSensorDeviceClass.CONDUCTIVITY,
|
||||
Units.CONDUCTIVITY,
|
||||
@@ -215,7 +215,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# PM10 (µg/m3)
|
||||
# PM10 (μg/m3)
|
||||
(
|
||||
BTHomeSensorDeviceClass.PM10,
|
||||
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
@@ -225,7 +225,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# PM2.5 (µg/m3)
|
||||
# PM2.5 (μg/m3)
|
||||
(
|
||||
BTHomeSensorDeviceClass.PM25,
|
||||
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
@@ -318,7 +318,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
key=str(BTHomeSensorDeviceClass.UV_INDEX),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Volatile organic Compounds (VOC) (µg/m3)
|
||||
# Volatile organic Compounds (VOC) (μg/m3)
|
||||
(
|
||||
BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/camera",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.8.0"]
|
||||
"requirements": ["PyTurboJPEG==1.7.5"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["numpy==2.2.6"]
|
||||
"requirements": ["numpy==2.2.2"]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ from .const import (
|
||||
|
||||
UNIT_PREFIXES = [
|
||||
selector.SelectOptionDict(value="n", label="n (nano)"),
|
||||
selector.SelectOptionDict(value="µ", label="µ (micro)"),
|
||||
selector.SelectOptionDict(value="μ", label="μ (micro)"),
|
||||
selector.SelectOptionDict(value="m", label="m (milli)"),
|
||||
selector.SelectOptionDict(value="k", label="k (kilo)"),
|
||||
selector.SelectOptionDict(value="M", label="M (mega)"),
|
||||
|
||||
@@ -61,7 +61,7 @@ ATTR_SOURCE_ID = "source"
|
||||
UNIT_PREFIXES = {
|
||||
None: 1,
|
||||
"n": 1e-9,
|
||||
"µ": 1e-6,
|
||||
"μ": 1e-6, # "μ" == "\u03bc"
|
||||
"m": 1e-3,
|
||||
"k": 1e3,
|
||||
"M": 1e6,
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
"""The eddystone_temperature component."""
|
||||
|
||||
DOMAIN = "eddystone_temperature"
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
@@ -23,18 +23,17 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_BT_DEVICE_ID = "bt_device_id"
|
||||
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
BEACON_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -59,21 +58,6 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Validate configuration, create devices and start monitoring thread."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Eddystone",
|
||||
},
|
||||
)
|
||||
|
||||
bt_device_id: int = config[CONF_BT_DEVICE_ID]
|
||||
|
||||
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
|
||||
|
||||
@@ -163,7 +163,7 @@ SENSORS: dict[str | None, SensorEntityDescription] = {
|
||||
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"µg/m³": SensorEntityDescription(
|
||||
"μg/m³": SensorEntityDescription(
|
||||
key="concentration|microgram_per_cubic_meter",
|
||||
translation_key="concentration",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==32.0.0",
|
||||
"aioesphomeapi==31.1.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
"bleak-esphome==2.15.1"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -71,11 +71,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
_attr_name = "DHW controller"
|
||||
_attr_icon = "mdi:thermometer-lines"
|
||||
_attr_operation_list = list(HA_STATE_TO_EVO)
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.AWAY_MODE
|
||||
| WaterHeaterEntityFeature.ON_OFF
|
||||
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
_evo_device: evo.HotWater
|
||||
@@ -96,6 +91,9 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
self._attr_precision = (
|
||||
PRECISION_TENTHS if coordinator.client_v1 else PRECISION_WHOLE
|
||||
)
|
||||
self._attr_supported_features = (
|
||||
WaterHeaterEntityFeature.AWAY_MODE | WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str | None:
|
||||
|
||||
@@ -84,7 +84,6 @@ async def async_setup_entry(
|
||||
name=f"Freebox {sensor_name}",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
for sensor_name in router.sensors_temperature
|
||||
|
||||
@@ -105,7 +105,7 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [
|
||||
FytaSensorEntityDescription(
|
||||
key="light",
|
||||
translation_key="light",
|
||||
native_unit_of_measurement="μmol/s⋅m²",
|
||||
native_unit_of_measurement="μmol/s⋅m²", # "μ" == "\u03bc"
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda plant: plant.light,
|
||||
),
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["go2rtc-client==0.2.1"],
|
||||
"requirements": ["go2rtc-client==0.1.3b0"],
|
||||
"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==10.0.0"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"]
|
||||
}
|
||||
|
||||
@@ -24,11 +24,9 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Google Mail integration."""
|
||||
"""Set up the Google Mail platform."""
|
||||
hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -54,6 +52,8 @@ 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,26 +7,17 @@ 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, config_validation as cv
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
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"]
|
||||
|
||||
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
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -57,6 +48,8 @@ async def async_setup_entry(
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
async_register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -152,10 +152,11 @@ def async_register_services(hass: HomeAssistant) -> None:
|
||||
}
|
||||
return None
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
"""The hddtemp component."""
|
||||
|
||||
DOMAIN = "hddtemp"
|
||||
|
||||
@@ -22,14 +22,11 @@ from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.core import 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"
|
||||
@@ -59,21 +56,6 @@ 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,9 +73,7 @@ async def async_setup_entry(
|
||||
class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
|
||||
"""Hive Water Heater Device."""
|
||||
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
_attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_operation_list = SUPPORT_WATER_HEATER
|
||||
|
||||
|
||||
@@ -83,54 +83,3 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the reconfigure flow."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input:
|
||||
self.homee = Homee(
|
||||
user_input[CONF_HOST],
|
||||
reconfigure_entry.data[CONF_USERNAME],
|
||||
reconfigure_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
await self.homee.wait_until_connected()
|
||||
self.homee.disconnect()
|
||||
await self.homee.wait_until_disconnected()
|
||||
|
||||
await self.async_set_unique_id(self.homee.settings.uid)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_hub")
|
||||
|
||||
_LOGGER.debug("Updated homee entry with ID %s", self.homee.settings.uid)
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(), data_updates=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_HOST, default=reconfigure_entry.data[CONF_HOST]
|
||||
): str
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"name": reconfigure_entry.runtime_data.settings.uid
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
"config": {
|
||||
"flow_title": "homee {name} ({host})",
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"wrong_hub": "Address belongs to a different homee."
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -24,16 +22,6 @@
|
||||
"username": "The username for your homee.",
|
||||
"password": "The password for your homee."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure homee {name}",
|
||||
"description": "Reconfigure the IP address of your homee.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of your homee."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -291,7 +291,7 @@ class NitrogenDioxideSensor(AirQualitySensor):
|
||||
class VolatileOrganicCompoundsSensor(AirQualitySensor):
|
||||
"""Generate a VolatileOrganicCompoundsSensor accessory as VOCs sensor.
|
||||
|
||||
Sensor entity must return VOC in µg/m3.
|
||||
Sensor entity must return VOC in μg/m3.
|
||||
"""
|
||||
|
||||
def create_services(self) -> None:
|
||||
|
||||
@@ -480,7 +480,7 @@ def temperature_to_states(temperature: float, unit: str) -> float:
|
||||
|
||||
|
||||
def density_to_air_quality(density: float) -> int:
|
||||
"""Map PM2.5 µg/m3 density to HomeKit AirQuality level."""
|
||||
"""Map PM2.5 μg/m3 density to HomeKit AirQuality level."""
|
||||
if density <= 9: # US AQI 0-50 (HomeKit: Excellent)
|
||||
return 1
|
||||
if density <= 35.4: # US AQI 51-100 (HomeKit: Good)
|
||||
@@ -493,7 +493,7 @@ def density_to_air_quality(density: float) -> int:
|
||||
|
||||
|
||||
def density_to_air_quality_pm10(density: float) -> int:
|
||||
"""Map PM10 µg/m3 density to HomeKit AirQuality level."""
|
||||
"""Map PM10 μg/m3 density to HomeKit AirQuality level."""
|
||||
if density <= 54: # US AQI 0-50 (HomeKit: Excellent)
|
||||
return 1
|
||||
if density <= 154: # US AQI 51-100 (HomeKit: Good)
|
||||
@@ -506,7 +506,7 @@ def density_to_air_quality_pm10(density: float) -> int:
|
||||
|
||||
|
||||
def density_to_air_quality_nitrogen_dioxide(density: float) -> int:
|
||||
"""Map nitrogen dioxide µg/m3 to HomeKit AirQuality level."""
|
||||
"""Map nitrogen dioxide μg/m3 to HomeKit AirQuality level."""
|
||||
if density <= 30:
|
||||
return 1
|
||||
if density <= 60:
|
||||
@@ -519,7 +519,7 @@ def density_to_air_quality_nitrogen_dioxide(density: float) -> int:
|
||||
|
||||
|
||||
def density_to_air_quality_voc(density: float) -> int:
|
||||
"""Map VOCs µg/m3 to HomeKit AirQuality level.
|
||||
"""Map VOCs μg/m3 to HomeKit AirQuality level.
|
||||
|
||||
The VOC mappings use the IAQ guidelines for Europe released by the WHO (World Health Organization).
|
||||
Referenced from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.15"],
|
||||
"requirements": ["aiohomekit==3.2.14"],
|
||||
"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
|
||||
from .services import async_setup_services, async_unload_services
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -63,8 +63,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -85,6 +83,7 @@ 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
|
||||
@@ -116,6 +115,8 @@ async def async_unload_entry(
|
||||
assert hap.reset_connection_listener is not None
|
||||
hap.reset_connection_listener()
|
||||
|
||||
await async_unload_services(hass)
|
||||
|
||||
return await hap.async_reset()
|
||||
|
||||
|
||||
|
||||
@@ -123,29 +123,32 @@ SCHEMA_SET_HOME_COOLING_MODE = vol.Schema(
|
||||
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the HomematicIP Cloud services."""
|
||||
|
||||
if hass.services.async_services_for_domain(DOMAIN):
|
||||
return
|
||||
|
||||
@verify_domain_control(hass, DOMAIN)
|
||||
async def async_call_hmipc_service(service: ServiceCall) -> None:
|
||||
"""Call correct HomematicIP Cloud service."""
|
||||
service_name = service.service
|
||||
|
||||
if service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION:
|
||||
await _async_activate_eco_mode_with_duration(service)
|
||||
await _async_activate_eco_mode_with_duration(hass, service)
|
||||
elif service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD:
|
||||
await _async_activate_eco_mode_with_period(service)
|
||||
await _async_activate_eco_mode_with_period(hass, service)
|
||||
elif service_name == SERVICE_ACTIVATE_VACATION:
|
||||
await _async_activate_vacation(service)
|
||||
await _async_activate_vacation(hass, service)
|
||||
elif service_name == SERVICE_DEACTIVATE_ECO_MODE:
|
||||
await _async_deactivate_eco_mode(service)
|
||||
await _async_deactivate_eco_mode(hass, service)
|
||||
elif service_name == SERVICE_DEACTIVATE_VACATION:
|
||||
await _async_deactivate_vacation(service)
|
||||
await _async_deactivate_vacation(hass, service)
|
||||
elif service_name == SERVICE_DUMP_HAP_CONFIG:
|
||||
await _async_dump_hap_config(service)
|
||||
await _async_dump_hap_config(hass, service)
|
||||
elif service_name == SERVICE_RESET_ENERGY_COUNTER:
|
||||
await _async_reset_energy_counter(service)
|
||||
await _async_reset_energy_counter(hass, service)
|
||||
elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE:
|
||||
await _set_active_climate_profile(service)
|
||||
await _set_active_climate_profile(hass, service)
|
||||
elif service_name == SERVICE_SET_HOME_COOLING_MODE:
|
||||
await _async_set_home_cooling_mode(service)
|
||||
await _async_set_home_cooling_mode(hass, service)
|
||||
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
@@ -214,75 +217,90 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _async_activate_eco_mode_with_duration(service: ServiceCall) -> None:
|
||||
async def async_unload_services(hass: HomeAssistant):
|
||||
"""Unload HomematicIP Cloud services."""
|
||||
if hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
return
|
||||
|
||||
for hmipc_service in HMIPC_SERVICES:
|
||||
hass.services.async_remove(domain=DOMAIN, service=hmipc_service)
|
||||
|
||||
|
||||
async def _async_activate_eco_mode_with_duration(
|
||||
hass: HomeAssistant, service: ServiceCall
|
||||
) -> None:
|
||||
"""Service to activate eco mode with duration."""
|
||||
duration = service.data[ATTR_DURATION]
|
||||
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
if home := _get_home(hass, hapid):
|
||||
await home.activate_absence_with_duration_async(duration)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.activate_absence_with_duration_async(duration)
|
||||
|
||||
|
||||
async def _async_activate_eco_mode_with_period(service: ServiceCall) -> None:
|
||||
async def _async_activate_eco_mode_with_period(
|
||||
hass: HomeAssistant, service: ServiceCall
|
||||
) -> None:
|
||||
"""Service to activate eco mode with period."""
|
||||
endtime = service.data[ATTR_ENDTIME]
|
||||
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
if home := _get_home(hass, hapid):
|
||||
await home.activate_absence_with_period_async(endtime)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.activate_absence_with_period_async(endtime)
|
||||
|
||||
|
||||
async def _async_activate_vacation(service: ServiceCall) -> None:
|
||||
async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
"""Service to activate vacation."""
|
||||
endtime = service.data[ATTR_ENDTIME]
|
||||
temperature = service.data[ATTR_TEMPERATURE]
|
||||
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
if home := _get_home(hass, hapid):
|
||||
await home.activate_vacation_async(endtime, temperature)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.activate_vacation_async(endtime, temperature)
|
||||
|
||||
|
||||
async def _async_deactivate_eco_mode(service: ServiceCall) -> None:
|
||||
async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
"""Service to deactivate eco mode."""
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
if home := _get_home(hass, hapid):
|
||||
await home.deactivate_absence_async()
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.deactivate_absence_async()
|
||||
|
||||
|
||||
async def _async_deactivate_vacation(service: ServiceCall) -> None:
|
||||
async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
"""Service to deactivate vacation."""
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
if home := _get_home(hass, hapid):
|
||||
await home.deactivate_vacation_async()
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.deactivate_vacation_async()
|
||||
|
||||
|
||||
async def _set_active_climate_profile(service: ServiceCall) -> None:
|
||||
async def _set_active_climate_profile(
|
||||
hass: HomeAssistant, service: ServiceCall
|
||||
) -> None:
|
||||
"""Service to set the active climate profile."""
|
||||
entity_id_list = service.data[ATTR_ENTITY_ID]
|
||||
climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1
|
||||
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if entity_id_list != "all":
|
||||
for entity_id in entity_id_list:
|
||||
group = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
|
||||
@@ -294,16 +312,16 @@ async def _set_active_climate_profile(service: ServiceCall) -> None:
|
||||
await group.set_active_profile_async(climate_profile_index)
|
||||
|
||||
|
||||
async def _async_dump_hap_config(service: ServiceCall) -> None:
|
||||
async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
"""Service to dump the configuration of a Homematic IP Access Point."""
|
||||
config_path: str = (
|
||||
service.data.get(ATTR_CONFIG_OUTPUT_PATH) or service.hass.config.config_dir
|
||||
service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir
|
||||
)
|
||||
config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX]
|
||||
anonymize = service.data[ATTR_ANONYMIZE]
|
||||
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
hap_sgtin = entry.unique_id
|
||||
assert hap_sgtin is not None
|
||||
|
||||
@@ -320,12 +338,12 @@ async def _async_dump_hap_config(service: ServiceCall) -> None:
|
||||
config_file.write_text(json_state, encoding="utf8")
|
||||
|
||||
|
||||
async def _async_reset_energy_counter(service: ServiceCall):
|
||||
async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall):
|
||||
"""Service to reset the energy counter."""
|
||||
entity_id_list = service.data[ATTR_ENTITY_ID]
|
||||
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if entity_id_list != "all":
|
||||
for entity_id in entity_id_list:
|
||||
device = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
|
||||
@@ -337,16 +355,16 @@ async def _async_reset_energy_counter(service: ServiceCall):
|
||||
await device.reset_energy_counter_async()
|
||||
|
||||
|
||||
async def _async_set_home_cooling_mode(service: ServiceCall):
|
||||
async def _async_set_home_cooling_mode(hass: HomeAssistant, service: ServiceCall):
|
||||
"""Service to set the cooling mode."""
|
||||
cooling = service.data[ATTR_COOLING]
|
||||
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
if home := _get_home(hass, hapid):
|
||||
await home.set_cooling_async(cooling)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.set_cooling_async(cooling)
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["homewizard_energy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-homewizard-energy==8.3.3"],
|
||||
"requirements": ["python-homewizard-energy==v8.3.2"],
|
||||
"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( # type: ignore[no-untyped-call]
|
||||
host: aiohttp_cors.ResourceOptions(
|
||||
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
|
||||
)
|
||||
for host in origins
|
||||
},
|
||||
)
|
||||
|
||||
cors_added: set[str] = set()
|
||||
cors_added = 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) # type: ignore[arg-type]
|
||||
cors.add(route, config)
|
||||
cors_added.add(path_str)
|
||||
|
||||
app[KEY_ALLOW_ALL_CORS] = lambda route: _allow_cors(
|
||||
route,
|
||||
{
|
||||
"*": aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
|
||||
"*": aiohttp_cors.ResourceOptions(
|
||||
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
|
||||
)
|
||||
},
|
||||
|
||||
@@ -5,24 +5,13 @@ 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 config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .bridge import HueBridge, HueConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SERVICE_HUE_ACTIVATE_SCENE
|
||||
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."""
|
||||
@@ -34,6 +23,9 @@ 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
|
||||
@@ -114,4 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await entry.runtime_data.async_reset()
|
||||
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
|
||||
|
||||
@@ -59,20 +59,21 @@ def async_register_services(hass: HomeAssistant) -> None:
|
||||
group_name,
|
||||
)
|
||||
|
||||
# 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,
|
||||
}
|
||||
),
|
||||
)
|
||||
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,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def hue_activate_scene_v1(
|
||||
|
||||
@@ -6,32 +6,19 @@ 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."""
|
||||
@@ -64,6 +51,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bo
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioimmich==0.9.0"]
|
||||
"requirements": ["aioimmich==0.7.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.album_name,
|
||||
title=album.name,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.album_thumbnail_asset_id}/thumbnail/image/jpg",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg",
|
||||
)
|
||||
for album in albums
|
||||
]
|
||||
@@ -153,40 +153,47 @@ class ImmichMediaSource(MediaSource):
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
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
|
||||
|
||||
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}",
|
||||
)
|
||||
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.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",
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
"""Utilities used by insteon component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from pyinsteon import devices
|
||||
from pyinsteon.address import Address
|
||||
from pyinsteon.managers.link_manager import (
|
||||
async_enter_linking_mode,
|
||||
async_enter_unlinking_mode,
|
||||
)
|
||||
from pyinsteon.managers.scene_manager import (
|
||||
async_trigger_scene_off,
|
||||
async_trigger_scene_on,
|
||||
)
|
||||
from pyinsteon.managers.x10_manager import (
|
||||
async_x10_all_lights_off,
|
||||
async_x10_all_lights_on,
|
||||
async_x10_all_units_off,
|
||||
)
|
||||
from pyinsteon.x10_address import create as create_x10_address
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
ENTITY_MATCH_ALL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
dispatcher_send,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_CAT,
|
||||
CONF_DIM_STEPS,
|
||||
CONF_HOUSECODE,
|
||||
CONF_SUBCAT,
|
||||
CONF_UNITCODE,
|
||||
DOMAIN,
|
||||
SIGNAL_ADD_DEFAULT_LINKS,
|
||||
SIGNAL_ADD_DEVICE_OVERRIDE,
|
||||
SIGNAL_ADD_X10_DEVICE,
|
||||
SIGNAL_LOAD_ALDB,
|
||||
SIGNAL_PRINT_ALDB,
|
||||
SIGNAL_REMOVE_DEVICE_OVERRIDE,
|
||||
SIGNAL_REMOVE_ENTITY,
|
||||
SIGNAL_REMOVE_HA_DEVICE,
|
||||
SIGNAL_REMOVE_INSTEON_DEVICE,
|
||||
SIGNAL_REMOVE_X10_DEVICE,
|
||||
SIGNAL_SAVE_DEVICES,
|
||||
SRV_ADD_ALL_LINK,
|
||||
SRV_ADD_DEFAULT_LINKS,
|
||||
SRV_ALL_LINK_GROUP,
|
||||
SRV_ALL_LINK_MODE,
|
||||
SRV_CONTROLLER,
|
||||
SRV_DEL_ALL_LINK,
|
||||
SRV_HOUSECODE,
|
||||
SRV_LOAD_ALDB,
|
||||
SRV_LOAD_DB_RELOAD,
|
||||
SRV_PRINT_ALDB,
|
||||
SRV_PRINT_IM_ALDB,
|
||||
SRV_SCENE_OFF,
|
||||
SRV_SCENE_ON,
|
||||
SRV_X10_ALL_LIGHTS_OFF,
|
||||
SRV_X10_ALL_LIGHTS_ON,
|
||||
SRV_X10_ALL_UNITS_OFF,
|
||||
)
|
||||
from .schemas import (
|
||||
ADD_ALL_LINK_SCHEMA,
|
||||
ADD_DEFAULT_LINKS_SCHEMA,
|
||||
DEL_ALL_LINK_SCHEMA,
|
||||
LOAD_ALDB_SCHEMA,
|
||||
PRINT_ALDB_SCHEMA,
|
||||
TRIGGER_SCENE_SCHEMA,
|
||||
X10_HOUSECODE_SCHEMA,
|
||||
)
|
||||
from .utils import print_aldb_to_log
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_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,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -11,25 +12,90 @@ from pyinsteon.address import Address
|
||||
from pyinsteon.constants import ALDBStatus, DeviceAction
|
||||
from pyinsteon.device_types.device_base import Device
|
||||
from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT, Event
|
||||
from pyinsteon.managers.link_manager import (
|
||||
async_enter_linking_mode,
|
||||
async_enter_unlinking_mode,
|
||||
)
|
||||
from pyinsteon.managers.scene_manager import (
|
||||
async_trigger_scene_off,
|
||||
async_trigger_scene_on,
|
||||
)
|
||||
from pyinsteon.managers.x10_manager import (
|
||||
async_x10_all_lights_off,
|
||||
async_x10_all_lights_on,
|
||||
async_x10_all_units_off,
|
||||
)
|
||||
from pyinsteon.x10_address import create as create_x10_address
|
||||
from serial.tools import list_ports
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
ENTITY_MATCH_ALL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_CAT,
|
||||
CONF_DIM_STEPS,
|
||||
CONF_HOUSECODE,
|
||||
CONF_SUBCAT,
|
||||
CONF_UNITCODE,
|
||||
DOMAIN,
|
||||
EVENT_CONF_BUTTON,
|
||||
EVENT_GROUP_OFF,
|
||||
EVENT_GROUP_OFF_FAST,
|
||||
EVENT_GROUP_ON,
|
||||
EVENT_GROUP_ON_FAST,
|
||||
SIGNAL_ADD_DEFAULT_LINKS,
|
||||
SIGNAL_ADD_DEVICE_OVERRIDE,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ADD_X10_DEVICE,
|
||||
SIGNAL_LOAD_ALDB,
|
||||
SIGNAL_PRINT_ALDB,
|
||||
SIGNAL_REMOVE_DEVICE_OVERRIDE,
|
||||
SIGNAL_REMOVE_ENTITY,
|
||||
SIGNAL_REMOVE_HA_DEVICE,
|
||||
SIGNAL_REMOVE_INSTEON_DEVICE,
|
||||
SIGNAL_REMOVE_X10_DEVICE,
|
||||
SIGNAL_SAVE_DEVICES,
|
||||
SRV_ADD_ALL_LINK,
|
||||
SRV_ADD_DEFAULT_LINKS,
|
||||
SRV_ALL_LINK_GROUP,
|
||||
SRV_ALL_LINK_MODE,
|
||||
SRV_CONTROLLER,
|
||||
SRV_DEL_ALL_LINK,
|
||||
SRV_HOUSECODE,
|
||||
SRV_LOAD_ALDB,
|
||||
SRV_LOAD_DB_RELOAD,
|
||||
SRV_PRINT_ALDB,
|
||||
SRV_PRINT_IM_ALDB,
|
||||
SRV_SCENE_OFF,
|
||||
SRV_SCENE_ON,
|
||||
SRV_X10_ALL_LIGHTS_OFF,
|
||||
SRV_X10_ALL_LIGHTS_ON,
|
||||
SRV_X10_ALL_UNITS_OFF,
|
||||
)
|
||||
from .ipdb import get_device_platform_groups, get_device_platforms
|
||||
from .schemas import (
|
||||
ADD_ALL_LINK_SCHEMA,
|
||||
ADD_DEFAULT_LINKS_SCHEMA,
|
||||
DEL_ALL_LINK_SCHEMA,
|
||||
LOAD_ALDB_SCHEMA,
|
||||
PRINT_ALDB_SCHEMA,
|
||||
TRIGGER_SCENE_SCHEMA,
|
||||
X10_HOUSECODE_SCHEMA,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .entity import InsteonEntity
|
||||
@@ -88,7 +154,7 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None:
|
||||
_register_event(event, async_fire_insteon_event)
|
||||
|
||||
|
||||
def register_new_device_callback(hass: HomeAssistant) -> None:
|
||||
def register_new_device_callback(hass):
|
||||
"""Register callback for new Insteon device."""
|
||||
|
||||
@callback
|
||||
@@ -114,6 +180,212 @@ def register_new_device_callback(hass: HomeAssistant) -> None:
|
||||
devices.subscribe(async_new_insteon_device, force_strong_ref=True)
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_services(hass): # noqa: C901
|
||||
"""Register services used by insteon component."""
|
||||
|
||||
save_lock = asyncio.Lock()
|
||||
|
||||
async def async_srv_add_all_link(service: ServiceCall) -> None:
|
||||
"""Add an INSTEON All-Link between two devices."""
|
||||
group = service.data[SRV_ALL_LINK_GROUP]
|
||||
mode = service.data[SRV_ALL_LINK_MODE]
|
||||
link_mode = mode.lower() == SRV_CONTROLLER
|
||||
await async_enter_linking_mode(link_mode, group)
|
||||
|
||||
async def async_srv_del_all_link(service: ServiceCall) -> None:
|
||||
"""Delete an INSTEON All-Link between two devices."""
|
||||
group = service.data.get(SRV_ALL_LINK_GROUP)
|
||||
await async_enter_unlinking_mode(group)
|
||||
|
||||
async def async_srv_load_aldb(service: ServiceCall) -> None:
|
||||
"""Load the device All-Link database."""
|
||||
entity_id = service.data[CONF_ENTITY_ID]
|
||||
reload = service.data[SRV_LOAD_DB_RELOAD]
|
||||
if entity_id.lower() == ENTITY_MATCH_ALL:
|
||||
await async_srv_load_aldb_all(reload)
|
||||
else:
|
||||
signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
|
||||
async_dispatcher_send(hass, signal, reload)
|
||||
|
||||
async def async_srv_load_aldb_all(reload):
|
||||
"""Load the All-Link database for all devices."""
|
||||
# Cannot be done concurrently due to issues with the underlying protocol.
|
||||
for address in devices:
|
||||
device = devices[address]
|
||||
if device != devices.modem and device.cat != 0x03:
|
||||
await device.aldb.async_load(refresh=reload)
|
||||
await async_srv_save_devices()
|
||||
|
||||
async def async_srv_save_devices():
|
||||
"""Write the Insteon device configuration to file."""
|
||||
async with save_lock:
|
||||
_LOGGER.debug("Saving Insteon devices")
|
||||
await devices.async_save(hass.config.config_dir)
|
||||
|
||||
def print_aldb(service: ServiceCall) -> None:
|
||||
"""Print the All-Link Database for a device."""
|
||||
# For now this sends logs to the log file.
|
||||
# Future direction is to create an INSTEON control panel.
|
||||
entity_id = service.data[CONF_ENTITY_ID]
|
||||
signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
|
||||
dispatcher_send(hass, signal)
|
||||
|
||||
def print_im_aldb(service: ServiceCall) -> None:
|
||||
"""Print the All-Link Database for a device."""
|
||||
# For now this sends logs to the log file.
|
||||
# Future direction is to create an INSTEON control panel.
|
||||
print_aldb_to_log(devices.modem.aldb)
|
||||
|
||||
async def async_srv_x10_all_units_off(service: ServiceCall) -> None:
|
||||
"""Send the X10 All Units Off command."""
|
||||
housecode = service.data.get(SRV_HOUSECODE)
|
||||
await async_x10_all_units_off(housecode)
|
||||
|
||||
async def async_srv_x10_all_lights_off(service: ServiceCall) -> None:
|
||||
"""Send the X10 All Lights Off command."""
|
||||
housecode = service.data.get(SRV_HOUSECODE)
|
||||
await async_x10_all_lights_off(housecode)
|
||||
|
||||
async def async_srv_x10_all_lights_on(service: ServiceCall) -> None:
|
||||
"""Send the X10 All Lights On command."""
|
||||
housecode = service.data.get(SRV_HOUSECODE)
|
||||
await async_x10_all_lights_on(housecode)
|
||||
|
||||
async def async_srv_scene_on(service: ServiceCall) -> None:
|
||||
"""Trigger an INSTEON scene ON."""
|
||||
group = service.data.get(SRV_ALL_LINK_GROUP)
|
||||
await async_trigger_scene_on(group)
|
||||
|
||||
async def async_srv_scene_off(service: ServiceCall) -> None:
|
||||
"""Trigger an INSTEON scene ON."""
|
||||
group = service.data.get(SRV_ALL_LINK_GROUP)
|
||||
await async_trigger_scene_off(group)
|
||||
|
||||
@callback
|
||||
def async_add_default_links(service: ServiceCall) -> None:
|
||||
"""Add the default All-Link entries to a device."""
|
||||
entity_id = service.data[CONF_ENTITY_ID]
|
||||
signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}"
|
||||
async_dispatcher_send(hass, signal)
|
||||
|
||||
async def async_add_device_override(override):
|
||||
"""Remove an Insten device and associated entities."""
|
||||
address = Address(override[CONF_ADDRESS])
|
||||
await async_remove_ha_device(address)
|
||||
devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0)
|
||||
await async_srv_save_devices()
|
||||
|
||||
async def async_remove_device_override(address):
|
||||
"""Remove an Insten device and associated entities."""
|
||||
address = Address(address)
|
||||
await async_remove_ha_device(address)
|
||||
devices.set_id(address, None, None, None)
|
||||
await devices.async_identify_device(address)
|
||||
await async_srv_save_devices()
|
||||
|
||||
@callback
|
||||
def async_add_x10_device(x10_config):
|
||||
"""Add X10 device."""
|
||||
housecode = x10_config[CONF_HOUSECODE]
|
||||
unitcode = x10_config[CONF_UNITCODE]
|
||||
platform = x10_config[CONF_PLATFORM]
|
||||
steps = x10_config.get(CONF_DIM_STEPS, 22)
|
||||
x10_type = "on_off"
|
||||
if platform == "light":
|
||||
x10_type = "dimmable"
|
||||
elif platform == "binary_sensor":
|
||||
x10_type = "sensor"
|
||||
_LOGGER.debug(
|
||||
"Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type
|
||||
)
|
||||
# This must be run in the event loop
|
||||
devices.add_x10_device(housecode, unitcode, x10_type, steps)
|
||||
|
||||
async def async_remove_x10_device(housecode, unitcode):
|
||||
"""Remove an X10 device and associated entities."""
|
||||
address = create_x10_address(housecode, unitcode)
|
||||
devices.pop(address)
|
||||
await async_remove_ha_device(address)
|
||||
|
||||
async def async_remove_ha_device(address: Address, remove_all_refs: bool = False):
|
||||
"""Remove the device and all entities from hass."""
|
||||
signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}"
|
||||
async_dispatcher_send(hass, signal)
|
||||
dev_registry = dr.async_get(hass)
|
||||
device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))})
|
||||
if device:
|
||||
dev_registry.async_remove_device(device.id)
|
||||
|
||||
async def async_remove_insteon_device(
|
||||
address: Address, remove_all_refs: bool = False
|
||||
):
|
||||
"""Remove the underlying Insteon device from the network."""
|
||||
await devices.async_remove_device(
|
||||
address=address, force=False, remove_all_refs=remove_all_refs
|
||||
)
|
||||
await async_srv_save_devices()
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SRV_LOAD_ALDB, async_srv_load_aldb, schema=LOAD_ALDB_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
|
||||
)
|
||||
hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SRV_X10_ALL_UNITS_OFF,
|
||||
async_srv_x10_all_units_off,
|
||||
schema=X10_HOUSECODE_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SRV_X10_ALL_LIGHTS_OFF,
|
||||
async_srv_x10_all_lights_off,
|
||||
schema=X10_HOUSECODE_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SRV_X10_ALL_LIGHTS_ON,
|
||||
async_srv_x10_all_lights_on,
|
||||
schema=X10_HOUSECODE_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SRV_ADD_DEFAULT_LINKS,
|
||||
async_add_default_links,
|
||||
schema=ADD_DEFAULT_LINKS_SCHEMA,
|
||||
)
|
||||
async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_ADD_DEVICE_OVERRIDE, async_add_device_override
|
||||
)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, async_remove_device_override
|
||||
)
|
||||
async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device)
|
||||
async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device)
|
||||
async_dispatcher_connect(hass, SIGNAL_REMOVE_HA_DEVICE, async_remove_ha_device)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_REMOVE_INSTEON_DEVICE, async_remove_insteon_device
|
||||
)
|
||||
_LOGGER.debug("Insteon Services registered")
|
||||
|
||||
|
||||
def print_aldb_to_log(aldb):
|
||||
"""Print the All-Link Database to the log file."""
|
||||
logger = logging.getLogger(f"{__name__}.links")
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyiqvia"],
|
||||
"requirements": ["numpy==2.2.6", "pyiqvia==2022.04.0"]
|
||||
"requirements": ["numpy==2.2.2", "pyiqvia==2022.04.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyiskra"],
|
||||
"requirements": ["pyiskra==0.1.21"]
|
||||
"requirements": ["pyiskra==0.1.19"]
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ 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,
|
||||
@@ -47,7 +46,7 @@ from .const import (
|
||||
)
|
||||
from .helpers import _categorize_nodes, _categorize_programs
|
||||
from .models import IsyConfigEntry, IsyData
|
||||
from .services import async_setup_services
|
||||
from .services import async_setup_services, async_unload_services
|
||||
from .util import _async_cleanup_registry_entries
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@@ -56,14 +55,6 @@ 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
|
||||
@@ -176,6 +167,9 @@ 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
|
||||
|
||||
|
||||
@@ -227,6 +221,9 @@ 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,6 +137,10 @@ 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."""
|
||||
@@ -226,3 +230,18 @@ 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 .services import async_setup_services
|
||||
from .service 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: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
|
||||
@@ -87,9 +87,7 @@ def get_knx_module(hass: HomeAssistant) -> KNXModule:
|
||||
try:
|
||||
return hass.data[KNX_MODULE_KEY]
|
||||
except KeyError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="integration_not_loaded"
|
||||
) from err
|
||||
raise HomeAssistantError("KNX entry not loaded") from err
|
||||
|
||||
|
||||
SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema(
|
||||
@@ -168,11 +166,7 @@ async def service_exposure_register_modify(call: ServiceCall) -> None:
|
||||
removed_exposure = knx_module.service_exposures.pop(group_address)
|
||||
except KeyError as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_exposure_remove_not_found",
|
||||
translation_placeholders={
|
||||
"group_address": group_address,
|
||||
},
|
||||
f"Could not find exposure for '{group_address}' to remove."
|
||||
) from err
|
||||
|
||||
removed_exposure.async_remove()
|
||||
@@ -240,17 +234,13 @@ async def service_send_to_knx_bus(call: ServiceCall) -> None:
|
||||
transcoder = DPTBase.parse_transcoder(attr_type)
|
||||
if transcoder is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_send_invalid_type",
|
||||
translation_placeholders={"type": attr_type},
|
||||
f"Invalid type for knx.send service: {attr_type}"
|
||||
)
|
||||
try:
|
||||
payload = transcoder.to_knx(attr_payload)
|
||||
except ConversionError as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_send_invalid_payload",
|
||||
translation_placeholders={"error": str(err)},
|
||||
f"Invalid payload for knx.send service: {err}"
|
||||
) from err
|
||||
elif isinstance(attr_payload, int):
|
||||
payload = DPTBinary(attr_payload)
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal digits expected.",
|
||||
"invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.",
|
||||
"invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'",
|
||||
"invalid_ip_address": "Invalid IPv4 address.",
|
||||
"keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
|
||||
@@ -143,20 +143,6 @@
|
||||
"unsupported_tunnel_type": "Selected tunneling type not supported by gateway."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"integration_not_loaded": {
|
||||
"message": "KNX integration is not loaded."
|
||||
},
|
||||
"service_exposure_remove_not_found": {
|
||||
"message": "Could not find exposure for `{group_address}` to remove."
|
||||
},
|
||||
"service_send_invalid_payload": {
|
||||
"message": "Invalid payload for `knx.send` service. {error}"
|
||||
},
|
||||
"service_send_invalid_type": {
|
||||
"message": "Invalid type for `knx.send` service: {type}"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==9.2.5"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==9.2.5"]
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ class MatterLight(MatterEntity, LightEntity):
|
||||
|
||||
assert level_control is not None
|
||||
|
||||
level = round(
|
||||
level = round( # type: ignore[unreachable]
|
||||
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(
|
||||
LOGGER.debug( # type: ignore[unreachable]
|
||||
"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.9.2"]
|
||||
"requirements": ["pymodbus==3.8.3"]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components.number import (
|
||||
NumberMode,
|
||||
RestoreNumber,
|
||||
)
|
||||
from homeassistant.components.sensor.recorder import EQUIVALENT_UNITS
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
@@ -70,6 +71,13 @@ MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset(
|
||||
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
"""Validate that the configuration is valid, throws if it isn't."""
|
||||
if (
|
||||
CONF_UNIT_OF_MEASUREMENT in config
|
||||
and (unit_of_measurement := config[CONF_UNIT_OF_MEASUREMENT])
|
||||
in EQUIVALENT_UNITS
|
||||
):
|
||||
config[CONF_UNIT_OF_MEASUREMENT] = EQUIVALENT_UNITS[unit_of_measurement]
|
||||
|
||||
if config[CONF_MIN] > config[CONF_MAX]:
|
||||
raise vol.Invalid(f"{CONF_MAX} must be >= {CONF_MIN}")
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.components.sensor import (
|
||||
SensorExtraStoredData,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sensor.recorder import EQUIVALENT_UNITS
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
@@ -129,9 +130,14 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||
f"together with state class '{state_class}'"
|
||||
)
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
|
||||
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
) is None:
|
||||
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is None:
|
||||
return config
|
||||
|
||||
config[CONF_UNIT_OF_MEASUREMENT] = EQUIVALENT_UNITS.get(
|
||||
unit_of_measurement, unit_of_measurement
|
||||
)
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is None:
|
||||
return config
|
||||
|
||||
if (
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyatmo"],
|
||||
"requirements": ["pyatmo==9.2.1"]
|
||||
"requirements": ["pyatmo==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["py_nextbus"],
|
||||
"requirements": ["py-nextbusnext==2.2.0"]
|
||||
"requirements": ["py-nextbusnext==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ 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,
|
||||
@@ -20,6 +22,7 @@ from .const import (
|
||||
CONF_EXCLUDE_VIAS,
|
||||
CONF_SHOW_ON_MAP,
|
||||
CONF_STATION_FROM,
|
||||
CONF_STATION_LIVE,
|
||||
CONF_STATION_TO,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -112,6 +115,68 @@ 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,19 +8,30 @@ from typing import Any
|
||||
|
||||
from pyrail import iRail
|
||||
from pyrail.models import ConnectionDetails, LiveboardDeparture, StationDetails
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_SHOW_ON_MAP,
|
||||
UnitOfTime,
|
||||
)
|
||||
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.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
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.util import dt as dt_util
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
@@ -36,9 +47,22 @@ 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."""
|
||||
@@ -61,6 +85,71 @@ 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,
|
||||
@@ -247,6 +336,7 @@ 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,
|
||||
@@ -256,13 +346,14 @@ class NMBSSensor(SensorEntity):
|
||||
"vehicle_id": self._attrs.departure.vehicle,
|
||||
}
|
||||
|
||||
attrs["canceled"] = self._attrs.departure.canceled
|
||||
if attrs["canceled"]:
|
||||
attrs["departure"] = None
|
||||
attrs["departure_minutes"] = None
|
||||
else:
|
||||
if not canceled:
|
||||
attrs["departure"] = f"In {departure} minutes"
|
||||
attrs["departure_minutes"] = departure
|
||||
attrs["canceled"] = False
|
||||
else:
|
||||
attrs["departure"] = None
|
||||
attrs["departure_minutes"] = None
|
||||
attrs["canceled"] = True
|
||||
|
||||
if self._show_on_map and self.station_coordinates:
|
||||
attrs[ATTR_LATITUDE] = self.station_coordinates[0]
|
||||
@@ -278,8 +369,9 @@ class NMBSSensor(SensorEntity):
|
||||
via.timebetween
|
||||
) + get_delay_in_minutes(via.departure.delay)
|
||||
|
||||
attrs["delay"] = f"{delay} minutes"
|
||||
attrs["delay_minutes"] = delay
|
||||
if delay > 0:
|
||||
attrs["delay"] = f"{delay} minutes"
|
||||
attrs["delay_minutes"] = delay
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
@@ -25,5 +25,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ from homeassistant.loader import async_suggest_report_issue
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
AMBIGUOUS_UNITS,
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_STEP,
|
||||
@@ -367,6 +368,15 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
return None
|
||||
|
||||
@final
|
||||
@property
|
||||
def __native_unit_of_measurement_compat(self) -> str | None:
|
||||
"""Process ambiguous units."""
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
return AMBIGUOUS_UNITS.get(
|
||||
native_unit_of_measurement, native_unit_of_measurement
|
||||
)
|
||||
|
||||
@property
|
||||
@final
|
||||
def unit_of_measurement(self) -> str | None:
|
||||
@@ -374,7 +384,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if self._number_option_unit_of_measurement:
|
||||
return self._number_option_unit_of_measurement
|
||||
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||
# device_class is checked after native_unit_of_measurement since most
|
||||
# of the time we can avoid the device_class check
|
||||
if (
|
||||
@@ -441,7 +451,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if device_class not in UNIT_CONVERTERS:
|
||||
return value
|
||||
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||
unit_of_measurement = self.unit_of_measurement
|
||||
if native_unit_of_measurement != unit_of_measurement:
|
||||
if TYPE_CHECKING:
|
||||
@@ -470,7 +480,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if value is None or (device_class := self.device_class) not in UNIT_CONVERTERS:
|
||||
return value
|
||||
|
||||
native_unit_of_measurement = self.native_unit_of_measurement
|
||||
native_unit_of_measurement = self.__native_unit_of_measurement_compat
|
||||
unit_of_measurement = self.unit_of_measurement
|
||||
if native_unit_of_measurement != unit_of_measurement:
|
||||
if TYPE_CHECKING:
|
||||
@@ -493,7 +503,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
(number_options := self.registry_entry.options.get(DOMAIN))
|
||||
and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT))
|
||||
and (device_class := self.device_class) in UNIT_CONVERTERS
|
||||
and self.native_unit_of_measurement
|
||||
and self.__native_unit_of_measurement_compat
|
||||
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
):
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Final
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
@@ -130,7 +131,7 @@ class NumberDeviceClass(StrEnum):
|
||||
CONDUCTIVITY = "conductivity"
|
||||
"""Conductivity.
|
||||
|
||||
Unit of measurement: `S/cm`, `mS/cm`, `µS/cm`
|
||||
Unit of measurement: `S/cm`, `mS/cm`, `μS/cm`
|
||||
"""
|
||||
|
||||
CURRENT = "current"
|
||||
@@ -162,7 +163,7 @@ class NumberDeviceClass(StrEnum):
|
||||
DURATION = "duration"
|
||||
"""Fixed duration.
|
||||
|
||||
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `µs`
|
||||
Unit of measurement: `d`, `h`, `min`, `s`, `ms`, `μs`
|
||||
"""
|
||||
|
||||
ENERGY = "energy"
|
||||
@@ -240,25 +241,25 @@ class NumberDeviceClass(StrEnum):
|
||||
NITROGEN_DIOXIDE = "nitrogen_dioxide"
|
||||
"""Amount of NO2.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
Unit of measurement: `μg/m³`
|
||||
"""
|
||||
|
||||
NITROGEN_MONOXIDE = "nitrogen_monoxide"
|
||||
"""Amount of NO.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
Unit of measurement: `μg/m³`
|
||||
"""
|
||||
|
||||
NITROUS_OXIDE = "nitrous_oxide"
|
||||
"""Amount of N2O.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
Unit of measurement: `μg/m³`
|
||||
"""
|
||||
|
||||
OZONE = "ozone"
|
||||
"""Amount of O3.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
Unit of measurement: `μg/m³`
|
||||
"""
|
||||
|
||||
PH = "ph"
|
||||
@@ -270,19 +271,19 @@ class NumberDeviceClass(StrEnum):
|
||||
PM1 = "pm1"
|
||||
"""Particulate matter <= 1 μm.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
Unit of measurement: `μg/m³`
|
||||
"""
|
||||
|
||||
PM10 = "pm10"
|
||||
"""Particulate matter <= 10 μm.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
Unit of measurement: `μg/m³`
|
||||
"""
|
||||
|
||||
PM25 = "pm25"
|
||||
"""Particulate matter <= 2.5 μm.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
Unit of measurement: `μg/m³`
|
||||
"""
|
||||
|
||||
POWER_FACTOR = "power_factor"
|
||||
@@ -359,7 +360,7 @@ class NumberDeviceClass(StrEnum):
|
||||
SULPHUR_DIOXIDE = "sulphur_dioxide"
|
||||
"""Amount of SO2.
|
||||
|
||||
Unit of measurement: `µg/m³`
|
||||
Unit of measurement: `μg/m³`
|
||||
"""
|
||||
|
||||
TEMPERATURE = "temperature"
|
||||
@@ -371,7 +372,7 @@ class NumberDeviceClass(StrEnum):
|
||||
VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds"
|
||||
"""Amount of VOC.
|
||||
|
||||
Unit of measurement: `µg/m³`, `mg/m³`
|
||||
Unit of measurement: `μg/m³`, `mg/m³`
|
||||
"""
|
||||
|
||||
VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts"
|
||||
@@ -383,7 +384,7 @@ class NumberDeviceClass(StrEnum):
|
||||
VOLTAGE = "voltage"
|
||||
"""Voltage.
|
||||
|
||||
Unit of measurement: `V`, `mV`, `µV`, `kV`, `MV`
|
||||
Unit of measurement: `V`, `mV`, `μV`, `kV`, `MV`
|
||||
"""
|
||||
|
||||
VOLUME = "volume"
|
||||
@@ -430,7 +431,7 @@ class NumberDeviceClass(StrEnum):
|
||||
Weight is used instead of mass to fit with every day language.
|
||||
|
||||
Unit of measurement: `MASS_*` units
|
||||
- SI / metric: `µg`, `mg`, `g`, `kg`
|
||||
- SI / metric: `μg`, `mg`, `g`, `kg`
|
||||
- USCS / imperial: `oz`, `lb`
|
||||
"""
|
||||
|
||||
@@ -546,3 +547,14 @@ UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = {
|
||||
NumberDeviceClass.TEMPERATURE: TemperatureConverter,
|
||||
NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter,
|
||||
}
|
||||
|
||||
AMBIGUOUS_UNITS: dict[str | None, str] = {
|
||||
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
|
||||
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
|
||||
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
|
||||
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
|
||||
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
|
||||
"\u00b5g": UnitOfMass.MICROGRAMS,
|
||||
"\u00b5s": UnitOfTime.MICROSECONDS,
|
||||
}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
"""The NZBGet integration."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DATA_COORDINATOR, DATA_UNDO_UPDATE_LISTENER, DOMAIN
|
||||
from .const import (
|
||||
ATTR_SPEED,
|
||||
DATA_COORDINATOR,
|
||||
DATA_UNDO_UPDATE_LISTENER,
|
||||
DEFAULT_SPEED_LIMIT,
|
||||
DOMAIN,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RESUME,
|
||||
SERVICE_SET_SPEED,
|
||||
)
|
||||
from .coordinator import NZBGetDataUpdateCoordinator
|
||||
from .services import async_register_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up NZBGet integration."""
|
||||
|
||||
async_register_services(hass)
|
||||
|
||||
return True
|
||||
SPEED_LIMIT_SCHEMA = vol.Schema(
|
||||
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -39,6 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
_async_register_services(hass, coordinator)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -53,6 +60,31 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return unload_ok
|
||||
|
||||
|
||||
def _async_register_services(
|
||||
hass: HomeAssistant,
|
||||
coordinator: NZBGetDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Register integration-level services."""
|
||||
|
||||
def pause(call: ServiceCall) -> None:
|
||||
"""Service call to pause downloads in NZBGet."""
|
||||
coordinator.nzbget.pausedownload()
|
||||
|
||||
def resume(call: ServiceCall) -> None:
|
||||
"""Service call to resume downloads in NZBGet."""
|
||||
coordinator.nzbget.resumedownload()
|
||||
|
||||
def set_speed(call: ServiceCall) -> None:
|
||||
"""Service call to rate limit speeds in NZBGet."""
|
||||
coordinator.nzbget.rate(call.data[ATTR_SPEED])
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_PAUSE, pause, schema=vol.Schema({}))
|
||||
hass.services.async_register(DOMAIN, SERVICE_RESUME, resume, schema=vol.Schema({}))
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SPEED, set_speed, schema=SPEED_LIMIT_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
"""The NZBGet integration."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
ATTR_SPEED,
|
||||
DATA_COORDINATOR,
|
||||
DEFAULT_SPEED_LIMIT,
|
||||
DOMAIN,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RESUME,
|
||||
SERVICE_SET_SPEED,
|
||||
)
|
||||
from .coordinator import NZBGetDataUpdateCoordinator
|
||||
|
||||
SPEED_LIMIT_SCHEMA = vol.Schema(
|
||||
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int}
|
||||
)
|
||||
|
||||
|
||||
def _get_coordinator(call: ServiceCall) -> NZBGetDataUpdateCoordinator:
|
||||
"""Service call to pause downloads in NZBGet."""
|
||||
entries = call.hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
if not entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_config_entry",
|
||||
)
|
||||
return call.hass.data[DOMAIN][entries[0].entry_id][DATA_COORDINATOR]
|
||||
|
||||
|
||||
def pause(call: ServiceCall) -> None:
|
||||
"""Service call to pause downloads in NZBGet."""
|
||||
_get_coordinator(call).nzbget.pausedownload()
|
||||
|
||||
|
||||
def resume(call: ServiceCall) -> None:
|
||||
"""Service call to resume downloads in NZBGet."""
|
||||
_get_coordinator(call).nzbget.resumedownload()
|
||||
|
||||
|
||||
def set_speed(call: ServiceCall) -> None:
|
||||
"""Service call to rate limit speeds in NZBGet."""
|
||||
_get_coordinator(call).nzbget.rate(call.data[ATTR_SPEED])
|
||||
|
||||
|
||||
def async_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,11 +64,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_config_entry": {
|
||||
"message": "Config entry not found or not loaded!"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"pause": {
|
||||
"name": "[%key:common::action::pause%]",
|
||||
|
||||
@@ -21,7 +21,6 @@ from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_NUM_CTX,
|
||||
CONF_PROMPT,
|
||||
CONF_THINK,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -34,7 +33,6 @@ __all__ = [
|
||||
"CONF_MODEL",
|
||||
"CONF_NUM_CTX",
|
||||
"CONF_PROMPT",
|
||||
"CONF_THINK",
|
||||
"CONF_URL",
|
||||
"DOMAIN",
|
||||
]
|
||||
|
||||
@@ -22,7 +22,6 @@ 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,
|
||||
@@ -42,12 +41,10 @@ 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,
|
||||
@@ -283,12 +280,6 @@ 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,7 +4,6 @@ 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
|
||||
@@ -16,7 +15,6 @@ 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,7 +24,6 @@ from .const import (
|
||||
CONF_MODEL,
|
||||
CONF_NUM_CTX,
|
||||
CONF_PROMPT,
|
||||
CONF_THINK,
|
||||
DEFAULT_KEEP_ALIVE,
|
||||
DEFAULT_MAX_HISTORY,
|
||||
DEFAULT_NUM_CTX,
|
||||
@@ -257,7 +256,6 @@ 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.5.1"]
|
||||
"requirements": ["ollama==0.4.7"]
|
||||
}
|
||||
|
||||
@@ -30,14 +30,12 @@
|
||||
"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",
|
||||
"think": "Think before responding"
|
||||
"keep_alive": "Keep alive"
|
||||
},
|
||||
"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.",
|
||||
"think": "If enabled, the LLM will think before responding. This can improve response quality but may increase latency."
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +121,11 @@ def async_register_services(hass: HomeAssistant) -> None:
|
||||
return {"files": [asdict(item_result) for item_result in upload_results]}
|
||||
return None
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
|
||||
"requirements": ["pyoverkiz==1.17.2"],
|
||||
"requirements": ["pyoverkiz==1.17.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
||||
@@ -5,25 +5,14 @@ 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(
|
||||
@@ -48,6 +37,9 @@ 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.3.1"]
|
||||
"requirements": ["python-picnic-api2==1.2.4"]
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ 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)
|
||||
|
||||
@@ -366,6 +366,7 @@ class PrometheusMetrics:
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_metric_name(metric: str) -> str:
|
||||
metric.replace("\u03bc", "\u00b5")
|
||||
return "".join(
|
||||
[c if c in ALLOWED_METRIC_CHARS else f"u{hex(ord(c))}" for c in metric]
|
||||
)
|
||||
@@ -747,6 +748,9 @@ class PrometheusMetrics:
|
||||
PERCENTAGE: "percent",
|
||||
}
|
||||
default = unit.replace("/", "_per_")
|
||||
# Unit conversion for CONCENTRATION_MICROGRAMS_PER_CUBIC_METER "μg/m³"
|
||||
# "μ" == "\u03bc" but the API uses "\u00b5"
|
||||
default = default.replace("\u03bc", "\u00b5")
|
||||
default = default.lower()
|
||||
return units.get(unit, default)
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
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
|
||||
@@ -38,5 +40,7 @@ class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator[T]]):
|
||||
name=self.coordinator.config_entry.title,
|
||||
)
|
||||
if isinstance(self.coordinator, StatusDataUpdateCoordinator):
|
||||
device_info[ATTR_SW_VERSION] = self.coordinator.data.version
|
||||
device_info[ATTR_SW_VERSION] = cast(
|
||||
StatusDataUpdateCoordinator, self.coordinator
|
||||
).data.version
|
||||
return device_info
|
||||
|
||||
@@ -263,7 +263,7 @@ def correct_db_schema_precision(
|
||||
)
|
||||
|
||||
precision_columns = _get_precision_column_types(table_object)
|
||||
# Attempt to convert timestamp columns to µs precision
|
||||
# Attempt to convert timestamp columns to μs precision
|
||||
session_maker = instance.get_session
|
||||
engine = instance.engine
|
||||
assert engine is not None, "Engine should be set"
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiokem"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiokem==1.0.1"]
|
||||
"requirements": ["aiokem==0.5.12"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==9.2.5"]
|
||||
}
|
||||
|
||||
@@ -150,10 +150,6 @@ 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,13 +194,6 @@ 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.5"]
|
||||
"requirements": ["reolink-aio==0.13.4"]
|
||||
}
|
||||
|
||||
@@ -636,21 +636,14 @@ class SamsungTVWSBridge(
|
||||
)
|
||||
self._remote = None
|
||||
except ConnectionFailure as err:
|
||||
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(
|
||||
LOGGER.warning(
|
||||
(
|
||||
"Unexpected ConnectionFailure trying to get remote for %s, "
|
||||
"please report this issue: %s",
|
||||
self.host,
|
||||
error_details,
|
||||
)
|
||||
"please report this issue: %s"
|
||||
),
|
||||
self.host,
|
||||
repr(err),
|
||||
)
|
||||
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 = None
|
||||
self.is_on: bool | None = False
|
||||
self.async_extra_update: Callable[[], Coroutine[Any, Any, None]] | None = None
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
@@ -52,12 +52,7 @@ 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 from %s to %s",
|
||||
self.bridge.host,
|
||||
old_state,
|
||||
self.is_on,
|
||||
)
|
||||
LOGGER.debug("TV %s state updated to %s", self.bridge.host, self.is_on)
|
||||
|
||||
if self.async_extra_update:
|
||||
await self.async_extra_update()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user