mirror of
https://github.com/home-assistant/core.git
synced 2025-11-08 10:29:27 +00:00
Compare commits
1 Commits
knx-moveou
...
edenhaus-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5e1869a90 |
@@ -33,7 +33,7 @@
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"GitHub.copilot"
|
||||
],
|
||||
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.jsonc
|
||||
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
|
||||
"settings": {
|
||||
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||
@@ -63,9 +63,6 @@
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"[json][jsonc][yaml]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
||||
|
||||
6
.github/workflows/builder.yml
vendored
6
.github/workflows/builder.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -464,7 +464,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -535,7 +535,7 @@ jobs:
|
||||
python --version
|
||||
uv pip freeze >> pip_freeze.txt
|
||||
- name: Upload pip_freeze artifact
|
||||
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: &actions-upload-artifact actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: pip-freeze-${{ matrix.python-version }}
|
||||
path: pip_freeze.txt
|
||||
@@ -867,7 +867,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: &actions-download-artifact actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- &compile-english-translations
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
4
.github/workflows/wheels.yml
vendored
4
.github/workflows/wheels.yml
vendored
@@ -92,7 +92,7 @@ jobs:
|
||||
) > build_constraints.txt
|
||||
|
||||
- name: Upload env_file
|
||||
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: &actions-upload-artifact actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
|
||||
- &download-env-file
|
||||
name: Download env_file
|
||||
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: &actions-download-artifact actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -111,7 +111,6 @@ virtualization/vagrant/config
|
||||
!.vscode/cSpell.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/settings.default.jsonc
|
||||
.env
|
||||
|
||||
# Windows Explorer
|
||||
@@ -141,5 +140,4 @@ pytest_buckets.txt
|
||||
|
||||
# AI tooling
|
||||
.claude/settings.local.json
|
||||
.serena/
|
||||
|
||||
|
||||
@@ -278,7 +278,6 @@ homeassistant.components.imap.*
|
||||
homeassistant.components.imgw_pib.*
|
||||
homeassistant.components.immich.*
|
||||
homeassistant.components.incomfort.*
|
||||
homeassistant.components.inels.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
homeassistant.components.input_text.*
|
||||
|
||||
@@ -9,17 +9,13 @@
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
// Pyright is too pedantic for Home Assistant
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"[json][jsonc][yaml]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
||||
// This value differs between working with devcontainer and locally, therefore this value should NOT be in sync!
|
||||
"url": "./script/json_schemas/manifest_schema.json"
|
||||
}
|
||||
]
|
||||
{
|
||||
"fileMatch": [
|
||||
"homeassistant/components/*/manifest.json"
|
||||
],
|
||||
// This value differs between working with devcontainer and locally, therefor this value should NOT be in sync!
|
||||
"url": "./script/json_schemas/manifest_schema.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@@ -494,8 +494,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/filesize/ @gjohansson-ST
|
||||
/homeassistant/components/filter/ @dgomes
|
||||
/tests/components/filter/ @dgomes
|
||||
/homeassistant/components/fing/ @Lorenzo-Gasparini
|
||||
/tests/components/fing/ @Lorenzo-Gasparini
|
||||
/homeassistant/components/firefly_iii/ @erwindouna
|
||||
/tests/components/firefly_iii/ @erwindouna
|
||||
/homeassistant/components/fireservicerota/ @cyberjunky
|
||||
@@ -743,8 +741,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/improv_ble/ @emontnemery
|
||||
/homeassistant/components/incomfort/ @jbouwh
|
||||
/tests/components/incomfort/ @jbouwh
|
||||
/homeassistant/components/inels/ @epdevlab
|
||||
/tests/components/inels/ @epdevlab
|
||||
/homeassistant/components/influxdb/ @mdegat01
|
||||
/tests/components/influxdb/ @mdegat01
|
||||
/homeassistant/components/inkbird/ @bdraco
|
||||
@@ -1543,8 +1539,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/suez_water/ @ooii @jb101010-2
|
||||
/homeassistant/components/sun/ @home-assistant/core
|
||||
/tests/components/sun/ @home-assistant/core
|
||||
/homeassistant/components/sunricher_dali_center/ @niracler
|
||||
/tests/components/sunricher_dali_center/ @niracler
|
||||
/homeassistant/components/supla/ @mwegrzynek
|
||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||
|
||||
15
Dockerfile
generated
15
Dockerfile
generated
@@ -15,20 +15,7 @@ ARG QEMU_CPU
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
RUN \
|
||||
case "${BUILD_ARCH}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.11/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
COPY --from=ghcr.io/alexxit/go2rtc:1.9.11 /usr/local/bin/go2rtc /bin/go2rtc
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.9.5
|
||||
|
||||
@@ -5,6 +5,9 @@ build_from:
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
cosign:
|
||||
base_identity: https://github.com/home-assistant/docker/.*
|
||||
identity: https://github.com/home-assistant/core/.*
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["airos==0.6.0"]
|
||||
"requirements": ["airos==0.5.6"]
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==1.0.2"]
|
||||
"requirements": ["aioairzone==1.0.1"]
|
||||
}
|
||||
|
||||
@@ -109,12 +109,12 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_and_abort(
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data_updates=config
|
||||
)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_and_abort(
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(), data_updates=config
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
@@ -248,7 +248,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(discovery_info[CONF_MAC])
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: discovery_info[CONF_HOST]}, reload_on_update=False
|
||||
updates={CONF_HOST: discovery_info[CONF_HOST]}
|
||||
)
|
||||
|
||||
self.context.update(
|
||||
|
||||
@@ -816,20 +816,13 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
return MediaPlayerState.PAUSED
|
||||
if media_status.player_is_idle:
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
if self._chromecast is not None and self._chromecast.is_idle:
|
||||
# If library consider us idle, that is our off state
|
||||
# it takes HDMI status into account for cast devices.
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO:
|
||||
# Some apps don't report media status, show the player as playing
|
||||
return MediaPlayerState.PLAYING
|
||||
|
||||
if self.app_id is not None:
|
||||
# We have an active app
|
||||
if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID:
|
||||
if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO:
|
||||
# Some apps don't report media status, show the player as playing
|
||||
return MediaPlayerState.PLAYING
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
if self._chromecast is not None and self._chromecast.is_idle:
|
||||
return MediaPlayerState.OFF
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from .const import DEFAULT_PORT, DOMAIN
|
||||
from .errors import (
|
||||
ConnectionRefused,
|
||||
ConnectionReset,
|
||||
ConnectionTimeout,
|
||||
ResolveFailed,
|
||||
ValidationFailure,
|
||||
@@ -50,8 +49,6 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._errors[CONF_HOST] = "connection_timeout"
|
||||
except ConnectionRefused:
|
||||
self._errors[CONF_HOST] = "connection_refused"
|
||||
except ConnectionReset:
|
||||
self._errors[CONF_HOST] = "connection_reset"
|
||||
except ValidationFailure:
|
||||
return True
|
||||
else:
|
||||
|
||||
@@ -25,7 +25,3 @@ class ConnectionTimeout(TemporaryFailure):
|
||||
|
||||
class ConnectionRefused(TemporaryFailure):
|
||||
"""Network connection refused."""
|
||||
|
||||
|
||||
class ConnectionReset(TemporaryFailure):
|
||||
"""Network connection reset."""
|
||||
|
||||
@@ -13,7 +13,6 @@ from homeassistant.util.ssl import get_default_context
|
||||
from .const import TIMEOUT
|
||||
from .errors import (
|
||||
ConnectionRefused,
|
||||
ConnectionReset,
|
||||
ConnectionTimeout,
|
||||
ResolveFailed,
|
||||
ValidationFailure,
|
||||
@@ -59,8 +58,6 @@ async def get_cert_expiry_timestamp(
|
||||
raise ConnectionRefused(
|
||||
f"Connection refused by server: {hostname}:{port}"
|
||||
) from err
|
||||
except ConnectionResetError as err:
|
||||
raise ConnectionReset(f"Connection reset by server: {hostname}:{port}") from err
|
||||
except ssl.CertificateError as err:
|
||||
raise ValidationFailure(err.verify_message) from err
|
||||
except ssl.SSLError as err:
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
"error": {
|
||||
"resolve_failed": "This host cannot be resolved",
|
||||
"connection_timeout": "Timeout when connecting to this host",
|
||||
"connection_refused": "Connection refused when connecting to host",
|
||||
"connection_reset": "Connection reset when connecting to host"
|
||||
"connection_refused": "Connection refused when connecting to host"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
|
||||
@@ -34,7 +34,7 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT, Platform.MEDIA_PLAYER]
|
||||
PLATFORMS = [Platform.LIGHT, Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
"""Platform for Control4 Climate/Thermostat."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyControl4.climate import C4Climate
|
||||
from pyControl4.error_handling import C4Exception
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from . import Control4ConfigEntry, Control4RuntimeData, get_items_of_category
|
||||
from .const import CONTROL4_ENTITY_TYPE
|
||||
from .director_utils import update_variables_for_config_entry
|
||||
from .entity import Control4Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONTROL4_CATEGORY = "comfort"
|
||||
|
||||
# Control4 variable names
|
||||
CONTROL4_HVAC_STATE = "HVAC_STATE"
|
||||
CONTROL4_HVAC_MODE = "HVAC_MODE"
|
||||
CONTROL4_CURRENT_TEMPERATURE = "TEMPERATURE_F"
|
||||
CONTROL4_HUMIDITY = "HUMIDITY"
|
||||
CONTROL4_COOL_SETPOINT = "COOL_SETPOINT_F"
|
||||
CONTROL4_HEAT_SETPOINT = "HEAT_SETPOINT_F"
|
||||
|
||||
VARIABLES_OF_INTEREST = {
|
||||
CONTROL4_HVAC_STATE,
|
||||
CONTROL4_HVAC_MODE,
|
||||
CONTROL4_CURRENT_TEMPERATURE,
|
||||
CONTROL4_HUMIDITY,
|
||||
CONTROL4_COOL_SETPOINT,
|
||||
CONTROL4_HEAT_SETPOINT,
|
||||
}
|
||||
|
||||
# Map Control4 HVAC modes to Home Assistant
|
||||
C4_TO_HA_HVAC_MODE = {
|
||||
"Off": HVACMode.OFF,
|
||||
"Cool": HVACMode.COOL,
|
||||
"Heat": HVACMode.HEAT,
|
||||
"Auto": HVACMode.HEAT_COOL,
|
||||
}
|
||||
|
||||
HA_TO_C4_HVAC_MODE = {v: k for k, v in C4_TO_HA_HVAC_MODE.items()}
|
||||
|
||||
# Map Control4 HVAC state to Home Assistant HVAC action
|
||||
C4_TO_HA_HVAC_ACTION = {
|
||||
"heating": HVACAction.HEATING,
|
||||
"cooling": HVACAction.COOLING,
|
||||
"idle": HVACAction.IDLE,
|
||||
"off": HVACAction.OFF,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: Control4ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Control4 thermostats from a config entry."""
|
||||
runtime_data = entry.runtime_data
|
||||
|
||||
async def async_update_data() -> dict[int, dict[str, Any]]:
|
||||
"""Fetch data from Control4 director for thermostats."""
|
||||
try:
|
||||
return await update_variables_for_config_entry(
|
||||
hass, entry, VARIABLES_OF_INTEREST
|
||||
)
|
||||
except C4Exception as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator[dict[int, dict[str, Any]]](
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="climate",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=runtime_data.scan_interval),
|
||||
config_entry=entry,
|
||||
)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_refresh()
|
||||
|
||||
items_of_category = await get_items_of_category(hass, entry, CONTROL4_CATEGORY)
|
||||
entity_list = []
|
||||
for item in items_of_category:
|
||||
try:
|
||||
if item["type"] == CONTROL4_ENTITY_TYPE:
|
||||
item_name = item["name"]
|
||||
item_id = item["id"]
|
||||
item_parent_id = item["parentId"]
|
||||
item_manufacturer = None
|
||||
item_device_name = None
|
||||
item_model = None
|
||||
|
||||
for parent_item in items_of_category:
|
||||
if parent_item["id"] == item_parent_id:
|
||||
item_manufacturer = parent_item.get("manufacturer")
|
||||
item_device_name = parent_item.get("roomName")
|
||||
item_model = parent_item.get("model")
|
||||
else:
|
||||
continue
|
||||
except KeyError:
|
||||
_LOGGER.exception(
|
||||
"Unknown device properties received from Control4: %s",
|
||||
item,
|
||||
)
|
||||
continue
|
||||
|
||||
# Skip if we don't have data for this thermostat
|
||||
if item_id not in coordinator.data:
|
||||
_LOGGER.warning(
|
||||
"Couldn't get climate state data for %s (ID: %s), skipping setup",
|
||||
item_name,
|
||||
item_id,
|
||||
)
|
||||
continue
|
||||
|
||||
entity_list.append(
|
||||
Control4Climate(
|
||||
runtime_data,
|
||||
coordinator,
|
||||
item_name,
|
||||
item_id,
|
||||
item_device_name,
|
||||
item_manufacturer,
|
||||
item_model,
|
||||
item_parent_id,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entity_list)
|
||||
|
||||
|
||||
class Control4Climate(Control4Entity, ClimateEntity):
|
||||
"""Control4 climate entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
runtime_data: Control4RuntimeData,
|
||||
coordinator: DataUpdateCoordinator[dict[int, dict[str, Any]]],
|
||||
name: str,
|
||||
idx: int,
|
||||
device_name: str | None,
|
||||
device_manufacturer: str | None,
|
||||
device_model: str | None,
|
||||
device_id: int,
|
||||
) -> None:
|
||||
"""Initialize Control4 climate entity."""
|
||||
super().__init__(
|
||||
runtime_data,
|
||||
coordinator,
|
||||
name,
|
||||
idx,
|
||||
device_name,
|
||||
device_manufacturer,
|
||||
device_model,
|
||||
device_id,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self._thermostat_data is not None
|
||||
|
||||
def _create_api_object(self) -> C4Climate:
|
||||
"""Create a pyControl4 device object.
|
||||
|
||||
This exists so the director token used is always the latest one, without needing to re-init the entire entity.
|
||||
"""
|
||||
return C4Climate(self.runtime_data.director, self._idx)
|
||||
|
||||
@property
|
||||
def _thermostat_data(self) -> dict[str, Any] | None:
|
||||
"""Return the thermostat data from the coordinator."""
|
||||
return self.coordinator.data.get(self._idx)
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
data = self._thermostat_data
|
||||
if data is None:
|
||||
return None
|
||||
return data.get(CONTROL4_CURRENT_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def current_humidity(self) -> int | None:
|
||||
"""Return the current humidity."""
|
||||
data = self._thermostat_data
|
||||
if data is None:
|
||||
return None
|
||||
humidity = data.get(CONTROL4_HUMIDITY)
|
||||
return int(humidity) if humidity is not None else None
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return current HVAC mode."""
|
||||
data = self._thermostat_data
|
||||
if data is None:
|
||||
return HVACMode.OFF
|
||||
c4_mode = data.get(CONTROL4_HVAC_MODE) or ""
|
||||
return C4_TO_HA_HVAC_MODE.get(c4_mode, HVACMode.OFF)
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return current HVAC action."""
|
||||
data = self._thermostat_data
|
||||
if data is None:
|
||||
return None
|
||||
c4_state = data.get(CONTROL4_HVAC_STATE)
|
||||
if c4_state is None:
|
||||
return None
|
||||
# Convert state to lowercase for mapping
|
||||
return C4_TO_HA_HVAC_ACTION.get(str(c4_state).lower())
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the target temperature."""
|
||||
data = self._thermostat_data
|
||||
if data is None:
|
||||
return None
|
||||
hvac_mode = self.hvac_mode
|
||||
if hvac_mode == HVACMode.COOL:
|
||||
return data.get(CONTROL4_COOL_SETPOINT)
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
return data.get(CONTROL4_HEAT_SETPOINT)
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the high target temperature for auto mode."""
|
||||
data = self._thermostat_data
|
||||
if data is None:
|
||||
return None
|
||||
if self.hvac_mode == HVACMode.HEAT_COOL:
|
||||
return data.get(CONTROL4_COOL_SETPOINT)
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the low target temperature for auto mode."""
|
||||
data = self._thermostat_data
|
||||
if data is None:
|
||||
return None
|
||||
if self.hvac_mode == HVACMode.HEAT_COOL:
|
||||
return data.get(CONTROL4_HEAT_SETPOINT)
|
||||
return None
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target HVAC mode."""
|
||||
c4_hvac_mode = HA_TO_C4_HVAC_MODE[hvac_mode]
|
||||
c4_climate = self._create_api_object()
|
||||
await c4_climate.setHvacMode(c4_hvac_mode)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
c4_climate = self._create_api_object()
|
||||
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
# Handle temperature range for auto mode
|
||||
if self.hvac_mode == HVACMode.HEAT_COOL:
|
||||
if low_temp is not None:
|
||||
await c4_climate.setHeatSetpointF(low_temp)
|
||||
if high_temp is not None:
|
||||
await c4_climate.setCoolSetpointF(high_temp)
|
||||
# Handle single temperature setpoint
|
||||
elif temp is not None:
|
||||
if self.hvac_mode == HVACMode.COOL:
|
||||
await c4_climate.setCoolSetpointF(temp)
|
||||
elif self.hvac_mode == HVACMode.HEAT:
|
||||
await c4_climate.setHeatSetpointF(temp)
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -184,8 +184,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_HOST: self.host,
|
||||
CONF_PORT: self.port,
|
||||
CONF_API_KEY: self.api_key,
|
||||
},
|
||||
reload_on_update=False,
|
||||
}
|
||||
)
|
||||
|
||||
except TimeoutError:
|
||||
@@ -232,8 +231,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
updates={
|
||||
CONF_HOST: self.host,
|
||||
CONF_PORT: self.port,
|
||||
},
|
||||
reload_on_update=False,
|
||||
}
|
||||
)
|
||||
|
||||
self.context.update(
|
||||
@@ -267,8 +265,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_HOST: self.host,
|
||||
CONF_PORT: self.port,
|
||||
CONF_API_KEY: self.api_key,
|
||||
},
|
||||
reload_on_update=False,
|
||||
}
|
||||
)
|
||||
|
||||
self.context["configuration_url"] = HASSIO_CONFIGURATION_URL
|
||||
|
||||
@@ -122,12 +122,10 @@ class WanIpSensor(SensorEntity):
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
response = await self.resolver.query(self.hostname, self.querytype)
|
||||
except TimeoutError as err:
|
||||
_LOGGER.debug("Timeout while resolving host: %s", err)
|
||||
except TimeoutError:
|
||||
await self.resolver.close()
|
||||
except DNSError as err:
|
||||
_LOGGER.warning("Exception while resolving host: %s", err)
|
||||
await self.resolver.close()
|
||||
|
||||
if response:
|
||||
sorted_ips = sort_ips(
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/droplet",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pydroplet==2.3.4"],
|
||||
"requirements": ["pydroplet==2.3.3"],
|
||||
"zeroconf": ["_droplet._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS
|
||||
from .const import SUPPORTED_LIFESPANS
|
||||
from .entity import (
|
||||
EcovacsCapabilityEntityDescription,
|
||||
EcovacsDescriptionEntity,
|
||||
@@ -62,7 +62,7 @@ STATION_ENTITY_DESCRIPTIONS = tuple(
|
||||
key=f"station_action_{action.name.lower()}",
|
||||
translation_key=f"station_action_{action.name.lower()}",
|
||||
)
|
||||
for action in SUPPORTED_STATION_ACTIONS
|
||||
for action in StationAction
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,11 +23,7 @@ SUPPORTED_LIFESPANS = (
|
||||
LifeSpan.STATION_FILTER,
|
||||
)
|
||||
|
||||
SUPPORTED_STATION_ACTIONS = (
|
||||
StationAction.CLEAN_BASE,
|
||||
StationAction.DRY_MOP,
|
||||
StationAction.EMPTY_DUSTBIN,
|
||||
)
|
||||
SUPPORTED_STATION_ACTIONS = (StationAction.EMPTY_DUSTBIN,)
|
||||
|
||||
LEGACY_SUPPORTED_LIFESPANS = (
|
||||
"main_brush",
|
||||
|
||||
@@ -36,12 +36,6 @@
|
||||
"reset_lifespan_round_mop": {
|
||||
"default": "mdi:broom"
|
||||
},
|
||||
"station_action_clean_base": {
|
||||
"default": "mdi:home"
|
||||
},
|
||||
"station_action_dry_mop": {
|
||||
"default": "mdi:broom"
|
||||
},
|
||||
"station_action_empty_dustbin": {
|
||||
"default": "mdi:delete-restore"
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==16.1.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==15.1.0"]
|
||||
}
|
||||
|
||||
@@ -70,12 +70,6 @@
|
||||
"reset_lifespan_side_brush": {
|
||||
"name": "Reset side brush lifespan"
|
||||
},
|
||||
"station_action_clean_base": {
|
||||
"name": "Clean base"
|
||||
},
|
||||
"station_action_dry_mop": {
|
||||
"name": "Dry mop"
|
||||
},
|
||||
"station_action_empty_dustbin": {
|
||||
"name": "Empty dustbin"
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
"""The Fing integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
|
||||
from .coordinator import FingConfigEntry, FingDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.DEVICE_TRACKER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: FingConfigEntry) -> bool:
|
||||
"""Set up the Fing component."""
|
||||
|
||||
coordinator = FingDataUpdateCoordinator(hass, config_entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
if coordinator.data.network_id is None:
|
||||
_LOGGER.warning(
|
||||
"Skip setting up Fing integration; Received an empty NetworkId from the request - Check if the API version is the latest"
|
||||
)
|
||||
raise ConfigEntryError(
|
||||
"The Agent's API version is outdated. Please update the agent to the latest version."
|
||||
)
|
||||
|
||||
config_entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: FingConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
@@ -1,114 +0,0 @@
|
||||
"""Config flow file."""
|
||||
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fing_agent_api import FingAgent
|
||||
import httpx
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT
|
||||
|
||||
from .const import DOMAIN, UPNP_AVAILABLE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Fing config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Set up user step."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
devices_response = None
|
||||
agent_info_response = None
|
||||
|
||||
self._async_abort_entries_match(
|
||||
{CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]}
|
||||
)
|
||||
|
||||
fing_api = FingAgent(
|
||||
ip=user_input[CONF_IP_ADDRESS],
|
||||
port=int(user_input[CONF_PORT]),
|
||||
key=user_input[CONF_API_KEY],
|
||||
)
|
||||
|
||||
try:
|
||||
devices_response = await fing_api.get_devices()
|
||||
|
||||
with suppress(httpx.ConnectError):
|
||||
# The suppression is needed because the get_agent_info method isn't available for desktop agents
|
||||
agent_info_response = await fing_api.get_agent_info()
|
||||
|
||||
except httpx.NetworkError as _:
|
||||
errors["base"] = "cannot_connect"
|
||||
except httpx.TimeoutException as _:
|
||||
errors["base"] = "timeout_connect"
|
||||
except httpx.HTTPStatusError as exception:
|
||||
description_placeholders["message"] = (
|
||||
f"{exception.response.status_code} - {exception.response.reason_phrase}"
|
||||
)
|
||||
if exception.response.status_code == 401:
|
||||
errors["base"] = "invalid_api_key"
|
||||
else:
|
||||
errors["base"] = "http_status_error"
|
||||
except httpx.InvalidURL as _:
|
||||
errors["base"] = "url_error"
|
||||
except (
|
||||
httpx.HTTPError,
|
||||
httpx.CookieConflict,
|
||||
httpx.StreamError,
|
||||
) as ex:
|
||||
_LOGGER.error("Unexpected exception: %s", ex)
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
if (
|
||||
devices_response.network_id is not None
|
||||
and len(devices_response.network_id) > 0
|
||||
):
|
||||
agent_name = user_input.get(CONF_IP_ADDRESS)
|
||||
upnp_available = False
|
||||
if agent_info_response is not None:
|
||||
upnp_available = True
|
||||
agent_name = agent_info_response.agent_id
|
||||
await self.async_set_unique_id(agent_info_response.agent_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
data = {
|
||||
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
UPNP_AVAILABLE: upnp_available,
|
||||
}
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Fing Agent {agent_name}",
|
||||
data=data,
|
||||
)
|
||||
|
||||
return self.async_abort(reason="api_version_error")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_IP_ADDRESS): str,
|
||||
vol.Required(CONF_PORT, default="49090"): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
),
|
||||
user_input,
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
@@ -1,4 +0,0 @@
|
||||
"""Const for the Fing integration."""
|
||||
|
||||
DOMAIN = "fing"
|
||||
UPNP_AVAILABLE = "upnp_available"
|
||||
@@ -1,85 +0,0 @@
|
||||
"""DataUpdateCoordinator for Fing integration."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from fing_agent_api import FingAgent
|
||||
from fing_agent_api.models import AgentInfoResponse, Device
|
||||
import httpx
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, UPNP_AVAILABLE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type FingConfigEntry = ConfigEntry[FingDataUpdateCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class FingDataObject:
|
||||
"""Fing Data Object."""
|
||||
|
||||
network_id: str | None = None
|
||||
agent_info: AgentInfoResponse | None = None
|
||||
devices: dict[str, Device] = field(default_factory=dict)
|
||||
|
||||
|
||||
class FingDataUpdateCoordinator(DataUpdateCoordinator[FingDataObject]):
|
||||
"""Class to manage fetching data from Fing Agent."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: FingConfigEntry) -> None:
|
||||
"""Initialize global Fing updater."""
|
||||
self._fing = FingAgent(
|
||||
ip=config_entry.data[CONF_IP_ADDRESS],
|
||||
port=int(config_entry.data[CONF_PORT]),
|
||||
key=config_entry.data[CONF_API_KEY],
|
||||
)
|
||||
self._upnp_available = config_entry.data[UPNP_AVAILABLE]
|
||||
update_interval = timedelta(seconds=30)
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=update_interval,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> FingDataObject:
|
||||
"""Fetch data from Fing Agent."""
|
||||
device_response = None
|
||||
agent_info_response = None
|
||||
try:
|
||||
device_response = await self._fing.get_devices()
|
||||
|
||||
if self._upnp_available:
|
||||
agent_info_response = await self._fing.get_agent_info()
|
||||
|
||||
except httpx.NetworkError as err:
|
||||
raise UpdateFailed("Failed to connect") from err
|
||||
except httpx.TimeoutException as err:
|
||||
raise UpdateFailed("Timeout establishing connection") from err
|
||||
except httpx.HTTPStatusError as err:
|
||||
if err.response.status_code == 401:
|
||||
raise UpdateFailed("Invalid API key") from err
|
||||
raise UpdateFailed(
|
||||
f"Http request failed -> {err.response.status_code} - {err.response.reason_phrase}"
|
||||
) from err
|
||||
except httpx.InvalidURL as err:
|
||||
raise UpdateFailed("Invalid hostname or IP address") from err
|
||||
except (
|
||||
httpx.HTTPError,
|
||||
httpx.CookieConflict,
|
||||
httpx.StreamError,
|
||||
) as err:
|
||||
raise UpdateFailed("Unexpected error from HTTP request") from err
|
||||
else:
|
||||
return FingDataObject(
|
||||
device_response.network_id,
|
||||
agent_info_response,
|
||||
{device.mac: device for device in device_response.devices},
|
||||
)
|
||||
@@ -1,127 +0,0 @@
|
||||
"""Platform for Device tracker integration."""
|
||||
|
||||
from fing_agent_api.models import Device
|
||||
|
||||
from homeassistant.components.device_tracker import ScannerEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import FingConfigEntry
|
||||
from .coordinator import FingDataUpdateCoordinator
|
||||
from .utils import get_icon_from_type
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: FingConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add sensors for passed config_entry in HA."""
|
||||
coordinator = config_entry.runtime_data
|
||||
entity_registry = er.async_get(hass)
|
||||
tracked_devices: set[str] = set()
|
||||
|
||||
@callback
|
||||
def add_entities() -> None:
|
||||
latest_devices = set(coordinator.data.devices.keys())
|
||||
|
||||
devices_to_remove = tracked_devices - set(latest_devices)
|
||||
devices_to_add = set(latest_devices) - tracked_devices
|
||||
|
||||
entities_to_remove = []
|
||||
for entity_entry in entity_registry.entities.values():
|
||||
if entity_entry.config_entry_id != config_entry.entry_id:
|
||||
continue
|
||||
try:
|
||||
_, mac = entity_entry.unique_id.rsplit("-", 1)
|
||||
if mac in devices_to_remove:
|
||||
entities_to_remove.append(entity_entry.entity_id)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
for entity_id in entities_to_remove:
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
entities_to_add = []
|
||||
for mac_addr in devices_to_add:
|
||||
device = coordinator.data.devices[mac_addr]
|
||||
entities_to_add.append(FingTrackedDevice(coordinator, device))
|
||||
|
||||
tracked_devices.clear()
|
||||
tracked_devices.update(latest_devices)
|
||||
async_add_entities(entities_to_add)
|
||||
|
||||
add_entities()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(add_entities))
|
||||
|
||||
|
||||
class FingTrackedDevice(CoordinatorEntity[FingDataUpdateCoordinator], ScannerEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: FingDataUpdateCoordinator, device: Device) -> None:
|
||||
"""Set up FingDevice entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._device = device
|
||||
agent_id = coordinator.data.network_id
|
||||
if coordinator.data.agent_info is not None:
|
||||
agent_id = coordinator.data.agent_info.agent_id
|
||||
|
||||
self._attr_mac_address = self._device.mac
|
||||
self._attr_unique_id = f"{agent_id}-{self._attr_mac_address}"
|
||||
self._attr_name = self._device.name
|
||||
self._attr_icon = get_icon_from_type(self._device.type)
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""Return true if the device is connected to the network."""
|
||||
return self._device.active
|
||||
|
||||
@property
|
||||
def ip_address(self) -> str | None:
|
||||
"""Return the primary ip address of the device."""
|
||||
return self._device.ip[0] if self._device.ip else None
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Enable entity by default."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str | None:
|
||||
"""Return the unique ID of the entity."""
|
||||
return self._attr_unique_id
|
||||
|
||||
def check_for_updates(self, new_device: Device) -> bool:
|
||||
"""Return true if the device has updates."""
|
||||
new_device_ip = new_device.ip[0] if new_device.ip else None
|
||||
current_device_ip = self._device.ip[0] if self._device.ip else None
|
||||
|
||||
return (
|
||||
current_device_ip != new_device_ip
|
||||
or self._device.active != new_device.active
|
||||
or self._device.type != new_device.type
|
||||
or self._attr_name != new_device.name
|
||||
or self._attr_icon != get_icon_from_type(new_device.type)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
updated_device_data = self.coordinator.data.devices.get(self._device.mac)
|
||||
if updated_device_data is not None and self.check_for_updates(
|
||||
updated_device_data
|
||||
):
|
||||
self._device = updated_device_data
|
||||
self._attr_name = updated_device_data.name
|
||||
self._attr_icon = get_icon_from_type(updated_device_data.type)
|
||||
er.async_get(self.hass).async_update_entity(
|
||||
entity_id=self.entity_id,
|
||||
original_name=self._attr_name,
|
||||
original_icon=self._attr_icon,
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"domain": "fing",
|
||||
"name": "Fing",
|
||||
"codeowners": ["@Lorenzo-Gasparini"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fing",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["fing_agent_api==1.0.3"]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: The integration has no actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow: done
|
||||
config-flow-test-coverage: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: There are no actions in Fing integration.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: Fing integration entities do not use events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: The integration has no actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: The integration has no options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery: todo
|
||||
discovery-update-info: todo
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: done
|
||||
entity-category: todo
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: The integration creates only device tracker entities
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up Fing agent",
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ip_address": "IP address of the Fing agent.",
|
||||
"port": "Port number of the Fing API.",
|
||||
"api_key": "API key used to authenticate with the Fing API."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"url_error": "[%key:common::config_flow::error::invalid_host%]",
|
||||
"http_status_error": "HTTP request failed: {message}"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"api_version_error": "Your agent is using an outdated API version. The required 'network_id' parameter is missing. Please update to the latest API version."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
"""Utils functions."""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class DeviceType(Enum):
|
||||
"""Device types enum."""
|
||||
|
||||
GENERIC = "mdi:lan-connect"
|
||||
MOBILE = PHONE = "mdi:cellphone"
|
||||
TABLET = IPOD = EREADER = "mdi:tablet"
|
||||
WATCH = WEARABLE = "mdi:watch"
|
||||
CAR = AUTOMOTIVE = "mdi:car-back"
|
||||
MEDIA_PLAYER = "mdi:volume-high"
|
||||
TELEVISION = "mdi:television"
|
||||
GAME_CONSOLE = "mdi:nintendo-game-boy"
|
||||
STREAMING_DONGLE = "mdi:cast"
|
||||
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
|
||||
DISC_PLAYER = "mdi:disk-player"
|
||||
REMOTE_CONTROL = "mdi:remote-tv"
|
||||
RADIO = "mdi:radio"
|
||||
PHOTO_CAMERA = PHOTOS = "mdi:camera"
|
||||
MICROPHONE = VOICE_CONTROL = "mdi:microphone"
|
||||
PROJECTOR = "mdi:projector"
|
||||
COMPUTER = DESKTOP = "mdi:desktop-tower"
|
||||
LAPTOP = "mdi:laptop"
|
||||
PRINTER = "mdi:printer"
|
||||
SCANNER = "mdi:scanner"
|
||||
POS = "mdi:printer-pos"
|
||||
CLOCK = "mdi:clock"
|
||||
BARCODE = "mdi:barcode"
|
||||
SURVEILLANCE_CAMERA = BABY_MONITOR = PET_MONITOR = "mdi:cctv"
|
||||
POE_PLUG = HEALTH_MONITOR = SMART_HOME = SMART_METER = APPLIANCE = SLEEP = (
|
||||
"mdi:home-automation"
|
||||
)
|
||||
SMART_PLUG = "mdi:power-plug"
|
||||
LIGHT = "mdi:lightbulb"
|
||||
THERMOSTAT = HEATING = "mdi:home-thermometer"
|
||||
POWER_SYSTEM = ENERGY = "mdi:lightning-bolt"
|
||||
SOLAR_PANEL = "mdi:solar-power"
|
||||
WASHER = "mdi:washing-machine"
|
||||
FRIDGE = "mdi:fridge"
|
||||
CLEANER = "mdi:vacuum"
|
||||
GARAGE = "mdi:garage"
|
||||
SPRINKLER = "mdi:sprinkler"
|
||||
BELL = "mdi:doorbell"
|
||||
KEY_LOCK = "mdi:lock-smart"
|
||||
CONTROL_PANEL = SMART_CONTROLLER = "mdi:alarm-panel"
|
||||
SCALE = "mdi:scale-bathroom"
|
||||
TOY = "mdi:teddy-bear"
|
||||
ROBOT = "mdi:robot"
|
||||
WEATHER = "mdi:weather-cloudy"
|
||||
ALARM = "mdi:alarm-light"
|
||||
MOTION_DETECTOR = "mdi:motion-sensor"
|
||||
SMOKE = HUMIDITY = SENSOR = DOMOTZ_BOX = FINGBOX = "mdi:smoke-detector"
|
||||
ROUTER = MODEM = GATEWAY = FIREWALL = VPN = SMALL_CELL = "mdi:router-network"
|
||||
WIFI = WIFI_EXTENDER = "mdi:wifi"
|
||||
NAS_STORAGE = "mdi:nas"
|
||||
SWITCH = "mdi:switch"
|
||||
USB = "mdi:usb"
|
||||
CLOUD = "mdi:cloud"
|
||||
BATTERY = "mdi:battery"
|
||||
NETWORK_APPLIANCE = "mdi:network"
|
||||
VIRTUAL_MACHINE = MAIL_SERVER = FILE_SERVER = PROXY_SERVER = WEB_SERVER = (
|
||||
DOMAIN_SERVER
|
||||
) = COMMUNICATION = "mdi:monitor"
|
||||
SERVER = "mdi:server"
|
||||
TERMINAL = "mdi:console"
|
||||
DATABASE = "mdi:database"
|
||||
RASPBERRY = ARDUINO = "mdi:raspberry-pi"
|
||||
PROCESSOR = CIRCUIT_CARD = RFID = "mdi:chip"
|
||||
INDUSTRIAL = "mdi:factory"
|
||||
MEDICAL = "mdi:medical-bag"
|
||||
VOIP = CONFERENCING = "mdi:phone-voip"
|
||||
FITNESS = "mdi:dumbbell"
|
||||
POOL = "mdi:pool"
|
||||
SECURITY_SYSTEM = "mdi:security"
|
||||
|
||||
|
||||
def get_icon_from_type(type: str) -> str:
|
||||
"""Return the right icon based on the type."""
|
||||
try:
|
||||
return DeviceType[type].value
|
||||
except (ValueError, KeyError):
|
||||
return "mdi:lan-connect"
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/firefly_iii",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyfirefly==0.1.8"]
|
||||
"requirements": ["pyfirefly==0.1.6"]
|
||||
}
|
||||
|
||||
@@ -15,13 +15,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.json import json_loads
|
||||
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_RECOMMENDED,
|
||||
LOGGER,
|
||||
RECOMMENDED_AI_TASK_MAX_TOKENS,
|
||||
RECOMMENDED_IMAGE_MODEL,
|
||||
)
|
||||
from .const import CONF_CHAT_MODEL, CONF_RECOMMENDED, LOGGER, RECOMMENDED_IMAGE_MODEL
|
||||
from .entity import (
|
||||
ERROR_GETTING_RESPONSE,
|
||||
GoogleGenerativeAILLMBaseEntity,
|
||||
@@ -79,9 +73,7 @@ class GoogleGenerativeAITaskEntity(
|
||||
chat_log: conversation.ChatLog,
|
||||
) -> ai_task.GenDataTaskResult:
|
||||
"""Handle a generate data task."""
|
||||
await self._async_handle_chat_log(
|
||||
chat_log, task.structure, default_max_tokens=RECOMMENDED_AI_TASK_MAX_TOKENS
|
||||
)
|
||||
await self._async_handle_chat_log(chat_log, task.structure)
|
||||
|
||||
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
||||
LOGGER.error(
|
||||
|
||||
@@ -32,8 +32,6 @@ CONF_TOP_K = "top_k"
|
||||
RECOMMENDED_TOP_K = 64
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
RECOMMENDED_MAX_TOKENS = 3000
|
||||
# Input 5000, output 19400 = 0.05 USD
|
||||
RECOMMENDED_AI_TASK_MAX_TOKENS = 19400
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD = "harassment_block_threshold"
|
||||
CONF_HATE_BLOCK_THRESHOLD = "hate_block_threshold"
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD = "sexual_block_threshold"
|
||||
|
||||
@@ -472,7 +472,6 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
self,
|
||||
chat_log: conversation.ChatLog,
|
||||
structure: vol.Schema | None = None,
|
||||
default_max_tokens: int | None = None,
|
||||
) -> None:
|
||||
"""Generate an answer for the chat log."""
|
||||
options = self.subentry.data
|
||||
@@ -619,9 +618,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
if not chat_log.unresponded_tool_results:
|
||||
break
|
||||
|
||||
def create_generate_content_config(
|
||||
self, default_max_tokens: int | None = None
|
||||
) -> GenerateContentConfig:
|
||||
def create_generate_content_config(self) -> GenerateContentConfig:
|
||||
"""Create the GenerateContentConfig for the LLM."""
|
||||
options = self.subentry.data
|
||||
model = options.get(CONF_CHAT_MODEL, self.default_model)
|
||||
@@ -635,12 +632,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
|
||||
top_k=options.get(CONF_TOP_K, RECOMMENDED_TOP_K),
|
||||
top_p=options.get(CONF_TOP_P, RECOMMENDED_TOP_P),
|
||||
max_output_tokens=options.get(
|
||||
CONF_MAX_TOKENS,
|
||||
default_max_tokens
|
||||
if default_max_tokens is not None
|
||||
else RECOMMENDED_MAX_TOKENS,
|
||||
),
|
||||
max_output_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
||||
safety_settings=[
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||
|
||||
@@ -36,7 +36,7 @@ DEFAULT_URL = SERVER_URLS[0]
|
||||
|
||||
DOMAIN = "growatt_server"
|
||||
|
||||
PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
LOGIN_INVALID_AUTH_CODE = "502"
|
||||
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
"""Number platform for Growatt."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from growattServer import GrowattV1ApiError
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GrowattConfigEntry, GrowattCoordinator
|
||||
from .sensor.sensor_entity_description import GrowattRequiredKeysMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = (
|
||||
1 # Serialize updates as inverter does not handle concurrent requests
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GrowattNumberEntityDescription(NumberEntityDescription, GrowattRequiredKeysMixin):
|
||||
"""Describes Growatt number entity."""
|
||||
|
||||
write_key: str | None = None # Parameter ID for writing (if different from api_key)
|
||||
|
||||
|
||||
# Note that the Growatt V1 API uses different keys for reading and writing parameters.
|
||||
# Reading values returns camelCase keys, while writing requires snake_case keys.
|
||||
|
||||
MIN_NUMBER_TYPES: tuple[GrowattNumberEntityDescription, ...] = (
|
||||
GrowattNumberEntityDescription(
|
||||
key="battery_charge_power_limit",
|
||||
translation_key="battery_charge_power_limit",
|
||||
api_key="chargePowerCommand", # Key returned by V1 API
|
||||
write_key="charge_power", # Key used to write parameter
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
GrowattNumberEntityDescription(
|
||||
key="battery_charge_soc_limit",
|
||||
translation_key="battery_charge_soc_limit",
|
||||
api_key="wchargeSOCLowLimit", # Key returned by V1 API
|
||||
write_key="charge_stop_soc", # Key used to write parameter
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
GrowattNumberEntityDescription(
|
||||
key="battery_discharge_power_limit",
|
||||
translation_key="battery_discharge_power_limit",
|
||||
api_key="disChargePowerCommand", # Key returned by V1 API
|
||||
write_key="discharge_power", # Key used to write parameter
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
GrowattNumberEntityDescription(
|
||||
key="battery_discharge_soc_limit",
|
||||
translation_key="battery_discharge_soc_limit",
|
||||
api_key="wdisChargeSOCLowLimit", # Key returned by V1 API
|
||||
write_key="discharge_stop_soc", # Key used to write parameter
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GrowattConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Growatt number entities."""
|
||||
runtime_data = entry.runtime_data
|
||||
|
||||
# Add number entities for each MIN device (only supported with V1 API)
|
||||
async_add_entities(
|
||||
GrowattNumber(device_coordinator, description)
|
||||
for device_coordinator in runtime_data.devices.values()
|
||||
if (
|
||||
device_coordinator.device_type == "min"
|
||||
and device_coordinator.api_version == "v1"
|
||||
)
|
||||
for description in MIN_NUMBER_TYPES
|
||||
)
|
||||
|
||||
|
||||
class GrowattNumber(CoordinatorEntity[GrowattCoordinator], NumberEntity):
|
||||
"""Representation of a Growatt number."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
entity_description: GrowattNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: GrowattCoordinator,
|
||||
description: GrowattNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the number."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.device_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.device_id)},
|
||||
manufacturer="Growatt",
|
||||
name=coordinator.device_id,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the current value of the number."""
|
||||
value = self.coordinator.data.get(self.entity_description.api_key)
|
||||
if value is None:
|
||||
return None
|
||||
return int(value)
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the value of the number."""
|
||||
# Use write_key if specified, otherwise fall back to api_key
|
||||
parameter_id = (
|
||||
self.entity_description.write_key or self.entity_description.api_key
|
||||
)
|
||||
int_value = int(value)
|
||||
|
||||
try:
|
||||
# Use V1 API to write parameter
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.api.min_write_parameter,
|
||||
self.coordinator.device_id,
|
||||
parameter_id,
|
||||
int_value,
|
||||
)
|
||||
except GrowattV1ApiError as e:
|
||||
raise HomeAssistantError(f"Error while setting parameter: {e}") from e
|
||||
|
||||
# If no exception was raised, the write was successful
|
||||
_LOGGER.debug(
|
||||
"Set parameter %s to %s",
|
||||
parameter_id,
|
||||
value,
|
||||
)
|
||||
|
||||
# Update the value in coordinator data to avoid triggering an immediate
|
||||
# refresh that would hit the API rate limit (5-minute polling interval)
|
||||
self.coordinator.data[self.entity_description.api_key] = int_value
|
||||
self.async_write_ha_state()
|
||||
@@ -504,20 +504,6 @@
|
||||
"name": "Maximum power"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"battery_charge_power_limit": {
|
||||
"name": "Battery charge power limit"
|
||||
},
|
||||
"battery_charge_soc_limit": {
|
||||
"name": "Battery charge SOC limit"
|
||||
},
|
||||
"battery_discharge_power_limit": {
|
||||
"name": "Battery discharge power limit"
|
||||
},
|
||||
"battery_discharge_soc_limit": {
|
||||
"name": "Battery discharge SOC limit"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"ac_charge": {
|
||||
"name": "Charge from grid"
|
||||
|
||||
@@ -109,8 +109,6 @@ DATA_KEY_HOST = "host"
|
||||
DATA_KEY_SUPERVISOR_ISSUES = "supervisor_issues"
|
||||
|
||||
PLACEHOLDER_KEY_ADDON = "addon"
|
||||
PLACEHOLDER_KEY_ADDON_INFO = "addon_info"
|
||||
PLACEHOLDER_KEY_ADDON_DOCUMENTATION = "addon_documentation"
|
||||
PLACEHOLDER_KEY_ADDON_URL = "addon_url"
|
||||
PLACEHOLDER_KEY_REFERENCE = "reference"
|
||||
PLACEHOLDER_KEY_COMPONENTS = "components"
|
||||
@@ -122,7 +120,6 @@ ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING = "issue_addon_detached_addon_missing"
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
|
||||
ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned"
|
||||
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
|
||||
ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon"
|
||||
|
||||
CORE_CONTAINER = "homeassistant"
|
||||
SUPERVISOR_CONTAINER = "hassio_supervisor"
|
||||
@@ -159,7 +156,6 @@ EXTRA_PLACEHOLDERS = {
|
||||
ISSUE_KEY_ADDON_PWNED: {
|
||||
"more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords",
|
||||
},
|
||||
ISSUE_KEY_ADDON_DEPRECATED: HELP_URLS,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ from .const import (
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
EXTRA_PLACEHOLDERS,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_DEPRECATED,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
@@ -85,7 +84,6 @@ ISSUE_KEYS_FOR_REPAIRS = {
|
||||
"issue_system_disk_lifetime",
|
||||
ISSUE_KEY_SYSTEM_FREE_SPACE,
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
ISSUE_KEY_ADDON_DEPRECATED,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,13 +18,10 @@ from . import get_addons_info, get_issues_info
|
||||
from .const import (
|
||||
EXTRA_PLACEHOLDERS,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_DEPRECATED,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
PLACEHOLDER_KEY_ADDON,
|
||||
PLACEHOLDER_KEY_ADDON_DOCUMENTATION,
|
||||
PLACEHOLDER_KEY_ADDON_INFO,
|
||||
PLACEHOLDER_KEY_COMPONENTS,
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
)
|
||||
@@ -198,23 +195,6 @@ class AddonIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
return placeholders or None
|
||||
|
||||
|
||||
class DeprecatedAddonIssueRepairFlow(AddonIssueRepairFlow):
|
||||
"""Handler for deprecated addon issue fixing flows."""
|
||||
|
||||
@property
|
||||
def description_placeholders(self) -> dict[str, str] | None:
|
||||
"""Get description placeholders for steps."""
|
||||
placeholders: dict[str, str] = super().description_placeholders or {}
|
||||
if self.issue and self.issue.reference:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON_INFO] = (
|
||||
f"homeassistant://hassio/addon/{self.issue.reference}/info"
|
||||
)
|
||||
placeholders[PLACEHOLDER_KEY_ADDON_DOCUMENTATION] = (
|
||||
f"homeassistant://hassio/addon/{self.issue.reference}/documentation"
|
||||
)
|
||||
return placeholders or None
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
@@ -225,8 +205,6 @@ async def async_create_fix_flow(
|
||||
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
|
||||
return DockerConfigIssueRepairFlow(hass, issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_ADDON_DEPRECATED:
|
||||
return DeprecatedAddonIssueRepairFlow(hass, issue_id)
|
||||
if issue and issue.key in {
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
|
||||
@@ -56,19 +56,6 @@
|
||||
"title": "Insecure secrets detected in add-on configuration",
|
||||
"description": "Add-on {addon} uses secrets/passwords in its configuration which are detected as not secure. See [pwned passwords and secrets]({more_info_pwned}) for more information on this issue."
|
||||
},
|
||||
"issue_addon_deprecated_addon": {
|
||||
"title": "Installed add-on is deprecated",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"addon_execute_remove": {
|
||||
"description": "Add-on {addon} is marked deprecated by the developer. This means it is no longer being maintained and so may break or become a security issue over time.\n\nReview the [readme]({addon_info}) and [documentation]({addon_documentation}) of the add-on to see if the developer provided instructions.\n\nSelecting **Submit** will uninstall this deprecated add-on. Alternatively, you can check [Home Assistant help]({help_url}) and the [community forum]({community_url}) for alternatives to migrate to."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"apply_suggestion_fail": "Could not uninstall the add-on. Check the Supervisor logs for more details."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issue_mount_mount_failed": {
|
||||
"title": "Network storage device failed",
|
||||
"fix_flow": {
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.83", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.82", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.3.1"]
|
||||
"requirements": ["homematicip==2.3.0"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
@@ -72,25 +71,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry)
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
def get_main_device(
|
||||
hass: HomeAssistant, entry: HomeWizardConfigEntry
|
||||
) -> dr.DeviceEntry | None:
|
||||
"""Helper function to get the main device for the config entry."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry_id=entry.entry_id
|
||||
)
|
||||
|
||||
if not device_entries:
|
||||
return None
|
||||
|
||||
# Get first device that is not a sub-device, as this is the main device in HomeWizard
|
||||
# This is relevant for the P1 Meter which may create sub-devices for external utility meters
|
||||
return next(
|
||||
(device for device in device_entries if device.via_device_id is None), None
|
||||
)
|
||||
|
||||
|
||||
async def async_check_v2_support_and_create_issue(
|
||||
hass: HomeAssistant, entry: HomeWizardConfigEntry
|
||||
) -> None:
|
||||
@@ -99,16 +79,6 @@ async def async_check_v2_support_and_create_issue(
|
||||
if not await has_v2_api(entry.data[CONF_IP_ADDRESS], async_get_clientsession(hass)):
|
||||
return
|
||||
|
||||
title = entry.title
|
||||
|
||||
# Try to get the name from the device registry
|
||||
# This is to make it clearer which device needs reconfiguration, as the config entry title is kept default most of the time
|
||||
if main_device := get_main_device(hass, entry):
|
||||
device_name = main_device.name_by_user or main_device.name
|
||||
|
||||
if device_name and entry.title != device_name:
|
||||
title = f"{entry.title} ({device_name})"
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
@@ -118,7 +88,7 @@ async def async_check_v2_support_and_create_issue(
|
||||
learn_more_url="https://home-assistant.io/integrations/homewizard/#which-button-do-i-need-to-press-to-configure-the-device",
|
||||
translation_key="migrate_to_v2_api",
|
||||
translation_placeholders={
|
||||
"title": title,
|
||||
"title": entry.title,
|
||||
},
|
||||
severity=IssueSeverity.WARNING,
|
||||
data={"entry_id": entry.entry_id},
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
},
|
||||
"issues": {
|
||||
"migrate_to_v2_api": {
|
||||
"title": "Update the authentication method for {title}",
|
||||
"title": "Update authentication method",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2.6.0"]
|
||||
"requirements": ["aioautomower==2.5.0"]
|
||||
}
|
||||
|
||||
@@ -445,8 +445,7 @@
|
||||
"main_area": "Main area",
|
||||
"secondary_area": "Secondary area",
|
||||
"home": "Home",
|
||||
"demo": "Demo",
|
||||
"poi": "Point of interest"
|
||||
"demo": "Demo"
|
||||
}
|
||||
},
|
||||
"uptime": {
|
||||
|
||||
@@ -18,7 +18,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONFIG_DEFAULT_MAX_TEMP, CONFIG_DEFAULT_MIN_TEMP
|
||||
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
|
||||
from .entity import HuumBaseEntity
|
||||
|
||||
@@ -56,12 +55,12 @@ class HuumDevice(HuumBaseEntity, ClimateEntity):
|
||||
@property
|
||||
def min_temp(self) -> int:
|
||||
"""Return configured minimal temperature."""
|
||||
return self.coordinator.data.sauna_config.min_temp or CONFIG_DEFAULT_MIN_TEMP
|
||||
return self.coordinator.data.sauna_config.min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self) -> int:
|
||||
"""Return configured maximum temperature."""
|
||||
return self.coordinator.data.sauna_config.max_temp or CONFIG_DEFAULT_MAX_TEMP
|
||||
return self.coordinator.data.sauna_config.max_temp
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
|
||||
@@ -9,6 +9,3 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT, Platform.
|
||||
CONFIG_STEAMER = 1
|
||||
CONFIG_LIGHT = 2
|
||||
CONFIG_STEAMER_AND_LIGHT = 3
|
||||
|
||||
CONFIG_DEFAULT_MIN_TEMP = 40
|
||||
CONFIG_DEFAULT_MAX_TEMP = 110
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
"""The iNELS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from inelsmqtt import InelsMqtt
|
||||
from inelsmqtt.devices import Device
|
||||
from inelsmqtt.discovery import InelsDiscovery
|
||||
|
||||
from homeassistant.components import mqtt as ha_mqtt
|
||||
from homeassistant.components.mqtt import (
|
||||
ReceiveMessage,
|
||||
async_prepare_subscribe_topics,
|
||||
async_subscribe_topics,
|
||||
async_unsubscribe_topics,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import LOGGER, PLATFORMS
|
||||
|
||||
type InelsConfigEntry = ConfigEntry[InelsData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class InelsData:
|
||||
"""Represents the data structure for INELS runtime data."""
|
||||
|
||||
mqtt: InelsMqtt
|
||||
devices: list[Device]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: InelsConfigEntry) -> bool:
|
||||
"""Set up iNELS from a config entry."""
|
||||
|
||||
async def mqtt_publish(topic: str, payload: str, qos: int, retain: bool) -> None:
|
||||
"""Publish an MQTT message using the Home Assistant MQTT client."""
|
||||
await ha_mqtt.async_publish(hass, topic, payload, qos, retain)
|
||||
|
||||
async def mqtt_subscribe(
|
||||
sub_state: dict[str, Any] | None,
|
||||
topic: str,
|
||||
callback_func: Callable[[str, str], None],
|
||||
) -> dict[str, Any]:
|
||||
"""Subscribe to MQTT topics using the Home Assistant MQTT client."""
|
||||
|
||||
@callback
|
||||
def mqtt_message_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle iNELS mqtt messages."""
|
||||
# Payload is always str at runtime since we don't set encoding=None
|
||||
# HA uses UTF-8 by default
|
||||
callback_func(msg.topic, msg.payload) # type: ignore[arg-type]
|
||||
|
||||
topics = {
|
||||
"inels_subscribe_topic": {
|
||||
"topic": topic,
|
||||
"msg_callback": mqtt_message_received,
|
||||
}
|
||||
}
|
||||
|
||||
sub_state = async_prepare_subscribe_topics(hass, sub_state, topics)
|
||||
await async_subscribe_topics(hass, sub_state)
|
||||
return sub_state
|
||||
|
||||
async def mqtt_unsubscribe(sub_state: dict[str, Any]) -> None:
|
||||
async_unsubscribe_topics(hass, sub_state)
|
||||
|
||||
if not await ha_mqtt.async_wait_for_mqtt_client(hass):
|
||||
LOGGER.error("MQTT integration not available")
|
||||
raise ConfigEntryNotReady("MQTT integration not available")
|
||||
|
||||
inels_mqtt = InelsMqtt(mqtt_publish, mqtt_subscribe, mqtt_unsubscribe)
|
||||
devices: list[Device] = await InelsDiscovery(inels_mqtt).start()
|
||||
|
||||
# If no devices are discovered, continue with the setup
|
||||
if not devices:
|
||||
LOGGER.info("No devices discovered")
|
||||
|
||||
entry.runtime_data = InelsData(mqtt=inels_mqtt, devices=devices)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: InelsConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.mqtt.unsubscribe_topics()
|
||||
entry.runtime_data.mqtt.unsubscribe_listeners()
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -1,73 +0,0 @@
|
||||
"""Config flow for iNELS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
|
||||
|
||||
from .const import DOMAIN, TITLE
|
||||
|
||||
|
||||
class INelsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle of iNELS config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_mqtt(
|
||||
self, discovery_info: MqttServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by MQTT discovery."""
|
||||
if self._async_in_progress():
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
|
||||
# Validate the message, abort if it fails.
|
||||
if not discovery_info.topic.endswith("/gw"):
|
||||
# Not an iNELS discovery message.
|
||||
return self.async_abort(reason="invalid_discovery_info")
|
||||
if not discovery_info.payload:
|
||||
# Empty payload, unexpected payload.
|
||||
return self.async_abort(reason="invalid_discovery_info")
|
||||
|
||||
return await self.async_step_confirm_from_mqtt()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
try:
|
||||
if not mqtt.is_connected(self.hass):
|
||||
return self.async_abort(reason="mqtt_not_connected")
|
||||
except KeyError:
|
||||
return self.async_abort(reason="mqtt_not_configured")
|
||||
|
||||
return await self.async_step_confirm_from_user()
|
||||
|
||||
async def step_confirm(
|
||||
self, step_id: str, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the setup."""
|
||||
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(DOMAIN)
|
||||
return self.async_create_entry(title=TITLE, data={})
|
||||
|
||||
return self.async_show_form(step_id=step_id)
|
||||
|
||||
async def async_step_confirm_from_mqtt(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the setup from MQTT discovered."""
|
||||
return await self.step_confirm(
|
||||
step_id="confirm_from_mqtt", user_input=user_input
|
||||
)
|
||||
|
||||
async def async_step_confirm_from_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the setup from user add integration."""
|
||||
return await self.step_confirm(
|
||||
step_id="confirm_from_user", user_input=user_input
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
"""Constants for the iNELS integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "inels"
|
||||
TITLE = "iNELS"
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
@@ -1,61 +0,0 @@
|
||||
"""Base class for iNELS components."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from inelsmqtt.devices import Device
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class InelsBaseEntity(Entity):
|
||||
"""Base iNELS entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
key: str,
|
||||
index: int,
|
||||
) -> None:
|
||||
"""Init base entity."""
|
||||
self._device = device
|
||||
self._device_id = device.unique_id
|
||||
self._attr_unique_id = self._device_id
|
||||
|
||||
# The referenced variable to read from
|
||||
self._key = key
|
||||
# The index of the variable list to read from. '-1' for no index
|
||||
self._index = index
|
||||
|
||||
info = device.info()
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
manufacturer=info.manufacturer,
|
||||
model=info.model_number,
|
||||
name=device.title,
|
||||
sw_version=info.sw_version,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Add subscription of the data listener."""
|
||||
# Register the HA callback
|
||||
self._device.add_ha_callback(self._key, self._index, self._callback)
|
||||
# Subscribe to MQTT updates
|
||||
self._device.mqtt.subscribe_listener(
|
||||
self._device.state_topic, self._device.unique_id, self._device.callback
|
||||
)
|
||||
|
||||
def _callback(self) -> None:
|
||||
"""Get data from broker into the HA."""
|
||||
if hasattr(self, "hass"):
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return self._device.is_available
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"switch": {
|
||||
"bit": {
|
||||
"default": "mdi:power-socket-eu"
|
||||
},
|
||||
"simple_relay": {
|
||||
"default": "mdi:power-socket-eu"
|
||||
},
|
||||
"relay": {
|
||||
"default": "mdi:power-socket-eu"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"domain": "inels",
|
||||
"name": "iNELS",
|
||||
"codeowners": ["@epdevlab"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["mqtt"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/inels",
|
||||
"iot_class": "local_push",
|
||||
"mqtt": ["inels/status/#"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["elkoep-aio-mqtt==0.1.0b4"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
config-flow: done
|
||||
test-before-configure: done
|
||||
unique-config-entry: done
|
||||
config-flow-test-coverage: done
|
||||
runtime-data: done
|
||||
test-before-setup:
|
||||
status: done
|
||||
comment: >
|
||||
Raise "Invalid authentication" and "MQTT Broker is offline or
|
||||
cannot be reached" otherwise, async_setup_entry returns False
|
||||
appropriate-polling:
|
||||
status: done
|
||||
comment: |
|
||||
Integration uses local_push.
|
||||
entity-unique-id:
|
||||
status: done
|
||||
comment: |
|
||||
{MAC}_{DEVICE_ID} is used, for example, 0e97f8b7d30_02E8.
|
||||
has-entity-name:
|
||||
status: done
|
||||
comment: >
|
||||
Almost all devices are multi-functional, which means that all functions
|
||||
are equally important -> keep the descriptive name (not setting _attr_name to None).
|
||||
entity-event-setup:
|
||||
status: done
|
||||
comment: |
|
||||
Subscribe in async_added_to_hass & unsubscribe from async_unload_entry.
|
||||
dependency-transparency: done
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
common-modules: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions:
|
||||
status: done
|
||||
comment: |
|
||||
A link to the wiki is provided.
|
||||
docs-removal-instructions: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
brands: done
|
||||
# Silver
|
||||
config-entry-unloading: done
|
||||
log-when-unavailable: todo
|
||||
entity-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
available property.
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
reauthentication-flow: todo
|
||||
parallel-updates:
|
||||
status: todo
|
||||
comment: |
|
||||
For all platforms, add a constant PARALLEL_UPDATES = 0.
|
||||
test-coverage: done
|
||||
integration-owner: done
|
||||
docs-installation-parameters:
|
||||
status: done
|
||||
comment: |
|
||||
A link to the wiki is provided.
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: >
|
||||
There is the same options flow in the integration as there is in the
|
||||
configuration.
|
||||
|
||||
# Gold
|
||||
entity-translations: done
|
||||
entity-device-class: todo
|
||||
devices: done
|
||||
entity-category: todo
|
||||
entity-disabled-by-default: todo
|
||||
discovery:
|
||||
status: todo
|
||||
comment: |
|
||||
Currently blocked by a hw limitation.
|
||||
stale-devices:
|
||||
status: todo
|
||||
comment: >
|
||||
Same as discovery. The async_remove_config_entry_device function should be
|
||||
implemented at a minimum.
|
||||
diagnostics: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
dynamic-devices: todo
|
||||
discovery-update-info:
|
||||
status: todo
|
||||
comment: |
|
||||
Same as discovery.
|
||||
repair-issues: todo
|
||||
docs-use-cases: todo
|
||||
docs-supported-devices:
|
||||
status: todo
|
||||
comment: >
|
||||
In regards to this and below doc requirements, I am not sure whether the
|
||||
wiki link is acceptable.
|
||||
docs-supported-functions: todo
|
||||
docs-data-update: todo
|
||||
docs-known-limitations: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-examples: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration is not making any HTTP requests.
|
||||
strict-typing: todo
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"confirm_from_user": {
|
||||
"description": "iNELS devices must be connected to the same broker as the Home Assistant MQTT integration client. Continue setup?"
|
||||
},
|
||||
"confirm_from_mqtt": {
|
||||
"description": "Do you want to set up iNELS?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"mqtt_not_connected": "Home Assistant MQTT integration not connected to MQTT broker.",
|
||||
"mqtt_not_configured": "Home Assistant MQTT integration not configured.",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"bit": {
|
||||
"name": "Bit{addr}"
|
||||
},
|
||||
"simple_relay": {
|
||||
"name": "Simple relay{index}"
|
||||
},
|
||||
"relay": {
|
||||
"name": "Relay{index}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
"""iNELS switch entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from inelsmqtt.devices import Device
|
||||
from inelsmqtt.utils.common import Bit, Relay, SimpleRelay
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import InelsConfigEntry
|
||||
from .entity import InelsBaseEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class InelsSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Class describing iNELS switch entities."""
|
||||
|
||||
get_state_fn: Callable[[Device, int], Bit | SimpleRelay | Relay]
|
||||
alerts: list[str] | None = None
|
||||
placeholder_fn: Callable[[Device, int, bool], dict[str, str]]
|
||||
|
||||
|
||||
SWITCH_TYPES = [
|
||||
InelsSwitchEntityDescription(
|
||||
key="bit",
|
||||
translation_key="bit",
|
||||
get_state_fn=lambda device, index: device.state.bit[index],
|
||||
placeholder_fn=lambda device, index, indexed: {
|
||||
"addr": f" {device.state.bit[index].addr}"
|
||||
},
|
||||
),
|
||||
InelsSwitchEntityDescription(
|
||||
key="simple_relay",
|
||||
translation_key="simple_relay",
|
||||
get_state_fn=lambda device, index: device.state.simple_relay[index],
|
||||
placeholder_fn=lambda device, index, indexed: {
|
||||
"index": f" {index + 1}" if indexed else ""
|
||||
},
|
||||
),
|
||||
InelsSwitchEntityDescription(
|
||||
key="relay",
|
||||
translation_key="relay",
|
||||
get_state_fn=lambda device, index: device.state.relay[index],
|
||||
alerts=["overflow"],
|
||||
placeholder_fn=lambda device, index, indexed: {
|
||||
"index": f" {index + 1}" if indexed else ""
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: InelsConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Load iNELS switch."""
|
||||
entities: list[InelsSwitch] = []
|
||||
|
||||
for device in entry.runtime_data.devices:
|
||||
for description in SWITCH_TYPES:
|
||||
if hasattr(device.state, description.key):
|
||||
switch_count = len(getattr(device.state, description.key))
|
||||
entities.extend(
|
||||
InelsSwitch(
|
||||
device=device,
|
||||
description=description,
|
||||
index=idx,
|
||||
switch_count=switch_count,
|
||||
)
|
||||
for idx in range(switch_count)
|
||||
)
|
||||
|
||||
async_add_entities(entities, False)
|
||||
|
||||
|
||||
class InelsSwitch(InelsBaseEntity, SwitchEntity):
|
||||
"""The platform class required by Home Assistant."""
|
||||
|
||||
entity_description: InelsSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
description: InelsSwitchEntityDescription,
|
||||
index: int = 0,
|
||||
switch_count: int = 1,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device=device, key=description.key, index=index)
|
||||
self.entity_description = description
|
||||
self._switch_count = switch_count
|
||||
|
||||
# Include index in unique_id for devices with multiple switches
|
||||
unique_key = f"{description.key}{index}" if index else description.key
|
||||
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{unique_key}".lower()
|
||||
|
||||
# Set translation placeholders
|
||||
self._attr_translation_placeholders = self.entity_description.placeholder_fn(
|
||||
self._device, self._index, self._switch_count > 1
|
||||
)
|
||||
|
||||
def _check_alerts(self, current_state: Bit | SimpleRelay | Relay) -> None:
|
||||
"""Check if there are active alerts and raise ServiceValidationError if found."""
|
||||
if self.entity_description.alerts and any(
|
||||
getattr(current_state, alert_key, None)
|
||||
for alert_key in self.entity_description.alerts
|
||||
):
|
||||
raise ServiceValidationError("Cannot operate switch with active alerts")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return if switch is on."""
|
||||
current_state = self.entity_description.get_state_fn(self._device, self._index)
|
||||
return current_state.is_on
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the switch to turn off."""
|
||||
current_state = self.entity_description.get_state_fn(self._device, self._index)
|
||||
self._check_alerts(current_state)
|
||||
current_state.is_on = False
|
||||
await self._device.set_ha_value(self._device.state)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the switch to turn on."""
|
||||
current_state = self.entity_description.get_state_fn(self._device, self._index)
|
||||
self._check_alerts(current_state)
|
||||
current_state.is_on = True
|
||||
await self._device.set_ha_value(self._device.state)
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/israel_rail",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["israelrailapi"],
|
||||
"requirements": ["israel-rail-api==0.1.4"]
|
||||
"requirements": ["israel-rail-api==0.1.3"]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ from xknx.devices import (
|
||||
)
|
||||
from xknx.devices.fan import FanSpeedMode
|
||||
from xknx.dpt.dpt_20 import HVACControllerMode, HVACOperationMode
|
||||
from xknx.remote_value.remote_value_setpoint_shift import SetpointShiftMode
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.climate import (
|
||||
@@ -35,53 +34,13 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_SYNC_STATE,
|
||||
CONTROLLER_MODES,
|
||||
CURRENT_HVAC_ACTIONS,
|
||||
DOMAIN,
|
||||
KNX_MODULE_KEY,
|
||||
ClimateConf,
|
||||
)
|
||||
from .entity import (
|
||||
KnxUiEntity,
|
||||
KnxUiEntityPlatformController,
|
||||
KnxYamlEntity,
|
||||
_KnxEntityBase,
|
||||
)
|
||||
from .const import CONTROLLER_MODES, CURRENT_HVAC_ACTIONS, KNX_MODULE_KEY
|
||||
from .entity import KnxYamlEntity
|
||||
from .knx_module import KNXModule
|
||||
from .schema import ClimateSchema
|
||||
from .storage.const import (
|
||||
CONF_ENTITY,
|
||||
CONF_GA_ACTIVE,
|
||||
CONF_GA_CONTROLLER_MODE,
|
||||
CONF_GA_CONTROLLER_STATUS,
|
||||
CONF_GA_FAN_SPEED,
|
||||
CONF_GA_FAN_SWING,
|
||||
CONF_GA_FAN_SWING_HORIZONTAL,
|
||||
CONF_GA_HEAT_COOL,
|
||||
CONF_GA_HUMIDITY_CURRENT,
|
||||
CONF_GA_ON_OFF,
|
||||
CONF_GA_OP_MODE_COMFORT,
|
||||
CONF_GA_OP_MODE_ECO,
|
||||
CONF_GA_OP_MODE_PROTECTION,
|
||||
CONF_GA_OP_MODE_STANDBY,
|
||||
CONF_GA_OPERATION_MODE,
|
||||
CONF_GA_SETPOINT_SHIFT,
|
||||
CONF_GA_TEMPERATURE_CURRENT,
|
||||
CONF_GA_TEMPERATURE_TARGET,
|
||||
CONF_GA_VALVE,
|
||||
CONF_IGNORE_AUTO_MODE,
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
)
|
||||
from .storage.entity_store_schema import ConfClimateFanSpeedMode, ConfSetpointShiftMode
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
ATTR_COMMAND_VALUE = "command_value"
|
||||
CONTROLLER_MODES_INV = {value: key for key, value in CONTROLLER_MODES.items()}
|
||||
@@ -94,29 +53,11 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up climate(s) for KNX platform."""
|
||||
knx_module = hass.data[KNX_MODULE_KEY]
|
||||
platform = async_get_current_platform()
|
||||
knx_module.config_store.add_platform(
|
||||
platform=Platform.CLIMATE,
|
||||
controller=KnxUiEntityPlatformController(
|
||||
knx_module=knx_module,
|
||||
entity_platform=platform,
|
||||
entity_class=KnxUiClimate,
|
||||
),
|
||||
)
|
||||
config: list[ConfigType] = knx_module.config_yaml[Platform.CLIMATE]
|
||||
|
||||
entities: list[KnxYamlEntity | KnxUiEntity] = []
|
||||
if yaml_platform_config := knx_module.config_yaml.get(Platform.CLIMATE):
|
||||
entities.extend(
|
||||
KnxYamlClimate(knx_module, entity_config)
|
||||
for entity_config in yaml_platform_config
|
||||
)
|
||||
if ui_config := knx_module.config_store.data["entities"].get(Platform.CLIMATE):
|
||||
entities.extend(
|
||||
KnxUiClimate(knx_module, unique_id, config)
|
||||
for unique_id, config in ui_config.items()
|
||||
)
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
KNXClimate(knx_module, entity_config) for entity_config in config
|
||||
)
|
||||
|
||||
|
||||
def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
|
||||
@@ -158,8 +99,8 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
|
||||
group_address_heat_cool_state=config.get(
|
||||
ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS
|
||||
),
|
||||
operation_modes=config.get(ClimateConf.OPERATION_MODES),
|
||||
controller_modes=config.get(ClimateConf.CONTROLLER_MODES),
|
||||
operation_modes=config.get(ClimateSchema.CONF_OPERATION_MODES),
|
||||
controller_modes=config.get(ClimateSchema.CONF_CONTROLLER_MODES),
|
||||
)
|
||||
|
||||
return XknxClimate(
|
||||
@@ -179,24 +120,24 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
|
||||
ClimateSchema.CONF_SETPOINT_SHIFT_STATE_ADDRESS
|
||||
),
|
||||
setpoint_shift_mode=config.get(ClimateSchema.CONF_SETPOINT_SHIFT_MODE),
|
||||
setpoint_shift_max=config[ClimateConf.SETPOINT_SHIFT_MAX],
|
||||
setpoint_shift_min=config[ClimateConf.SETPOINT_SHIFT_MIN],
|
||||
temperature_step=config[ClimateConf.TEMPERATURE_STEP],
|
||||
setpoint_shift_max=config[ClimateSchema.CONF_SETPOINT_SHIFT_MAX],
|
||||
setpoint_shift_min=config[ClimateSchema.CONF_SETPOINT_SHIFT_MIN],
|
||||
temperature_step=config[ClimateSchema.CONF_TEMPERATURE_STEP],
|
||||
group_address_on_off=config.get(ClimateSchema.CONF_ON_OFF_ADDRESS),
|
||||
group_address_on_off_state=config.get(ClimateSchema.CONF_ON_OFF_STATE_ADDRESS),
|
||||
on_off_invert=config[ClimateConf.ON_OFF_INVERT],
|
||||
on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT],
|
||||
group_address_active_state=config.get(ClimateSchema.CONF_ACTIVE_STATE_ADDRESS),
|
||||
group_address_command_value_state=config.get(
|
||||
ClimateSchema.CONF_COMMAND_VALUE_STATE_ADDRESS
|
||||
),
|
||||
min_temp=config.get(ClimateConf.MIN_TEMP),
|
||||
max_temp=config.get(ClimateConf.MAX_TEMP),
|
||||
min_temp=config.get(ClimateSchema.CONF_MIN_TEMP),
|
||||
max_temp=config.get(ClimateSchema.CONF_MAX_TEMP),
|
||||
mode=climate_mode,
|
||||
group_address_fan_speed=config.get(ClimateSchema.CONF_FAN_SPEED_ADDRESS),
|
||||
group_address_fan_speed_state=config.get(
|
||||
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS
|
||||
),
|
||||
fan_speed_mode=config[ClimateConf.FAN_SPEED_MODE],
|
||||
fan_speed_mode=config[ClimateSchema.CONF_FAN_SPEED_MODE],
|
||||
group_address_swing=config.get(ClimateSchema.CONF_SWING_ADDRESS),
|
||||
group_address_swing_state=config.get(ClimateSchema.CONF_SWING_STATE_ADDRESS),
|
||||
group_address_horizontal_swing=config.get(
|
||||
@@ -211,195 +152,91 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
|
||||
)
|
||||
|
||||
|
||||
def _create_climate_ui(xknx: XKNX, conf: ConfigExtractor, name: str) -> XknxClimate:
|
||||
"""Return a KNX Climate device to be used within XKNX from UI config."""
|
||||
sync_state = conf.get(CONF_SYNC_STATE)
|
||||
op_modes: list[str | HVACOperationMode] = list(HVACOperationMode)
|
||||
if conf.get(CONF_IGNORE_AUTO_MODE):
|
||||
op_modes.remove(HVACOperationMode.AUTO)
|
||||
|
||||
climate_mode = XknxClimateMode(
|
||||
xknx,
|
||||
name=f"{name} Mode",
|
||||
group_address_operation_mode=conf.get_write(CONF_GA_OPERATION_MODE),
|
||||
group_address_operation_mode_state=conf.get_state_and_passive(
|
||||
CONF_GA_OPERATION_MODE
|
||||
),
|
||||
group_address_operation_mode_comfort=conf.get_write_and_passive(
|
||||
CONF_GA_OP_MODE_COMFORT
|
||||
),
|
||||
group_address_operation_mode_economy=conf.get_write_and_passive(
|
||||
CONF_GA_OP_MODE_ECO
|
||||
),
|
||||
group_address_operation_mode_protection=conf.get_write_and_passive(
|
||||
CONF_GA_OP_MODE_PROTECTION
|
||||
),
|
||||
group_address_operation_mode_standby=conf.get_write_and_passive(
|
||||
CONF_GA_OP_MODE_STANDBY
|
||||
),
|
||||
group_address_controller_status=conf.get_write(CONF_GA_CONTROLLER_STATUS),
|
||||
group_address_controller_status_state=conf.get_state_and_passive(
|
||||
CONF_GA_CONTROLLER_STATUS
|
||||
),
|
||||
group_address_controller_mode=conf.get_write(CONF_GA_CONTROLLER_MODE),
|
||||
group_address_controller_mode_state=conf.get_state_and_passive(
|
||||
CONF_GA_CONTROLLER_MODE
|
||||
),
|
||||
group_address_heat_cool=conf.get_write(CONF_GA_HEAT_COOL),
|
||||
group_address_heat_cool_state=conf.get_state_and_passive(CONF_GA_HEAT_COOL),
|
||||
sync_state=sync_state,
|
||||
operation_modes=op_modes,
|
||||
)
|
||||
|
||||
sps_mode = None
|
||||
if _sps_dpt := conf.get_dpt(CONF_TARGET_TEMPERATURE, CONF_GA_SETPOINT_SHIFT):
|
||||
sps_mode = (
|
||||
SetpointShiftMode.DPT6010
|
||||
if _sps_dpt == ConfSetpointShiftMode.COUNT
|
||||
else SetpointShiftMode.DPT9002
|
||||
)
|
||||
_fan_speed_dpt = conf.get_dpt(CONF_GA_FAN_SPEED)
|
||||
fan_speed_mode = (
|
||||
FanSpeedMode.STEP
|
||||
if _fan_speed_dpt == ConfClimateFanSpeedMode.STEPS
|
||||
else FanSpeedMode.PERCENT
|
||||
)
|
||||
|
||||
return XknxClimate(
|
||||
xknx,
|
||||
name=name,
|
||||
group_address_temperature=conf.get_state_and_passive(
|
||||
CONF_GA_TEMPERATURE_CURRENT
|
||||
),
|
||||
group_address_target_temperature=conf.get_write(
|
||||
CONF_TARGET_TEMPERATURE, CONF_GA_TEMPERATURE_TARGET
|
||||
),
|
||||
group_address_target_temperature_state=conf.get_state_and_passive(
|
||||
CONF_TARGET_TEMPERATURE, CONF_GA_TEMPERATURE_TARGET
|
||||
),
|
||||
group_address_setpoint_shift=conf.get_write(
|
||||
CONF_TARGET_TEMPERATURE, CONF_GA_SETPOINT_SHIFT
|
||||
),
|
||||
group_address_setpoint_shift_state=conf.get_state_and_passive(
|
||||
CONF_TARGET_TEMPERATURE, CONF_GA_SETPOINT_SHIFT
|
||||
),
|
||||
setpoint_shift_mode=sps_mode,
|
||||
setpoint_shift_max=conf.get(
|
||||
CONF_TARGET_TEMPERATURE, ClimateConf.SETPOINT_SHIFT_MAX, default=6
|
||||
),
|
||||
setpoint_shift_min=conf.get(
|
||||
CONF_TARGET_TEMPERATURE, ClimateConf.SETPOINT_SHIFT_MIN, default=-6
|
||||
),
|
||||
temperature_step=conf.get(
|
||||
CONF_TARGET_TEMPERATURE, ClimateConf.TEMPERATURE_STEP, default=0.1
|
||||
),
|
||||
group_address_on_off=conf.get_write(CONF_GA_ON_OFF),
|
||||
group_address_on_off_state=conf.get_state_and_passive(CONF_GA_ON_OFF),
|
||||
on_off_invert=conf.get(ClimateConf.ON_OFF_INVERT, default=False),
|
||||
group_address_active_state=conf.get_state_and_passive(CONF_GA_ACTIVE),
|
||||
group_address_command_value_state=conf.get_state_and_passive(CONF_GA_VALVE),
|
||||
sync_state=sync_state,
|
||||
min_temp=conf.get(ClimateConf.MIN_TEMP),
|
||||
max_temp=conf.get(ClimateConf.MAX_TEMP),
|
||||
mode=climate_mode,
|
||||
group_address_fan_speed=conf.get_write(CONF_GA_FAN_SPEED),
|
||||
group_address_fan_speed_state=conf.get_state_and_passive(CONF_GA_FAN_SPEED),
|
||||
fan_speed_mode=fan_speed_mode,
|
||||
group_address_humidity_state=conf.get_state_and_passive(
|
||||
CONF_GA_HUMIDITY_CURRENT
|
||||
),
|
||||
group_address_swing=conf.get_write(CONF_GA_FAN_SWING),
|
||||
group_address_swing_state=conf.get_state_and_passive(CONF_GA_FAN_SWING),
|
||||
group_address_horizontal_swing=conf.get_write(CONF_GA_FAN_SWING_HORIZONTAL),
|
||||
group_address_horizontal_swing_state=conf.get_state_and_passive(
|
||||
CONF_GA_FAN_SWING_HORIZONTAL
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class _KnxClimate(ClimateEntity, _KnxEntityBase):
|
||||
class KNXClimate(KnxYamlEntity, ClimateEntity):
|
||||
"""Representation of a KNX climate device."""
|
||||
|
||||
_device: XknxClimate
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_translation_key = "knx_climate"
|
||||
|
||||
default_hvac_mode: HVACMode
|
||||
_last_hvac_mode: HVACMode
|
||||
fan_zero_mode: str
|
||||
_fan_modes_percentages: list[int]
|
||||
|
||||
def _init_from_device_config(
|
||||
self,
|
||||
device: XknxClimate,
|
||||
default_hvac_mode: HVACMode,
|
||||
fan_max_step: int,
|
||||
fan_zero_mode: str,
|
||||
) -> None:
|
||||
"""Set attributes that depend on device config."""
|
||||
self.default_hvac_mode = default_hvac_mode
|
||||
# non-OFF HVAC mode to be used when turning on the device without on_off address
|
||||
self._last_hvac_mode = self.default_hvac_mode
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize of a KNX climate device."""
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=_create_climate(knx_module.xknx, config),
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
if device.supports_on_off:
|
||||
if self._device.supports_on_off:
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
if (
|
||||
device.mode is not None
|
||||
and len(device.mode.controller_modes) >= 2
|
||||
and HVACControllerMode.OFF in device.mode.controller_modes
|
||||
self._device.mode is not None
|
||||
and len(self._device.mode.controller_modes) >= 2
|
||||
and HVACControllerMode.OFF in self._device.mode.controller_modes
|
||||
):
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
|
||||
if (
|
||||
device.mode is not None
|
||||
and device.mode.operation_modes # empty list when not writable
|
||||
self._device.mode is not None
|
||||
and self._device.mode.operation_modes # empty list when not writable
|
||||
):
|
||||
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
||||
self._attr_preset_modes = [
|
||||
mode.name.lower() for mode in device.mode.operation_modes
|
||||
mode.name.lower() for mode in self._device.mode.operation_modes
|
||||
]
|
||||
|
||||
self.fan_zero_mode = fan_zero_mode
|
||||
fan_max_step = config[ClimateSchema.CONF_FAN_MAX_STEP]
|
||||
self._fan_modes_percentages = [
|
||||
int(100 * i / fan_max_step) for i in range(fan_max_step + 1)
|
||||
]
|
||||
if device.fan_speed is not None and device.fan_speed.initialized:
|
||||
self.fan_zero_mode: str = config[ClimateSchema.CONF_FAN_ZERO_MODE]
|
||||
|
||||
if self._device.fan_speed is not None and self._device.fan_speed.initialized:
|
||||
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
|
||||
|
||||
if fan_max_step == 3:
|
||||
self._attr_fan_modes = [
|
||||
fan_zero_mode,
|
||||
self.fan_zero_mode,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
FAN_HIGH,
|
||||
]
|
||||
elif fan_max_step == 2:
|
||||
self._attr_fan_modes = [fan_zero_mode, FAN_LOW, FAN_HIGH]
|
||||
self._attr_fan_modes = [self.fan_zero_mode, FAN_LOW, FAN_HIGH]
|
||||
elif fan_max_step == 1:
|
||||
self._attr_fan_modes = [fan_zero_mode, FAN_ON]
|
||||
elif device.fan_speed_mode == FanSpeedMode.STEP:
|
||||
self._attr_fan_modes = [fan_zero_mode] + [
|
||||
self._attr_fan_modes = [self.fan_zero_mode, FAN_ON]
|
||||
elif self._device.fan_speed_mode == FanSpeedMode.STEP:
|
||||
self._attr_fan_modes = [self.fan_zero_mode] + [
|
||||
str(i) for i in range(1, fan_max_step + 1)
|
||||
]
|
||||
else:
|
||||
self._attr_fan_modes = [fan_zero_mode] + [
|
||||
self._attr_fan_modes = [self.fan_zero_mode] + [
|
||||
f"{percentage}%" for percentage in self._fan_modes_percentages[1:]
|
||||
]
|
||||
|
||||
if device.swing.initialized:
|
||||
if self._device.swing.initialized:
|
||||
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
|
||||
self._attr_swing_modes = [SWING_ON, SWING_OFF]
|
||||
|
||||
if device.horizontal_swing.initialized:
|
||||
if self._device.horizontal_swing.initialized:
|
||||
self._attr_supported_features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
|
||||
self._attr_swing_horizontal_modes = [SWING_ON, SWING_OFF]
|
||||
|
||||
self._attr_target_temperature_step = device.temperature_step
|
||||
self._attr_target_temperature_step = self._device.temperature_step
|
||||
self._attr_unique_id = (
|
||||
f"{self._device.temperature.group_address_state}_"
|
||||
f"{self._device.target_temperature.group_address_state}_"
|
||||
f"{self._device.target_temperature.group_address}_"
|
||||
f"{self._device._setpoint_shift.group_address}" # noqa: SLF001
|
||||
)
|
||||
self.default_hvac_mode: HVACMode = config[
|
||||
ClimateSchema.CONF_DEFAULT_CONTROLLER_MODE
|
||||
]
|
||||
# non-OFF HVAC mode to be used when turning on the device without on_off address
|
||||
self._last_hvac_mode: HVACMode = self.default_hvac_mode
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
@@ -638,63 +475,3 @@ class _KnxClimate(ClimateEntity, _KnxEntityBase):
|
||||
if hvac_mode is not HVACMode.OFF:
|
||||
self._last_hvac_mode = hvac_mode
|
||||
super().after_update_callback(device)
|
||||
|
||||
|
||||
class KnxYamlClimate(_KnxClimate, KnxYamlEntity):
|
||||
"""Representation of a KNX climate device configured from YAML."""
|
||||
|
||||
_device: XknxClimate
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize of a KNX climate device."""
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=_create_climate(knx_module.xknx, config),
|
||||
)
|
||||
default_hvac_mode: HVACMode = config[ClimateConf.DEFAULT_CONTROLLER_MODE]
|
||||
fan_max_step = config[ClimateConf.FAN_MAX_STEP]
|
||||
fan_zero_mode: str = config[ClimateConf.FAN_ZERO_MODE]
|
||||
self._init_from_device_config(
|
||||
device=self._device,
|
||||
default_hvac_mode=default_hvac_mode,
|
||||
fan_max_step=fan_max_step,
|
||||
fan_zero_mode=fan_zero_mode,
|
||||
)
|
||||
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = (
|
||||
f"{self._device.temperature.group_address_state}_"
|
||||
f"{self._device.target_temperature.group_address_state}_"
|
||||
f"{self._device.target_temperature.group_address}_"
|
||||
f"{self._device._setpoint_shift.group_address}" # noqa: SLF001
|
||||
)
|
||||
|
||||
|
||||
class KnxUiClimate(_KnxClimate, KnxUiEntity):
|
||||
"""Representation of a KNX climate device configured from the UI."""
|
||||
|
||||
_device: XknxClimate
|
||||
|
||||
def __init__(
|
||||
self, knx_module: KNXModule, unique_id: str, config: ConfigType
|
||||
) -> None:
|
||||
"""Initialize of a KNX climate device."""
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
||||
self._device = _create_climate_ui(
|
||||
knx_module.xknx, knx_conf, config[CONF_ENTITY][CONF_NAME]
|
||||
)
|
||||
|
||||
default_hvac_mode = HVACMode(knx_conf.get(ClimateConf.DEFAULT_CONTROLLER_MODE))
|
||||
fan_max_step = knx_conf.get(ClimateConf.FAN_MAX_STEP)
|
||||
fan_zero_mode = knx_conf.get(ClimateConf.FAN_ZERO_MODE)
|
||||
self._init_from_device_config(
|
||||
device=self._device,
|
||||
default_hvac_mode=default_hvac_mode,
|
||||
fan_max_step=fan_max_step,
|
||||
fan_zero_mode=fan_zero_mode,
|
||||
)
|
||||
|
||||
@@ -160,7 +160,6 @@ SUPPORTED_PLATFORMS_YAML: Final = {
|
||||
|
||||
SUPPORTED_PLATFORMS_UI: Final = {
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.SWITCH,
|
||||
@@ -194,23 +193,3 @@ class CoverConf:
|
||||
INVERT_UPDOWN: Final = "invert_updown"
|
||||
INVERT_POSITION: Final = "invert_position"
|
||||
INVERT_ANGLE: Final = "invert_angle"
|
||||
|
||||
|
||||
class ClimateConf:
|
||||
"""Common config keys for climate."""
|
||||
|
||||
MIN_TEMP: Final = "min_temp"
|
||||
MAX_TEMP: Final = "max_temp"
|
||||
TEMPERATURE_STEP: Final = "temperature_step"
|
||||
SETPOINT_SHIFT_MAX: Final = "setpoint_shift_max"
|
||||
SETPOINT_SHIFT_MIN: Final = "setpoint_shift_min"
|
||||
|
||||
ON_OFF_INVERT: Final = "on_off_invert"
|
||||
|
||||
OPERATION_MODES: Final = "operation_modes"
|
||||
CONTROLLER_MODES: Final = "controller_modes"
|
||||
DEFAULT_CONTROLLER_MODE: Final = "default_controller_mode"
|
||||
|
||||
FAN_MAX_STEP: Final = "fan_max_step"
|
||||
FAN_SPEED_MODE: Final = "fan_speed_mode"
|
||||
FAN_ZERO_MODE: Final = "fan_zero_mode"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.10.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.10.26.81530"
|
||||
"knx-frontend==2025.10.17.202411"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ from .const import (
|
||||
CONF_STATE_ADDRESS,
|
||||
CONF_SYNC_STATE,
|
||||
KNX_ADDRESS,
|
||||
ClimateConf,
|
||||
ColorTempModes,
|
||||
CoverConf,
|
||||
FanZeroMode,
|
||||
@@ -307,7 +306,10 @@ class ClimateSchema(KNXPlatformSchema):
|
||||
CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address"
|
||||
CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address"
|
||||
CONF_SETPOINT_SHIFT_MODE = "setpoint_shift_mode"
|
||||
CONF_SETPOINT_SHIFT_MAX = "setpoint_shift_max"
|
||||
CONF_SETPOINT_SHIFT_MIN = "setpoint_shift_min"
|
||||
CONF_TEMPERATURE_ADDRESS = "temperature_address"
|
||||
CONF_TEMPERATURE_STEP = "temperature_step"
|
||||
CONF_TARGET_TEMPERATURE_ADDRESS = "target_temperature_address"
|
||||
CONF_TARGET_TEMPERATURE_STATE_ADDRESS = "target_temperature_state_address"
|
||||
CONF_OPERATION_MODE_ADDRESS = "operation_mode_address"
|
||||
@@ -325,10 +327,19 @@ class ClimateSchema(KNXPlatformSchema):
|
||||
CONF_OPERATION_MODE_NIGHT_ADDRESS = "operation_mode_night_address"
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address"
|
||||
CONF_OPERATION_MODE_STANDBY_ADDRESS = "operation_mode_standby_address"
|
||||
CONF_OPERATION_MODES = "operation_modes"
|
||||
CONF_CONTROLLER_MODES = "controller_modes"
|
||||
CONF_DEFAULT_CONTROLLER_MODE = "default_controller_mode"
|
||||
CONF_ON_OFF_ADDRESS = "on_off_address"
|
||||
CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address"
|
||||
CONF_ON_OFF_INVERT = "on_off_invert"
|
||||
CONF_MIN_TEMP = "min_temp"
|
||||
CONF_MAX_TEMP = "max_temp"
|
||||
CONF_FAN_SPEED_ADDRESS = "fan_speed_address"
|
||||
CONF_FAN_SPEED_STATE_ADDRESS = "fan_speed_state_address"
|
||||
CONF_FAN_MAX_STEP = "fan_max_step"
|
||||
CONF_FAN_SPEED_MODE = "fan_speed_mode"
|
||||
CONF_FAN_ZERO_MODE = "fan_zero_mode"
|
||||
CONF_HUMIDITY_STATE_ADDRESS = "humidity_state_address"
|
||||
CONF_SWING_ADDRESS = "swing_address"
|
||||
CONF_SWING_STATE_ADDRESS = "swing_state_address"
|
||||
@@ -348,13 +359,13 @@ class ClimateSchema(KNXPlatformSchema):
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(
|
||||
ClimateConf.SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX
|
||||
CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX
|
||||
): vol.All(int, vol.Range(min=0, max=32)),
|
||||
vol.Optional(
|
||||
ClimateConf.SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN
|
||||
CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN
|
||||
): vol.All(int, vol.Range(min=-32, max=0)),
|
||||
vol.Optional(
|
||||
ClimateConf.TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP
|
||||
CONF_TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP
|
||||
): vol.All(float, vol.Range(min=0, max=2)),
|
||||
vol.Required(CONF_TEMPERATURE_ADDRESS): ga_list_validator,
|
||||
vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): ga_list_validator,
|
||||
@@ -397,29 +408,29 @@ class ClimateSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_ON_OFF_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ON_OFF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(
|
||||
ClimateConf.ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT
|
||||
CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT
|
||||
): cv.boolean,
|
||||
vol.Optional(ClimateConf.OPERATION_MODES): vol.All(
|
||||
vol.Optional(CONF_OPERATION_MODES): vol.All(
|
||||
cv.ensure_list,
|
||||
[backwards_compatible_xknx_climate_enum_member(HVACOperationMode)],
|
||||
),
|
||||
vol.Optional(ClimateConf.CONTROLLER_MODES): vol.All(
|
||||
vol.Optional(CONF_CONTROLLER_MODES): vol.All(
|
||||
cv.ensure_list,
|
||||
[backwards_compatible_xknx_climate_enum_member(HVACControllerMode)],
|
||||
),
|
||||
vol.Optional(
|
||||
ClimateConf.DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT
|
||||
CONF_DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT
|
||||
): vol.Coerce(HVACMode),
|
||||
vol.Optional(ClimateConf.MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(ClimateConf.MAX_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_FAN_SPEED_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_FAN_SPEED_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(ClimateConf.FAN_MAX_STEP, default=3): cv.byte,
|
||||
vol.Optional(CONF_FAN_MAX_STEP, default=3): cv.byte,
|
||||
vol.Optional(
|
||||
ClimateConf.FAN_SPEED_MODE, default=DEFAULT_FAN_SPEED_MODE
|
||||
CONF_FAN_SPEED_MODE, default=DEFAULT_FAN_SPEED_MODE
|
||||
): vol.All(vol.Upper, cv.enum(FanSpeedMode)),
|
||||
vol.Optional(ClimateConf.FAN_ZERO_MODE, default=FAN_OFF): vol.Coerce(
|
||||
vol.Optional(CONF_FAN_ZERO_MODE, default=FAN_OFF): vol.Coerce(
|
||||
FanZeroMode
|
||||
),
|
||||
vol.Optional(CONF_SWING_ADDRESS): ga_list_validator,
|
||||
|
||||
@@ -39,10 +39,6 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DESCRIPTION_PLACEHOLDERS = {
|
||||
"sensor_value_types_url": "https://www.home-assistant.io/integrations/knx/#value-types"
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
@@ -52,7 +48,6 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
SERVICE_KNX_SEND,
|
||||
service_send_to_knx_bus,
|
||||
schema=SERVICE_KNX_SEND_SCHEMA,
|
||||
description_placeholders=_DESCRIPTION_PLACEHOLDERS,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
@@ -68,7 +63,6 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
SERVICE_KNX_EVENT_REGISTER,
|
||||
service_event_register_modify,
|
||||
schema=SERVICE_KNX_EVENT_REGISTER_SCHEMA,
|
||||
description_placeholders=_DESCRIPTION_PLACEHOLDERS,
|
||||
)
|
||||
|
||||
async_register_admin_service(
|
||||
@@ -77,7 +71,6 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
SERVICE_KNX_EXPOSURE_REGISTER,
|
||||
service_exposure_register_modify,
|
||||
schema=SERVICE_KNX_EXPOSURE_REGISTER_SCHEMA,
|
||||
description_placeholders=_DESCRIPTION_PLACEHOLDERS,
|
||||
)
|
||||
|
||||
async_register_admin_service(
|
||||
|
||||
@@ -14,28 +14,6 @@ CONF_DPT: Final = "dpt"
|
||||
CONF_GA_SENSOR: Final = "ga_sensor"
|
||||
CONF_GA_SWITCH: Final = "ga_switch"
|
||||
|
||||
# Climate
|
||||
CONF_GA_TEMPERATURE_CURRENT: Final = "ga_temperature_current"
|
||||
CONF_GA_HUMIDITY_CURRENT: Final = "ga_humidity_current"
|
||||
CONF_TARGET_TEMPERATURE: Final = "target_temperature"
|
||||
CONF_GA_TEMPERATURE_TARGET: Final = "ga_temperature_target"
|
||||
CONF_GA_SETPOINT_SHIFT: Final = "ga_setpoint_shift"
|
||||
CONF_GA_ACTIVE: Final = "ga_active"
|
||||
CONF_GA_VALVE: Final = "ga_valve"
|
||||
CONF_GA_OPERATION_MODE: Final = "ga_operation_mode"
|
||||
CONF_IGNORE_AUTO_MODE: Final = "ignore_auto_mode"
|
||||
CONF_GA_OP_MODE_COMFORT: Final = "ga_operation_mode_comfort"
|
||||
CONF_GA_OP_MODE_ECO: Final = "ga_operation_mode_economy"
|
||||
CONF_GA_OP_MODE_STANDBY: Final = "ga_operation_mode_standby"
|
||||
CONF_GA_OP_MODE_PROTECTION: Final = "ga_operation_mode_protection"
|
||||
CONF_GA_HEAT_COOL: Final = "ga_heat_cool"
|
||||
CONF_GA_ON_OFF: Final = "ga_on_off"
|
||||
CONF_GA_CONTROLLER_MODE: Final = "ga_controller_mode"
|
||||
CONF_GA_CONTROLLER_STATUS: Final = "ga_controller_status"
|
||||
CONF_GA_FAN_SPEED: Final = "ga_fan_speed"
|
||||
CONF_GA_FAN_SWING: Final = "ga_fan_swing"
|
||||
CONF_GA_FAN_SWING_HORIZONTAL: Final = "ga_fan_swing_horizontal"
|
||||
|
||||
# Cover
|
||||
CONF_GA_UP_DOWN: Final = "ga_up_down"
|
||||
CONF_GA_STOP: Final = "ga_stop"
|
||||
|
||||
@@ -4,7 +4,6 @@ from enum import StrEnum, unique
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import HVACMode
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ENTITY_ID,
|
||||
@@ -25,10 +24,8 @@ from ..const import (
|
||||
CONF_SYNC_STATE,
|
||||
DOMAIN,
|
||||
SUPPORTED_PLATFORMS_UI,
|
||||
ClimateConf,
|
||||
ColorTempModes,
|
||||
CoverConf,
|
||||
FanZeroMode,
|
||||
)
|
||||
from .const import (
|
||||
CONF_COLOR,
|
||||
@@ -37,47 +34,27 @@ from .const import (
|
||||
CONF_DATA,
|
||||
CONF_DEVICE_INFO,
|
||||
CONF_ENTITY,
|
||||
CONF_GA_ACTIVE,
|
||||
CONF_GA_ANGLE,
|
||||
CONF_GA_BLUE_BRIGHTNESS,
|
||||
CONF_GA_BLUE_SWITCH,
|
||||
CONF_GA_BRIGHTNESS,
|
||||
CONF_GA_COLOR,
|
||||
CONF_GA_COLOR_TEMP,
|
||||
CONF_GA_CONTROLLER_MODE,
|
||||
CONF_GA_CONTROLLER_STATUS,
|
||||
CONF_GA_FAN_SPEED,
|
||||
CONF_GA_FAN_SWING,
|
||||
CONF_GA_FAN_SWING_HORIZONTAL,
|
||||
CONF_GA_GREEN_BRIGHTNESS,
|
||||
CONF_GA_GREEN_SWITCH,
|
||||
CONF_GA_HEAT_COOL,
|
||||
CONF_GA_HUE,
|
||||
CONF_GA_HUMIDITY_CURRENT,
|
||||
CONF_GA_ON_OFF,
|
||||
CONF_GA_OP_MODE_COMFORT,
|
||||
CONF_GA_OP_MODE_ECO,
|
||||
CONF_GA_OP_MODE_PROTECTION,
|
||||
CONF_GA_OP_MODE_STANDBY,
|
||||
CONF_GA_OPERATION_MODE,
|
||||
CONF_GA_POSITION_SET,
|
||||
CONF_GA_POSITION_STATE,
|
||||
CONF_GA_RED_BRIGHTNESS,
|
||||
CONF_GA_RED_SWITCH,
|
||||
CONF_GA_SATURATION,
|
||||
CONF_GA_SENSOR,
|
||||
CONF_GA_SETPOINT_SHIFT,
|
||||
CONF_GA_STEP,
|
||||
CONF_GA_STOP,
|
||||
CONF_GA_SWITCH,
|
||||
CONF_GA_TEMPERATURE_CURRENT,
|
||||
CONF_GA_TEMPERATURE_TARGET,
|
||||
CONF_GA_UP_DOWN,
|
||||
CONF_GA_VALVE,
|
||||
CONF_GA_WHITE_BRIGHTNESS,
|
||||
CONF_GA_WHITE_SWITCH,
|
||||
CONF_IGNORE_AUTO_MODE,
|
||||
CONF_TARGET_TEMPERATURE,
|
||||
)
|
||||
from .knx_selector import (
|
||||
AllSerializeFirst,
|
||||
@@ -132,9 +109,7 @@ BINARY_SENSOR_KNX_SCHEMA = vol.Schema(
|
||||
min=0, max=600, step=0.1, unit_of_measurement="s"
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_SYNC_STATE, default=True): SyncStateSelector(
|
||||
allow_false=True
|
||||
),
|
||||
vol.Required(CONF_SYNC_STATE, default=True): SyncStateSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -336,151 +311,8 @@ SWITCH_KNX_SCHEMA = vol.Schema(
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@unique
|
||||
class ConfSetpointShiftMode(StrEnum):
|
||||
"""Enum for setpoint shift mode."""
|
||||
|
||||
COUNT = "6.010"
|
||||
FLOAT = "9.002"
|
||||
|
||||
|
||||
@unique
|
||||
class ConfClimateFanSpeedMode(StrEnum):
|
||||
"""Enum for climate fan speed mode."""
|
||||
|
||||
PERCENTAGE = "5.001"
|
||||
STEPS = "5.010"
|
||||
|
||||
|
||||
CLIMATE_KNX_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_GA_TEMPERATURE_CURRENT): GASelector(
|
||||
write=False, state_required=True, valid_dpt="9.001"
|
||||
),
|
||||
vol.Optional(CONF_GA_HUMIDITY_CURRENT): GASelector(
|
||||
write=False, valid_dpt="9.002"
|
||||
),
|
||||
vol.Required(CONF_TARGET_TEMPERATURE): GroupSelect(
|
||||
GroupSelectOption(
|
||||
translation_key="group_direct_temp",
|
||||
schema={
|
||||
vol.Required(CONF_GA_TEMPERATURE_TARGET): GASelector(
|
||||
write_required=True, valid_dpt="9.001"
|
||||
),
|
||||
vol.Required(
|
||||
ClimateConf.MIN_TEMP, default=7
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=-20, max=80, step=1, unit_of_measurement="°C"
|
||||
)
|
||||
),
|
||||
vol.Required(
|
||||
ClimateConf.MAX_TEMP, default=28
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0, max=100, step=1, unit_of_measurement="°C"
|
||||
)
|
||||
),
|
||||
vol.Required(
|
||||
ClimateConf.TEMPERATURE_STEP, default=0.1
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0.1, max=2, step=0.1, unit_of_measurement="K"
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
GroupSelectOption(
|
||||
translation_key="group_setpoint_shift",
|
||||
schema={
|
||||
vol.Required(CONF_GA_TEMPERATURE_TARGET): GASelector(
|
||||
write=False, state_required=True, valid_dpt="9.001"
|
||||
),
|
||||
vol.Required(CONF_GA_SETPOINT_SHIFT): GASelector(
|
||||
write_required=True,
|
||||
state_required=True,
|
||||
dpt=ConfSetpointShiftMode,
|
||||
),
|
||||
vol.Required(
|
||||
ClimateConf.SETPOINT_SHIFT_MIN, default=-6
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=-32, max=0, step=1, unit_of_measurement="K"
|
||||
)
|
||||
),
|
||||
vol.Required(
|
||||
ClimateConf.SETPOINT_SHIFT_MAX, default=6
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0, max=32, step=1, unit_of_measurement="K"
|
||||
)
|
||||
),
|
||||
vol.Required(
|
||||
ClimateConf.TEMPERATURE_STEP, default=0.1
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0.1, max=2, step=0.1, unit_of_measurement="K"
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
collapsible=False,
|
||||
),
|
||||
"section_activity": KNXSectionFlat(collapsible=True),
|
||||
vol.Optional(CONF_GA_ACTIVE): GASelector(write=False, valid_dpt="1"),
|
||||
vol.Optional(CONF_GA_VALVE): GASelector(write=False, valid_dpt="5.001"),
|
||||
"section_operation_mode": KNXSectionFlat(collapsible=True),
|
||||
vol.Optional(CONF_GA_OPERATION_MODE): GASelector(valid_dpt="20.102"),
|
||||
vol.Optional(CONF_IGNORE_AUTO_MODE): selector.BooleanSelector(),
|
||||
"section_operation_mode_individual": KNXSectionFlat(collapsible=True),
|
||||
vol.Optional(CONF_GA_OP_MODE_COMFORT): GASelector(state=False, valid_dpt="1"),
|
||||
vol.Optional(CONF_GA_OP_MODE_ECO): GASelector(state=False, valid_dpt="1"),
|
||||
vol.Optional(CONF_GA_OP_MODE_STANDBY): GASelector(state=False, valid_dpt="1"),
|
||||
vol.Optional(CONF_GA_OP_MODE_PROTECTION): GASelector(
|
||||
state=False, valid_dpt="1"
|
||||
),
|
||||
"section_heat_cool": KNXSectionFlat(collapsible=True),
|
||||
vol.Optional(CONF_GA_HEAT_COOL): GASelector(valid_dpt="1.100"),
|
||||
"section_on_off": KNXSectionFlat(collapsible=True),
|
||||
vol.Optional(CONF_GA_ON_OFF): GASelector(valid_dpt="1"),
|
||||
vol.Optional(ClimateConf.ON_OFF_INVERT): selector.BooleanSelector(),
|
||||
"section_controller_mode": KNXSectionFlat(collapsible=True),
|
||||
vol.Optional(CONF_GA_CONTROLLER_MODE): GASelector(valid_dpt="20.105"),
|
||||
vol.Optional(CONF_GA_CONTROLLER_STATUS): GASelector(write=False),
|
||||
vol.Required(
|
||||
ClimateConf.DEFAULT_CONTROLLER_MODE, default=HVACMode.HEAT
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=list(HVACMode),
|
||||
translation_key="component.climate.selector.hvac_mode",
|
||||
)
|
||||
),
|
||||
"section_fan": KNXSectionFlat(collapsible=True),
|
||||
vol.Optional(CONF_GA_FAN_SPEED): GASelector(dpt=ConfClimateFanSpeedMode),
|
||||
vol.Required(ClimateConf.FAN_MAX_STEP, default=3): AllSerializeFirst(
|
||||
selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(min=1, max=100, step=1)
|
||||
),
|
||||
vol.Coerce(int),
|
||||
),
|
||||
vol.Required(
|
||||
ClimateConf.FAN_ZERO_MODE, default=FanZeroMode.OFF
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=list(FanZeroMode),
|
||||
translation_key="component.knx.config_panel.entities.create.climate.knx.fan_zero_mode",
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_GA_FAN_SWING): GASelector(valid_dpt="1"),
|
||||
vol.Optional(CONF_GA_FAN_SWING_HORIZONTAL): GASelector(valid_dpt="1"),
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
KNX_SCHEMA_FOR_PLATFORM = {
|
||||
Platform.BINARY_SENSOR: BINARY_SENSOR_KNX_SCHEMA,
|
||||
Platform.CLIMATE: CLIMATE_KNX_SCHEMA,
|
||||
Platform.COVER: COVER_KNX_SCHEMA,
|
||||
Platform.LIGHT: LIGHT_KNX_SCHEMA,
|
||||
Platform.SWITCH: SWITCH_KNX_SCHEMA,
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
},
|
||||
"type": {
|
||||
"name": "Value type",
|
||||
"description": "If set, the payload will not be sent as raw bytes, but encoded as given DPT. KNX sensor types are valid values (see {sensor_value_types_url})."
|
||||
"description": "If set, the payload will not be sent as raw bytes, but encoded as given DPT. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)."
|
||||
},
|
||||
"response": {
|
||||
"name": "Send as Response",
|
||||
@@ -297,7 +297,7 @@
|
||||
},
|
||||
"type": {
|
||||
"name": "Value type",
|
||||
"description": "If set, the payload will be decoded as given DPT in the event data `value` key. KNX sensor types are valid values (see {sensor_value_types_url})."
|
||||
"description": "If set, the payload will be decoded as given DPT in the event data `value` key. KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)."
|
||||
},
|
||||
"remove": {
|
||||
"name": "Remove event registration",
|
||||
@@ -315,7 +315,7 @@
|
||||
},
|
||||
"type": {
|
||||
"name": "Value type",
|
||||
"description": "Telegrams will be encoded as given DPT. 'binary' and all KNX sensor types are valid values (see {sensor_value_types_url})."
|
||||
"description": "Telegrams will be encoded as given DPT. 'binary' and all KNX sensor types are valid values (see https://www.home-assistant.io/integrations/knx/#value-types)."
|
||||
},
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -412,164 +412,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"description": "The KNX climate platform is used as an interface to heating actuators, HVAC gateways, etc.",
|
||||
"knx": {
|
||||
"ga_temperature_current": {
|
||||
"label": "Current temperature"
|
||||
},
|
||||
"ga_humidity_current": {
|
||||
"label": "Current humidity"
|
||||
},
|
||||
"target_temperature": {
|
||||
"title": "Target temperature",
|
||||
"description": "Set the target temperature.",
|
||||
"options": {
|
||||
"group_direct_temp": {
|
||||
"label": "Absolute setpoint",
|
||||
"description": "Set the target temperature by an absolute value."
|
||||
},
|
||||
"group_setpoint_shift": {
|
||||
"label": "Setpoint shift",
|
||||
"description": "Shift the target temperature from a base setpoint."
|
||||
}
|
||||
},
|
||||
"ga_temperature_target": {
|
||||
"label": "Target temperature",
|
||||
"description": "Current absolute target temperature."
|
||||
},
|
||||
"min_temp": {
|
||||
"label": "Minimum temperature",
|
||||
"description": "Minimum temperature that can be set."
|
||||
},
|
||||
"max_temp": {
|
||||
"label": "Maximum temperature",
|
||||
"description": "Maximum temperature that can be set."
|
||||
},
|
||||
"temperature_step": {
|
||||
"label": "Temperature step",
|
||||
"description": "Smallest step size to change the temperature. For setpoint shift configurations this sets the scale factor of the shift value."
|
||||
},
|
||||
"ga_setpoint_shift": {
|
||||
"label": "Setpoint shift",
|
||||
"description": "Target temperature deviation from a base setpoint."
|
||||
},
|
||||
"setpoint_shift_min": {
|
||||
"label": "Minimum setpoint shift",
|
||||
"description": "Lowest allowed deviation from the base setpoint."
|
||||
},
|
||||
"setpoint_shift_max": {
|
||||
"label": "Maximum setpoint shift",
|
||||
"description": "Highest allowed deviation from the base setpoint."
|
||||
}
|
||||
},
|
||||
"section_activity": {
|
||||
"title": "Activity",
|
||||
"description": "Determine if the device is active or idle."
|
||||
},
|
||||
"ga_active": {
|
||||
"label": "Active",
|
||||
"description": "Binary value indicating if the device is active or idle. If configured, this takes precedence over valve position."
|
||||
},
|
||||
"ga_valve": {
|
||||
"label": "Valve position",
|
||||
"description": "Current control value / valve position in percent. `0` sets the climate entity to idle."
|
||||
},
|
||||
"section_operation_mode": {
|
||||
"title": "Operation mode",
|
||||
"description": "Set the preset mode of the device."
|
||||
},
|
||||
"ga_operation_mode": {
|
||||
"label": "Operation mode",
|
||||
"description": "Current operation mode."
|
||||
},
|
||||
"ignore_auto_mode": {
|
||||
"label": "Ignore auto mode",
|
||||
"description": "Enable when your controller doesn't support `auto` mode. It will be ignored by the integration then."
|
||||
},
|
||||
"section_operation_mode_individual": {
|
||||
"title": "Individual operation modes",
|
||||
"description": "Set the preset mode of the device using individual group addresses."
|
||||
},
|
||||
"ga_operation_mode_comfort": {
|
||||
"label": "Comfort mode"
|
||||
},
|
||||
"ga_operation_mode_economy": {
|
||||
"label": "Economy mode"
|
||||
},
|
||||
"ga_operation_mode_standby": {
|
||||
"label": "Standby mode"
|
||||
},
|
||||
"ga_operation_mode_protection": {
|
||||
"label": "Building protection mode"
|
||||
},
|
||||
"section_heat_cool": {
|
||||
"title": "Heating/Cooling",
|
||||
"description": "Set whether the device is in heating or cooling mode."
|
||||
},
|
||||
"ga_heat_cool": {
|
||||
"label": "Heating/Cooling"
|
||||
},
|
||||
"section_on_off": {
|
||||
"title": "On/Off",
|
||||
"description": "Turn the device on or off."
|
||||
},
|
||||
"ga_on_off": {
|
||||
"label": "On/Off"
|
||||
},
|
||||
"on_off_invert": {
|
||||
"label": "[%key:component::knx::config_panel::entities::create::binary_sensor::knx::invert::label%]",
|
||||
"description": "[%key:component::knx::config_panel::entities::create::binary_sensor::knx::invert::description%]"
|
||||
},
|
||||
"section_controller_mode": {
|
||||
"title": "Controller mode",
|
||||
"description": "Set the mode of the climate device."
|
||||
},
|
||||
"ga_controller_mode": {
|
||||
"label": "Controller mode"
|
||||
},
|
||||
"ga_controller_status": {
|
||||
"label": "Controller status",
|
||||
"description": "HVAC controller mode and preset status. Eberle Status octet (KNX AN 097/07 rev 3) non-standardized DPT."
|
||||
},
|
||||
"default_controller_mode": {
|
||||
"label": "Default mode",
|
||||
"description": "Climate mode to be set on initialization."
|
||||
},
|
||||
"section_fan": {
|
||||
"title": "Fan",
|
||||
"description": "Configuration for fan control (AC units)."
|
||||
},
|
||||
"ga_fan_speed": {
|
||||
"label": "Fan speed",
|
||||
"description": "Set the current fan speed.",
|
||||
"options": {
|
||||
"5_001": "Percent",
|
||||
"5_010": "Steps"
|
||||
}
|
||||
},
|
||||
"fan_max_step": {
|
||||
"label": "Fan steps",
|
||||
"description": "The maximum amount of steps for the fan."
|
||||
},
|
||||
"fan_zero_mode": {
|
||||
"label": "Zero fan speed mode",
|
||||
"description": "Set the mode that represents fan speed `0`.",
|
||||
"options": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"auto": "[%key:common::state::auto%]"
|
||||
}
|
||||
},
|
||||
"ga_fan_swing": {
|
||||
"label": "Fan swing",
|
||||
"description": "Toggle (vertical) fan swing mode. Use this if only one direction is supported."
|
||||
},
|
||||
"ga_fan_swing_horizontal": {
|
||||
"label": "Fan horizontal swing",
|
||||
"description": "Toggle horizontal fan swing mode."
|
||||
}
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"description": "The KNX cover platform is used as an interface to shutter actuators.",
|
||||
"knx": {
|
||||
|
||||
@@ -18,7 +18,6 @@ from homeassistant.const import (
|
||||
CONF_SOURCE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfSpeed,
|
||||
@@ -51,7 +50,6 @@ DEVICE_CLASS_MAPPING = {
|
||||
pypck.lcn_defs.VarUnit.VOLT: SensorDeviceClass.VOLTAGE,
|
||||
pypck.lcn_defs.VarUnit.AMPERE: SensorDeviceClass.CURRENT,
|
||||
pypck.lcn_defs.VarUnit.PPM: SensorDeviceClass.CO2,
|
||||
pypck.lcn_defs.VarUnit.PERCENT: SensorDeviceClass.HUMIDITY,
|
||||
}
|
||||
|
||||
UNIT_OF_MEASUREMENT_MAPPING = {
|
||||
@@ -64,7 +62,6 @@ UNIT_OF_MEASUREMENT_MAPPING = {
|
||||
pypck.lcn_defs.VarUnit.VOLT: UnitOfElectricPotential.VOLT,
|
||||
pypck.lcn_defs.VarUnit.AMPERE: UnitOfElectricCurrent.AMPERE,
|
||||
pypck.lcn_defs.VarUnit.PPM: CONCENTRATION_PARTS_PER_MILLION,
|
||||
pypck.lcn_defs.VarUnit.PERCENT: PERCENTAGE,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,17 +5,11 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
brightness_supported,
|
||||
)
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.color import brightness_to_value, value_to_brightness
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LunatoneConfigEntry, LunatoneDevicesDataUpdateCoordinator
|
||||
@@ -48,10 +42,8 @@ class LunatoneLight(
|
||||
):
|
||||
"""Representation of a Lunatone light."""
|
||||
|
||||
BRIGHTNESS_SCALE = (1, 100)
|
||||
|
||||
_last_brightness = 255
|
||||
|
||||
_attr_color_mode = ColorMode.ONOFF
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_should_poll = False
|
||||
@@ -90,25 +82,6 @@ class LunatoneLight(
|
||||
"""Return True if light is on."""
|
||||
return self._device is not None and self._device.is_on
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
if self._device is None:
|
||||
return 0
|
||||
return value_to_brightness(self.BRIGHTNESS_SCALE, self._device.brightness)
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode:
|
||||
"""Return the color mode of the light."""
|
||||
if self._device is not None and self._device.is_dimmable:
|
||||
return ColorMode.BRIGHTNESS
|
||||
return ColorMode.ONOFF
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[ColorMode]:
|
||||
"""Return the supported color modes."""
|
||||
return {self.color_mode}
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
@@ -118,27 +91,13 @@ class LunatoneLight(
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on."""
|
||||
assert self._device
|
||||
|
||||
if brightness_supported(self.supported_color_modes):
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness)
|
||||
await self._device.fade_to_brightness(
|
||||
brightness_to_value(self.BRIGHTNESS_SCALE, brightness)
|
||||
)
|
||||
else:
|
||||
await self._device.switch_on()
|
||||
|
||||
await self._device.switch_on()
|
||||
await asyncio.sleep(STATUS_UPDATE_DELAY)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn off."""
|
||||
assert self._device
|
||||
|
||||
if brightness_supported(self.supported_color_modes):
|
||||
self._last_brightness = self.brightness
|
||||
await self._device.fade_to_brightness(0)
|
||||
else:
|
||||
await self._device.switch_off()
|
||||
|
||||
await self._device.switch_off()
|
||||
await asyncio.sleep(STATUS_UPDATE_DELAY)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -20,14 +20,13 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .adapter import MatterAdapter
|
||||
from .addon import get_addon_manager
|
||||
@@ -41,13 +40,10 @@ from .helpers import (
|
||||
node_from_ha_device_id,
|
||||
)
|
||||
from .models import MatterDeviceInfo
|
||||
from .services import async_setup_services
|
||||
|
||||
CONNECT_TIMEOUT = 10
|
||||
LISTEN_READY_TIMEOUT = 30
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@callback
|
||||
@cache
|
||||
@@ -68,12 +64,6 @@ def get_matter_device_info(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Matter integration services."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Matter from a config entry."""
|
||||
if use_addon := entry.data.get(CONF_USE_ADDON):
|
||||
|
||||
@@ -155,18 +155,4 @@ DISCOVERY_SCHEMAS = [
|
||||
required_attributes=(clusters.SmokeCoAlarm.Attributes.AcceptedCommandList,),
|
||||
value_contains=clusters.SmokeCoAlarm.Commands.SelfTestRequest.command_id,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.BUTTON,
|
||||
entity_description=MatterButtonEntityDescription(
|
||||
key="WaterHeaterManagementCancelBoost",
|
||||
translation_key="cancel_boost",
|
||||
command=clusters.WaterHeaterManagement.Commands.CancelBoost,
|
||||
),
|
||||
entity_class=MatterCommandButton,
|
||||
required_attributes=(
|
||||
clusters.WaterHeaterManagement.Attributes.AcceptedCommandList,
|
||||
),
|
||||
value_contains=clusters.WaterHeaterManagement.Commands.CancelBoost.command_id,
|
||||
allow_multi=True, # Also used in water_heater
|
||||
),
|
||||
]
|
||||
|
||||
@@ -123,9 +123,6 @@
|
||||
"evse_fault_state": {
|
||||
"default": "mdi:ev-station"
|
||||
},
|
||||
"operational_error": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"pump_control_mode": {
|
||||
"default": "mdi:pipe-wrench"
|
||||
},
|
||||
@@ -163,10 +160,5 @@
|
||||
"default": "mdi:shield-lock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"water_heater_boost": {
|
||||
"service": "mdi:water-boiler"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +86,6 @@ OPERATIONAL_STATE_MAP = {
|
||||
clusters.OperationalState.Enums.OperationalStateEnum.kError: "error",
|
||||
}
|
||||
|
||||
OPERATIONAL_STATE_ERROR_MAP = {
|
||||
# enum with known Error state values which we can translate
|
||||
clusters.OperationalState.Enums.ErrorStateEnum.kNoError: "no_error",
|
||||
clusters.OperationalState.Enums.ErrorStateEnum.kUnableToStartOrResume: "unable_to_start_or_resume",
|
||||
clusters.OperationalState.Enums.ErrorStateEnum.kUnableToCompleteOperation: "unable_to_complete_operation",
|
||||
clusters.OperationalState.Enums.ErrorStateEnum.kCommandInvalidInState: "command_invalid_in_state",
|
||||
}
|
||||
|
||||
RVC_OPERATIONAL_STATE_MAP = {
|
||||
# enum with known Operation state values which we can translate
|
||||
**OPERATIONAL_STATE_MAP,
|
||||
@@ -102,29 +94,6 @@ RVC_OPERATIONAL_STATE_MAP = {
|
||||
clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked",
|
||||
}
|
||||
|
||||
RVC_OPERATIONAL_STATE_ERROR_MAP = {
|
||||
# enum with known Error state values which we can translate
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kNoError: "no_error",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kUnableToStartOrResume: "unable_to_start_or_resume",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kUnableToCompleteOperation: "unable_to_complete_operation",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kCommandInvalidInState: "command_invalid_in_state",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kFailedToFindChargingDock: "failed_to_find_charging_dock",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kStuck: "stuck",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kDustBinMissing: "dust_bin_missing",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kDustBinFull: "dust_bin_full",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kWaterTankEmpty: "water_tank_empty",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kWaterTankMissing: "water_tank_missing",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kWaterTankLidOpen: "water_tank_lid_open",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kMopCleaningPadMissing: "mop_cleaning_pad_missing",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kLowBattery: "low_battery",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kCannotReachTargetArea: "cannot_reach_target_area",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kDirtyWaterTankFull: "dirty_water_tank_full",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kDirtyWaterTankMissing: "dirty_water_tank_missing",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kWheelsJammed: "wheels_jammed",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kBrushJammed: "brush_jammed",
|
||||
clusters.RvcOperationalState.Enums.ErrorStateEnum.kNavigationSensorObscured: "navigation_sensor_obscured",
|
||||
}
|
||||
|
||||
BOOST_STATE_MAP = {
|
||||
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kInactive: "inactive",
|
||||
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive: "active",
|
||||
@@ -1132,19 +1101,6 @@ DISCOVERY_SCHEMAS = [
|
||||
# don't discover this entry if the supported state list is empty
|
||||
secondary_value_is_not=[],
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="OperationalStateOperationalError",
|
||||
translation_key="operational_error",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(OPERATIONAL_STATE_ERROR_MAP.values()),
|
||||
device_to_ha=lambda x: OPERATIONAL_STATE_ERROR_MAP.get(x.errorStateID),
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.OperationalState.Attributes.OperationalError,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterListSensorEntityDescription(
|
||||
@@ -1189,19 +1145,6 @@ DISCOVERY_SCHEMAS = [
|
||||
device_type=(device_types.Thermostat,),
|
||||
allow_multi=True, # also used for climate entity
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="ThermostatPIHeatingDemand",
|
||||
translation_key="pi_heating_demand",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.Thermostat.Attributes.PIHeatingDemand,),
|
||||
device_type=(device_types.Thermostat,),
|
||||
featuremap_contains=clusters.Thermostat.Bitmaps.Feature.kHeating,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
@@ -1238,19 +1181,6 @@ DISCOVERY_SCHEMAS = [
|
||||
# don't discover this entry if the supported state list is empty
|
||||
secondary_value_is_not=[],
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="RvcOperationalStateOperationalError",
|
||||
translation_key="operational_error",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(RVC_OPERATIONAL_STATE_ERROR_MAP.values()),
|
||||
device_to_ha=lambda x: RVC_OPERATIONAL_STATE_ERROR_MAP.get(x.errorStateID),
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.RvcOperationalState.Attributes.OperationalError,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterOperationalStateSensorEntityDescription(
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Services for Matter devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
ATTR_DURATION = "duration"
|
||||
ATTR_EMERGENCY_BOOST = "emergency_boost"
|
||||
ATTR_TEMPORARY_SETPOINT = "temporary_setpoint"
|
||||
|
||||
SERVICE_WATER_HEATER_BOOST = "water_heater_boost"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the Matter services."""
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_WATER_HEATER_BOOST,
|
||||
entity_domain=WATER_HEATER_DOMAIN,
|
||||
schema={
|
||||
# duration >=1
|
||||
vol.Required(ATTR_DURATION): vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(ATTR_EMERGENCY_BOOST): cv.boolean,
|
||||
vol.Optional(ATTR_TEMPORARY_SETPOINT): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=30, max=65)
|
||||
),
|
||||
},
|
||||
func="async_set_boost",
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
water_heater_boost:
|
||||
target:
|
||||
entity:
|
||||
domain: water_heater
|
||||
fields:
|
||||
duration:
|
||||
selector:
|
||||
number:
|
||||
min: 60
|
||||
max: 14400
|
||||
step: 60
|
||||
mode: box
|
||||
default: 3600
|
||||
required: true
|
||||
emergency_boost:
|
||||
selector:
|
||||
boolean:
|
||||
default: false
|
||||
temporary_setpoint:
|
||||
selector:
|
||||
number:
|
||||
min: 30
|
||||
max: 65
|
||||
step: 1
|
||||
mode: slider
|
||||
default: 65
|
||||
@@ -106,9 +106,6 @@
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"cancel_boost": {
|
||||
"name": "Cancel boost"
|
||||
},
|
||||
"pause": {
|
||||
"name": "[%key:common::action::pause%]"
|
||||
},
|
||||
@@ -444,33 +441,6 @@
|
||||
"evse_soc": {
|
||||
"name": "State of charge"
|
||||
},
|
||||
"operational_error": {
|
||||
"name": "Operational error",
|
||||
"state": {
|
||||
"no_error": "No error",
|
||||
"unable_to_start_or_resume": "Unable to start or resume",
|
||||
"unable_to_complete_operation": "Unable to complete operation",
|
||||
"command_invalid_in_state": "Command invalid in current state",
|
||||
"failed_to_find_charging_dock": "Failed to find charging dock",
|
||||
"stuck": "Stuck",
|
||||
"dust_bin_missing": "Dust bin missing",
|
||||
"dust_bin_full": "Dust bin full",
|
||||
"water_tank_empty": "Water tank empty",
|
||||
"water_tank_missing": "Water tank missing",
|
||||
"water_tank_lid_open": "Water tank lid open",
|
||||
"mop_cleaning_pad_missing": "Mop cleaning pad missing",
|
||||
"low_battery": "Low battery",
|
||||
"cannot_reach_target_area": "Cannot reach target area",
|
||||
"dirty_water_tank_full": "Dirty water tank full",
|
||||
"dirty_water_tank_missing": "Dirty water tank missing",
|
||||
"wheels_jammed": "Wheels jammed",
|
||||
"brush_jammed": "Brush jammed",
|
||||
"navigation_sensor_obscured": "Navigation sensor obscured"
|
||||
}
|
||||
},
|
||||
"pi_heating_demand": {
|
||||
"name": "Heating demand"
|
||||
},
|
||||
"nitrogen_dioxide": {
|
||||
"name": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]"
|
||||
},
|
||||
@@ -593,24 +563,6 @@
|
||||
"description": "The Matter device to add to the other Matter network."
|
||||
}
|
||||
}
|
||||
},
|
||||
"water_heater_boost": {
|
||||
"name": "Boost water heater",
|
||||
"description": "Enables water heater boost for a specific duration.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "Duration",
|
||||
"description": "Boost duration"
|
||||
},
|
||||
"emergency_boost": {
|
||||
"name": "Emergency boost",
|
||||
"description": "Whether to enable emergency boost mode."
|
||||
},
|
||||
"temporary_setpoint": {
|
||||
"name": "Temporary setpoint",
|
||||
"description": "Temporary setpoint temperature in Celsius during the boost period."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from chip.clusters.Types import Nullable
|
||||
from matter_server.client.models import device_types
|
||||
from matter_server.common.errors import MatterError
|
||||
from matter_server.common.helpers.util import create_attribute_path_from_attribute
|
||||
|
||||
from homeassistant.components.water_heater import (
|
||||
@@ -27,7 +25,6 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .entity import MatterEntity, MatterEntityDescription
|
||||
@@ -43,8 +40,6 @@ WATER_HEATER_SYSTEM_MODE_MAP = {
|
||||
STATE_OFF: 0,
|
||||
}
|
||||
|
||||
DEFAULT_BOOST_DURATION = 3600 # 1 hour
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -83,30 +78,6 @@ class MatterWaterHeater(MatterEntity, WaterHeaterEntity):
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_platform_translation_key = "water_heater"
|
||||
|
||||
async def async_set_boost(
|
||||
self,
|
||||
duration: int,
|
||||
emergency_boost: bool = False,
|
||||
temporary_setpoint: int | None = None,
|
||||
) -> None:
|
||||
"""Set boost."""
|
||||
boost_info: clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct = clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct(
|
||||
duration=duration,
|
||||
emergencyBoost=emergency_boost,
|
||||
temporarySetpoint=(
|
||||
temporary_setpoint * TEMPERATURE_SCALING_FACTOR
|
||||
if temporary_setpoint is not None
|
||||
else Nullable
|
||||
),
|
||||
)
|
||||
try:
|
||||
await self.send_device_command(
|
||||
clusters.WaterHeaterManagement.Commands.Boost(boostInfo=boost_info)
|
||||
)
|
||||
except MatterError as err:
|
||||
raise HomeAssistantError(f"Error sending Boost command: {err}") from err
|
||||
self._update_from_device()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
target_temperature: float | None = kwargs.get(ATTR_TEMPERATURE)
|
||||
@@ -123,11 +94,11 @@ class MatterWaterHeater(MatterEntity, WaterHeaterEntity):
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
"""Set new operation mode."""
|
||||
self._attr_current_operation = operation_mode
|
||||
# Use the constant for boost duration
|
||||
# Boost 1h (3600s)
|
||||
boost_info: type[
|
||||
clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct
|
||||
] = clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct(
|
||||
duration=DEFAULT_BOOST_DURATION
|
||||
duration=3600
|
||||
)
|
||||
system_mode_value = WATER_HEATER_SYSTEM_MODE_MAP[operation_mode]
|
||||
await self.write_attribute(
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp[default]==2025.10.22"],
|
||||
"requirements": ["yt-dlp[default]==2025.09.26"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -270,13 +270,10 @@ class MetOfficeWeather(
|
||||
self.forecast_coordinators["daily"],
|
||||
)
|
||||
timesteps = coordinator.data.timesteps
|
||||
start_datetime = datetime.now(tz=timesteps[0]["time"].tzinfo).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
return [
|
||||
_build_daily_forecast_data(timestep)
|
||||
for timestep in timesteps
|
||||
if timestep["time"] >= start_datetime
|
||||
if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo)
|
||||
]
|
||||
|
||||
@callback
|
||||
@@ -288,13 +285,10 @@ class MetOfficeWeather(
|
||||
)
|
||||
|
||||
timesteps = coordinator.data.timesteps
|
||||
start_datetime = datetime.now(tz=timesteps[0]["time"].tzinfo).replace(
|
||||
minute=0, second=0, microsecond=0
|
||||
)
|
||||
return [
|
||||
_build_hourly_forecast_data(timestep)
|
||||
for timestep in timesteps
|
||||
if timestep["time"] >= start_datetime
|
||||
if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo)
|
||||
]
|
||||
|
||||
@callback
|
||||
@@ -305,11 +299,8 @@ class MetOfficeWeather(
|
||||
self.forecast_coordinators["twice_daily"],
|
||||
)
|
||||
timesteps = coordinator.data.timesteps
|
||||
start_datetime = datetime.now(tz=timesteps[0]["time"].tzinfo).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
return [
|
||||
_build_twice_daily_forecast_data(timestep)
|
||||
for timestep in timesteps
|
||||
if timestep["time"] >= start_datetime
|
||||
if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo)
|
||||
]
|
||||
|
||||
@@ -248,7 +248,7 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -
|
||||
|
||||
Causes for this is config entry options changing.
|
||||
"""
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -3815,7 +3815,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
try_connection,
|
||||
new_entry_data,
|
||||
):
|
||||
return self.async_update_and_abort(reauth_entry, data=new_entry_data)
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry, data=new_entry_data
|
||||
)
|
||||
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
@@ -3861,7 +3863,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if can_connect:
|
||||
if is_reconfigure:
|
||||
return self.async_update_and_abort(
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data=validated_user_input,
|
||||
)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["nsapi==3.1.3"]
|
||||
"requirements": ["nsapi==3.1.2"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PROFILE_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_PROFILE_ID, DOMAIN
|
||||
@@ -24,40 +23,11 @@ AUTH_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_init_nextdns(
|
||||
hass: HomeAssistant, api_key: str, profile_id: str | None = None
|
||||
) -> NextDns:
|
||||
"""Check if credentials and profile_id are valid."""
|
||||
async def async_init_nextdns(hass: HomeAssistant, api_key: str) -> NextDns:
|
||||
"""Check if credentials are valid."""
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
nextdns = await NextDns.create(websession, api_key)
|
||||
|
||||
if profile_id:
|
||||
if not any(profile.id == profile_id for profile in nextdns.profiles):
|
||||
raise ProfileNotAvailable
|
||||
|
||||
return nextdns
|
||||
|
||||
|
||||
async def async_validate_new_api_key(
|
||||
hass: HomeAssistant, user_input: dict[str, Any], profile_id: str
|
||||
) -> dict[str, str]:
|
||||
"""Validate the new API key during reconfiguration or reauth."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
try:
|
||||
await async_init_nextdns(hass, user_input[CONF_API_KEY], profile_id)
|
||||
except InvalidApiKeyError:
|
||||
errors["base"] = "invalid_api_key"
|
||||
except (ApiError, ClientConnectorError, RetryError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except ProfileNotAvailable:
|
||||
errors["base"] = "profile_not_available"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return errors
|
||||
return await NextDns.create(websession, api_key)
|
||||
|
||||
|
||||
class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@@ -137,19 +107,20 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
errors: dict[str, str] = {}
|
||||
entry = self._get_reauth_entry()
|
||||
|
||||
if user_input is not None:
|
||||
errors = await async_validate_new_api_key(
|
||||
self.hass, user_input, entry.data[CONF_PROFILE_ID]
|
||||
)
|
||||
if errors.get("base") == "profile_not_available":
|
||||
return self.async_abort(reason="profile_not_available")
|
||||
|
||||
if not errors:
|
||||
try:
|
||||
await async_init_nextdns(self.hass, user_input[CONF_API_KEY])
|
||||
except InvalidApiKeyError:
|
||||
errors["base"] = "invalid_api_key"
|
||||
except (ApiError, ClientConnectorError, RetryError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data_updates=user_input,
|
||||
self._get_reauth_entry(), data_updates=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -157,33 +128,3 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a reconfiguration flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
errors = await async_validate_new_api_key(
|
||||
self.hass, user_input, entry.data[CONF_PROFILE_ID]
|
||||
)
|
||||
if errors.get("base") == "profile_not_available":
|
||||
return self.async_abort(reason="profile_not_available")
|
||||
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class ProfileNotAvailable(HomeAssistantError):
|
||||
"""Error to indicate that the profile is not available after reconfig/reauth."""
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nextdns"],
|
||||
"quality_scale": "platinum",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["nextdns==4.1.0"]
|
||||
}
|
||||
|
||||
@@ -48,17 +48,19 @@ rules:
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: The integration is a cloud service and thus does not support discovery.
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations:
|
||||
status: todo
|
||||
comment: Add info that there are no known limitations.
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: This is a service, which doesn't integrate with any devices.
|
||||
docs-supported-functions: done
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting:
|
||||
status: exempt
|
||||
comment: No known issues that could be resolved by the user.
|
||||
docs-use-cases: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: This integration has a fixed single service.
|
||||
@@ -68,7 +70,9 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
reconfiguration-flow:
|
||||
status: todo
|
||||
comment: Allow API key to be changed in the re-configure flow.
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: This integration doesn't have any cases where raising an issue is needed.
|
||||
|
||||
@@ -24,14 +24,6 @@
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::nextdns::config::step::user::data_description::api_key%]"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::nextdns::config::step::user::data_description::api_key%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -41,9 +33,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This NextDNS profile is already configured.",
|
||||
"profile_not_available": "The configured NextDNS profile is no longer available in your account. Remove the configuration and configure the integration again.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .const import _LOGGER
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT, Platform.SCENE]
|
||||
PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT]
|
||||
|
||||
type NikoHomeControlConfigEntry = ConfigEntry[NHCController]
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"""Scene Platform for Niko Home Control."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.scene import BaseScene
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import NikoHomeControlConfigEntry
|
||||
from .entity import NikoHomeControlEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: NikoHomeControlConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Niko Home Control scene entry."""
|
||||
controller = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
NikoHomeControlScene(scene, controller, entry.entry_id)
|
||||
for scene in controller.scenes
|
||||
)
|
||||
|
||||
|
||||
class NikoHomeControlScene(NikoHomeControlEntity, BaseScene):
|
||||
"""Representation of a Niko Home Control Scene."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
async def _async_activate(self, **kwargs: Any) -> None:
|
||||
"""Activate scene. Try to get entities into requested state."""
|
||||
await self._action.activate()
|
||||
|
||||
def update_state(self) -> None:
|
||||
"""Update HA state."""
|
||||
self._async_record_activation()
|
||||
@@ -2,17 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
ALL_MATCH_REGEX,
|
||||
CONF_AREA_FILTER,
|
||||
CONF_FILTER_CORONA,
|
||||
CONF_FILTERS,
|
||||
CONF_HEADLINE_FILTER,
|
||||
NO_MATCH_REGEX,
|
||||
)
|
||||
@@ -23,6 +19,20 @@ PLATFORMS: list[str] = [Platform.BINARY_SENSOR]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: NinaConfigEntry) -> bool:
|
||||
"""Set up platform from a ConfigEntry."""
|
||||
if CONF_HEADLINE_FILTER not in entry.data:
|
||||
filter_regex = NO_MATCH_REGEX
|
||||
|
||||
if entry.data[CONF_FILTER_CORONA]:
|
||||
filter_regex = ".*corona.*"
|
||||
|
||||
new_data = {**entry.data, CONF_HEADLINE_FILTER: filter_regex}
|
||||
new_data.pop(CONF_FILTER_CORONA, None)
|
||||
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
|
||||
if CONF_AREA_FILTER not in entry.data:
|
||||
new_data = {**entry.data, CONF_AREA_FILTER: ALL_MATCH_REGEX}
|
||||
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
|
||||
coordinator = NINADataUpdateCoordinator(hass, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@@ -37,52 +47,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: NinaConfigEntry) -> bool
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: NinaConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: NinaConfigEntry) -> bool:
|
||||
"""Migrate the config to the new format."""
|
||||
|
||||
version = entry.version
|
||||
minor_version = entry.minor_version
|
||||
|
||||
_LOGGER.debug("Migrating from version %s.%s", version, minor_version)
|
||||
if entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
new_data: dict[str, Any] = {**entry.data, CONF_FILTERS: {}}
|
||||
|
||||
if version == 1 and minor_version == 1:
|
||||
if CONF_HEADLINE_FILTER not in entry.data:
|
||||
filter_regex = NO_MATCH_REGEX
|
||||
|
||||
if entry.data.get(CONF_FILTER_CORONA, None):
|
||||
filter_regex = ".*corona.*"
|
||||
|
||||
new_data[CONF_HEADLINE_FILTER] = filter_regex
|
||||
new_data.pop(CONF_FILTER_CORONA, None)
|
||||
|
||||
if CONF_AREA_FILTER not in entry.data:
|
||||
new_data[CONF_AREA_FILTER] = ALL_MATCH_REGEX
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data=new_data,
|
||||
minor_version=2,
|
||||
)
|
||||
minor_version = 2
|
||||
|
||||
if version == 1 and minor_version == 2:
|
||||
new_data[CONF_FILTERS][CONF_HEADLINE_FILTER] = entry.data[CONF_HEADLINE_FILTER]
|
||||
new_data.pop(CONF_HEADLINE_FILTER, None)
|
||||
|
||||
new_data[CONF_FILTERS][CONF_AREA_FILTER] = entry.data[CONF_AREA_FILTER]
|
||||
new_data.pop(CONF_AREA_FILTER, None)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data=new_data,
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -14,16 +14,13 @@ from homeassistant.config_entries import (
|
||||
OptionsFlowWithReload,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import section
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
ALL_MATCH_REGEX,
|
||||
CONF_AREA_FILTER,
|
||||
CONF_FILTERS,
|
||||
CONF_HEADLINE_FILTER,
|
||||
CONF_MESSAGE_SLOTS,
|
||||
CONF_REGIONS,
|
||||
@@ -90,7 +87,6 @@ class NinaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for NINA."""
|
||||
|
||||
VERSION: int = 1
|
||||
MINOR_VERSION: int = 3
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
@@ -130,8 +126,8 @@ class NinaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if group_input := user_input.get(group):
|
||||
user_input[CONF_REGIONS] += group_input
|
||||
|
||||
if not user_input[CONF_FILTERS][CONF_HEADLINE_FILTER]:
|
||||
user_input[CONF_FILTERS][CONF_HEADLINE_FILTER] = NO_MATCH_REGEX
|
||||
if not user_input[CONF_HEADLINE_FILTER]:
|
||||
user_input[CONF_HEADLINE_FILTER] = NO_MATCH_REGEX
|
||||
|
||||
if user_input[CONF_REGIONS]:
|
||||
return self.async_create_entry(
|
||||
@@ -154,18 +150,7 @@ class NinaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(CONF_MESSAGE_SLOTS, default=5): vol.All(
|
||||
int, vol.Range(min=1, max=20)
|
||||
),
|
||||
vol.Required(CONF_FILTERS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_HEADLINE_FILTER, default=NO_MATCH_REGEX
|
||||
): cv.string,
|
||||
vol.Optional(
|
||||
CONF_AREA_FILTER, default=ALL_MATCH_REGEX
|
||||
): cv.string,
|
||||
}
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_HEADLINE_FILTER, default=""): cv.string,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
@@ -274,20 +259,14 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
CONF_MESSAGE_SLOTS,
|
||||
default=self.data[CONF_MESSAGE_SLOTS],
|
||||
): vol.All(int, vol.Range(min=1, max=20)),
|
||||
vol.Required(CONF_FILTERS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_HEADLINE_FILTER,
|
||||
default=self.data[CONF_FILTERS][CONF_HEADLINE_FILTER],
|
||||
): cv.string,
|
||||
vol.Optional(
|
||||
CONF_AREA_FILTER,
|
||||
default=self.data[CONF_FILTERS][CONF_AREA_FILTER],
|
||||
): cv.string,
|
||||
}
|
||||
)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_HEADLINE_FILTER,
|
||||
default=self.data[CONF_HEADLINE_FILTER],
|
||||
): cv.string,
|
||||
vol.Optional(
|
||||
CONF_AREA_FILTER,
|
||||
default=self.data[CONF_AREA_FILTER],
|
||||
): cv.string,
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -17,7 +17,6 @@ ALL_MATCH_REGEX: str = ".*"
|
||||
|
||||
CONF_REGIONS: str = "regions"
|
||||
CONF_MESSAGE_SLOTS: str = "slots"
|
||||
CONF_FILTERS: str = "filters"
|
||||
CONF_FILTER_CORONA: str = "corona_filter" # deprecated
|
||||
CONF_HEADLINE_FILTER: str = "headline_filter"
|
||||
CONF_AREA_FILTER: str = "area_filter"
|
||||
|
||||
@@ -17,7 +17,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
CONF_AREA_FILTER,
|
||||
CONF_FILTERS,
|
||||
CONF_HEADLINE_FILTER,
|
||||
CONF_REGIONS,
|
||||
DOMAIN,
|
||||
@@ -59,10 +58,8 @@ class NINADataUpdateCoordinator(
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self._nina: Nina = Nina(async_get_clientsession(hass))
|
||||
self.headline_filter: str = config_entry.data[CONF_FILTERS][
|
||||
CONF_HEADLINE_FILTER
|
||||
]
|
||||
self.area_filter: str = config_entry.data[CONF_FILTERS][CONF_AREA_FILTER]
|
||||
self.headline_filter: str = config_entry.data[CONF_HEADLINE_FILTER]
|
||||
self.area_filter: str = config_entry.data[CONF_AREA_FILTER]
|
||||
|
||||
regions: dict[str, str] = config_entry.data[CONF_REGIONS]
|
||||
for region in regions:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user