mirror of
https://github.com/home-assistant/core.git
synced 2025-12-18 05:48:11 +00:00
Compare commits
7 Commits
dev
...
fritzbox/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1dc193610 | ||
|
|
53b81c5bbd | ||
|
|
37a61427de | ||
|
|
f8fd4b99fe | ||
|
|
8785867c16 | ||
|
|
b5addcfc81 | ||
|
|
90e103beb9 |
3
.github/copilot-instructions.md
vendored
3
.github/copilot-instructions.md
vendored
@@ -51,9 +51,6 @@ rules:
|
||||
- **Missing imports** - We use static analysis tooling to catch that
|
||||
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
|
||||
|
||||
**Git commit practices during review:**
|
||||
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
||||
|
||||
## Python Requirements
|
||||
|
||||
- **Compatibility**: Python 3.13+
|
||||
|
||||
8
.github/workflows/builder.yml
vendored
8
.github/workflows/builder.yml
vendored
@@ -15,7 +15,7 @@ env:
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
# Base image version from https://github.com/home-assistant/docker
|
||||
BASE_IMAGE_VERSION: "2025.12.0"
|
||||
BASE_IMAGE_VERSION: "2025.11.3"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
jobs:
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -482,7 +482,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
|
||||
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -263,7 +263,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: &actions-cache actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
|
||||
with:
|
||||
path: venv
|
||||
key: &key-pre-commit-venv >-
|
||||
@@ -304,7 +304,7 @@ jobs:
|
||||
- &cache-restore-pre-commit-venv
|
||||
name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: &actions-cache-restore actions/cache/restore@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -511,7 +511,7 @@ jobs:
|
||||
fi
|
||||
- name: Save apt cache
|
||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
uses: &actions-cache-save actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: &actions-cache-save actions/cache/save@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0
|
||||
with:
|
||||
path: *path-apt-cache
|
||||
key: *key-apt-cache
|
||||
@@ -534,7 +534,7 @@ jobs:
|
||||
python --version
|
||||
uv pip freeze >> pip_freeze.txt
|
||||
- name: Upload pip_freeze artifact
|
||||
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: pip-freeze-${{ matrix.python-version }}
|
||||
path: pip_freeze.txt
|
||||
@@ -864,7 +864,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: "30"
|
||||
|
||||
4
.github/workflows/wheels.yml
vendored
4
.github/workflows/wheels.yml
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
) > .env_file
|
||||
|
||||
- name: Upload env_file
|
||||
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
|
||||
- &download-env-file
|
||||
name: Download env_file
|
||||
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
|
||||
1
CODEOWNERS
generated
1
CODEOWNERS
generated
@@ -665,7 +665,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/here_travel_time/ @eifinger
|
||||
/tests/components/here_travel_time/ @eifinger
|
||||
/homeassistant/components/hikvision/ @mezz64
|
||||
/tests/components/hikvision/ @mezz64
|
||||
/homeassistant/components/hikvisioncam/ @fbradyirl
|
||||
/homeassistant/components/hisense_aehw4a1/ @bannhead
|
||||
/tests/components/hisense_aehw4a1/ @bannhead
|
||||
|
||||
2
Dockerfile
generated
2
Dockerfile
generated
@@ -30,7 +30,7 @@ RUN \
|
||||
# Verify go2rtc can be executed
|
||||
go2rtc --version \
|
||||
# Install uv
|
||||
&& pip3 install uv==0.9.17
|
||||
&& pip3 install uv==0.9.6
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
||||
@@ -624,16 +624,13 @@ async def async_enable_logging(
|
||||
|
||||
if log_file is None:
|
||||
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
if "SUPERVISOR" in os.environ and "HA_DUPLICATE_LOG_FILE" not in os.environ:
|
||||
if "SUPERVISOR" in os.environ:
|
||||
_LOGGER.info("Running in Supervisor, not logging to file")
|
||||
# Rename the default log file if it exists, since previous versions created
|
||||
# it even on Supervisor
|
||||
def rename_old_file() -> None:
|
||||
"""Rename old log file in executor."""
|
||||
if os.path.isfile(default_log_path):
|
||||
with contextlib.suppress(OSError):
|
||||
os.rename(default_log_path, f"{default_log_path}.old")
|
||||
|
||||
await hass.async_add_executor_job(rename_old_file)
|
||||
if os.path.isfile(default_log_path):
|
||||
with contextlib.suppress(OSError):
|
||||
os.rename(default_log_path, f"{default_log_path}.old")
|
||||
err_log_path = None
|
||||
else:
|
||||
err_log_path = default_log_path
|
||||
|
||||
@@ -9,9 +9,8 @@ from actron_neo_api import (
|
||||
|
||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .const import _LOGGER
|
||||
from .coordinator import (
|
||||
ActronAirConfigEntry,
|
||||
ActronAirRuntimeData,
|
||||
@@ -30,13 +29,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
|
||||
try:
|
||||
systems = await api.get_ac_systems()
|
||||
await api.update_status()
|
||||
except ActronAirAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
) from err
|
||||
except ActronAirAuthError:
|
||||
_LOGGER.error("Authentication error while setting up Actron Air integration")
|
||||
raise
|
||||
except ActronAirAPIError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
|
||||
raise
|
||||
|
||||
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
||||
for system in systems:
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Setup config flow for Actron Air integration."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from actron_neo_api import ActronAirAPI, ActronAirAuthError
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
@@ -96,16 +95,8 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
unique_id = str(user_data["id"])
|
||||
await self.async_set_unique_id(unique_id)
|
||||
|
||||
# Check if this is a reauth flow
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
data_updates={CONF_API_TOKEN: self._api.refresh_token_value},
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_data["email"],
|
||||
data={CONF_API_TOKEN: self._api.refresh_token_value},
|
||||
@@ -123,21 +114,6 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
del self.login_task
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauthentication request."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm reauth dialog."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_user()
|
||||
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
|
||||
async def async_step_connection_error(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -5,23 +5,16 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from actron_neo_api import (
|
||||
ActronAirACSystem,
|
||||
ActronAirAPI,
|
||||
ActronAirAuthError,
|
||||
ActronAirStatus,
|
||||
)
|
||||
from actron_neo_api import ActronAirACSystem, ActronAirAPI, ActronAirStatus
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .const import _LOGGER
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
STALE_DEVICE_TIMEOUT = timedelta(minutes=5)
|
||||
STALE_DEVICE_TIMEOUT = timedelta(hours=24)
|
||||
ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
|
||||
ERROR_UNKNOWN = "unknown_error"
|
||||
|
||||
@@ -36,6 +29,9 @@ class ActronAirRuntimeData:
|
||||
|
||||
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
||||
|
||||
AUTH_ERROR_THRESHOLD = 3
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
|
||||
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
||||
"""System coordinator for Actron Air integration."""
|
||||
@@ -63,14 +59,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
||||
|
||||
async def _async_update_data(self) -> ActronAirStatus:
|
||||
"""Fetch updates and merge incremental changes into the full state."""
|
||||
try:
|
||||
await self.api.update_status()
|
||||
except ActronAirAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
) from err
|
||||
|
||||
await self.api.update_status()
|
||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
||||
self.last_seen = dt_util.utcnow()
|
||||
return self.status
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["actron-neo-api==0.2.0"]
|
||||
"requirements": ["actron-neo-api==0.1.87"]
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"oauth2_error": "Failed to start authentication flow",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"wrong_account": "You must reauthenticate with the same Actron Air account that was originally configured."
|
||||
"oauth2_error": "Failed to start OAuth2 flow"
|
||||
},
|
||||
"error": {
|
||||
"oauth2_error": "Failed to start authentication flow. Please try again later."
|
||||
"oauth2_error": "Failed to start OAuth2 flow. Please try again later."
|
||||
},
|
||||
"progress": {
|
||||
"wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes."
|
||||
@@ -18,23 +16,14 @@
|
||||
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
|
||||
"title": "Connection error"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "Your Actron Air authentication has expired. Select continue to reauthenticate with your Actron Air account. You will be prompted to log in again to restore the connection.",
|
||||
"title": "Authentication expired"
|
||||
},
|
||||
"timeout": {
|
||||
"data": {},
|
||||
"description": "The authentication process timed out. Please try again.",
|
||||
"title": "Authentication timeout"
|
||||
"description": "The authorization process timed out. Please try again.",
|
||||
"title": "Authorization timeout"
|
||||
},
|
||||
"user": {
|
||||
"title": "Actron Air Authentication"
|
||||
"title": "Actron Air OAuth2 Authorization"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"auth_error": {
|
||||
"message": "Authentication failed, please reauthenticate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import get_supported_features
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityTargetStateTriggerBase,
|
||||
EntityStateTriggerBase,
|
||||
Trigger,
|
||||
make_entity_target_state_trigger,
|
||||
make_entity_transition_trigger,
|
||||
make_conditional_entity_state_trigger,
|
||||
make_entity_state_trigger,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
|
||||
@@ -21,7 +21,7 @@ def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool
|
||||
return False
|
||||
|
||||
|
||||
class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
||||
class EntityStateTriggerRequiredFeatures(EntityStateTriggerBase):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
_required_features: int
|
||||
@@ -38,7 +38,7 @@ class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
||||
|
||||
def make_entity_state_trigger_required_features(
|
||||
domain: str, to_state: str, required_features: int
|
||||
) -> type[EntityTargetStateTriggerBase]:
|
||||
) -> type[EntityStateTriggerBase]:
|
||||
"""Create an entity state trigger class."""
|
||||
|
||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||
@@ -52,7 +52,7 @@ def make_entity_state_trigger_required_features(
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"armed": make_entity_transition_trigger(
|
||||
"armed": make_conditional_entity_state_trigger(
|
||||
DOMAIN,
|
||||
from_states={
|
||||
AlarmControlPanelState.ARMING,
|
||||
@@ -89,12 +89,8 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
AlarmControlPanelEntityFeature.ARM_VACATION,
|
||||
),
|
||||
"disarmed": make_entity_target_state_trigger(
|
||||
DOMAIN, AlarmControlPanelState.DISARMED
|
||||
),
|
||||
"triggered": make_entity_target_state_trigger(
|
||||
DOMAIN, AlarmControlPanelState.TRIGGERED
|
||||
),
|
||||
"disarmed": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.DISARMED),
|
||||
"triggered": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.TRIGGERED),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
"codeowners": ["@pantherale0"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.1.0"]
|
||||
"requirements": ["pyanglianwater==3.0.0"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@yuxincs"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["apcaccess"],
|
||||
"quality_scale": "platinum",
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
"""Provides triggers for assist satellites."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import AssistSatelliteState
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"idle": make_entity_target_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
|
||||
"listening": make_entity_target_state_trigger(
|
||||
DOMAIN, AssistSatelliteState.LISTENING
|
||||
),
|
||||
"processing": make_entity_target_state_trigger(
|
||||
DOMAIN, AssistSatelliteState.PROCESSING
|
||||
),
|
||||
"responding": make_entity_target_state_trigger(
|
||||
DOMAIN, AssistSatelliteState.RESPONDING
|
||||
),
|
||||
"idle": make_entity_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
|
||||
"listening": make_entity_state_trigger(DOMAIN, AssistSatelliteState.LISTENING),
|
||||
"processing": make_entity_state_trigger(DOMAIN, AssistSatelliteState.PROCESSING),
|
||||
"responding": make_entity_state_trigger(DOMAIN, AssistSatelliteState.RESPONDING),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
|
||||
"requirements": ["aioasuswrt==1.5.4", "asusrouter==1.21.3"]
|
||||
"requirements": ["aioasuswrt==1.5.3", "asusrouter==1.21.3"]
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
"codeowners": ["@klaasnicolaas"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["autarco==3.2.0"]
|
||||
}
|
||||
|
||||
@@ -125,17 +125,14 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"binary_sensor",
|
||||
"button",
|
||||
"climate",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"fan",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"media_player",
|
||||
"switch",
|
||||
"text",
|
||||
"update",
|
||||
"vacuum",
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import get_device_class
|
||||
from homeassistant.helpers.trigger import EntityTargetStateTriggerBase, Trigger
|
||||
from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
|
||||
from . import DOMAIN, BinarySensorDeviceClass
|
||||
@@ -20,7 +20,7 @@ def get_device_class_or_undefined(
|
||||
return UNDEFINED
|
||||
|
||||
|
||||
class BinarySensorOnOffTrigger(EntityTargetStateTriggerBase):
|
||||
class BinarySensorOnOffTrigger(EntityStateTriggerBase):
|
||||
"""Class for binary sensor on/off triggers."""
|
||||
|
||||
_device_class: BinarySensorDeviceClass | None
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["blinkpy"],
|
||||
"requirements": ["blinkpy==0.25.2"]
|
||||
"requirements": ["blinkpy==0.25.1"]
|
||||
}
|
||||
|
||||
@@ -17,10 +17,5 @@
|
||||
"press": {
|
||||
"service": "mdi:gesture-tap-button"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"pressed": {
|
||||
"trigger": "mdi:gesture-tap-button"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,5 @@
|
||||
"name": "Press"
|
||||
}
|
||||
},
|
||||
"title": "Button",
|
||||
"triggers": {
|
||||
"pressed": {
|
||||
"description": "Triggers when a button was pressed",
|
||||
"name": "Button pressed"
|
||||
}
|
||||
}
|
||||
"title": "Button"
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
"""Provides triggers for buttons."""
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
EntityTriggerBase,
|
||||
Trigger,
|
||||
)
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
class ButtonPressedTrigger(EntityTriggerBase):
|
||||
"""Trigger for button entity presses."""
|
||||
|
||||
_domain = DOMAIN
|
||||
_schema = ENTITY_STATE_TRIGGER_SCHEMA
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
"""Check if the origin state is valid and different from the current state."""
|
||||
|
||||
# UNKNOWN is a valid from_state, otherwise the first time the button is pressed
|
||||
# would not trigger
|
||||
if from_state.state == STATE_UNAVAILABLE:
|
||||
return False
|
||||
|
||||
return from_state.state != to_state.state
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the new state is not invalid."""
|
||||
return state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"pressed": ButtonPressedTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for buttons."""
|
||||
return TRIGGERS
|
||||
@@ -1,4 +0,0 @@
|
||||
pressed:
|
||||
target:
|
||||
entity:
|
||||
domain: button
|
||||
@@ -3,22 +3,22 @@
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_target_state_attribute_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
make_entity_transition_trigger,
|
||||
make_conditional_entity_state_trigger,
|
||||
make_entity_state_attribute_trigger,
|
||||
make_entity_state_trigger,
|
||||
)
|
||||
|
||||
from .const import ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"started_cooling": make_entity_target_state_attribute_trigger(
|
||||
"started_cooling": make_entity_state_attribute_trigger(
|
||||
DOMAIN, ATTR_HVAC_ACTION, HVACAction.COOLING
|
||||
),
|
||||
"started_drying": make_entity_target_state_attribute_trigger(
|
||||
"started_drying": make_entity_state_attribute_trigger(
|
||||
DOMAIN, ATTR_HVAC_ACTION, HVACAction.DRYING
|
||||
),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
|
||||
"turned_on": make_entity_transition_trigger(
|
||||
"turned_off": make_entity_state_trigger(DOMAIN, HVACMode.OFF),
|
||||
"turned_on": make_conditional_entity_state_trigger(
|
||||
DOMAIN,
|
||||
from_states={
|
||||
HVACMode.OFF,
|
||||
@@ -32,7 +32,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
HVACMode.HEAT_COOL,
|
||||
},
|
||||
),
|
||||
"started_heating": make_entity_target_state_attribute_trigger(
|
||||
"started_heating": make_entity_state_attribute_trigger(
|
||||
DOMAIN, ATTR_HVAC_ACTION, HVACAction.HEATING
|
||||
),
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["compit"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["compit-inext-api==0.3.4"]
|
||||
"requirements": ["compit-inext-api==0.3.1"]
|
||||
}
|
||||
|
||||
@@ -65,10 +65,8 @@ def websocket_create_area(
|
||||
data.pop("id")
|
||||
|
||||
if "aliases" in data:
|
||||
# Create a set for the aliases without:
|
||||
# - Empty strings
|
||||
# - Trailing and leading whitespace characters in the individual aliases
|
||||
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
|
||||
# Convert aliases to a set
|
||||
data["aliases"] = set(data["aliases"])
|
||||
|
||||
if "labels" in data:
|
||||
# Convert labels to a set
|
||||
@@ -135,10 +133,8 @@ def websocket_update_area(
|
||||
data.pop("id")
|
||||
|
||||
if "aliases" in data:
|
||||
# Create a set for the aliases without:
|
||||
# - Empty strings
|
||||
# - Trailing and leading whitespace characters in the individual aliases
|
||||
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
|
||||
# Convert aliases to a set
|
||||
data["aliases"] = set(data["aliases"])
|
||||
|
||||
if "labels" in data:
|
||||
# Convert labels to a set
|
||||
|
||||
@@ -227,10 +227,8 @@ def websocket_update_entity(
|
||||
changes[key] = msg[key]
|
||||
|
||||
if "aliases" in msg:
|
||||
# Create a set for the aliases without:
|
||||
# - Empty strings
|
||||
# - Trailing and leading whitespace characters in the individual aliases
|
||||
changes["aliases"] = {s_strip for s in msg["aliases"] if (s_strip := s.strip())}
|
||||
# Convert aliases to a set
|
||||
changes["aliases"] = set(msg["aliases"])
|
||||
|
||||
if "labels" in msg:
|
||||
# Convert labels to a set
|
||||
|
||||
@@ -61,10 +61,8 @@ def websocket_create_floor(
|
||||
data.pop("id")
|
||||
|
||||
if "aliases" in data:
|
||||
# Create a set for the aliases without:
|
||||
# - Empty strings
|
||||
# - Trailing and leading whitespace characters in the individual aliases
|
||||
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
|
||||
# Convert aliases to a set
|
||||
data["aliases"] = set(data["aliases"])
|
||||
|
||||
try:
|
||||
entry = registry.async_create(**data)
|
||||
@@ -119,10 +117,8 @@ def websocket_update_floor(
|
||||
data.pop("id")
|
||||
|
||||
if "aliases" in data:
|
||||
# Create a set for the aliases without:
|
||||
# - Empty strings
|
||||
# - Trailing and leading whitespace characters in the individual aliases
|
||||
data["aliases"] = {s_strip for s in data["aliases"] if (s_strip := s.strip())}
|
||||
# Convert aliases to a set
|
||||
data["aliases"] = set(data["aliases"])
|
||||
|
||||
try:
|
||||
entry = registry.async_update(**data)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@OnFreund"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/coolmaster",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pycoolmasternet_async"],
|
||||
"requirements": ["pycoolmasternet-async==0.2.2"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -11,15 +11,18 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
ConditionChecker,
|
||||
ConditionCheckerType,
|
||||
ConditionConfig,
|
||||
trace_condition_function,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import DeviceAutomationType, async_get_device_automation_platform
|
||||
from .helpers import async_validate_device_automation_config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers import condition
|
||||
|
||||
|
||||
class DeviceAutomationConditionProtocol(Protocol):
|
||||
"""Define the format of device_condition modules.
|
||||
@@ -87,21 +90,15 @@ class DeviceCondition(Condition):
|
||||
assert config.options is not None
|
||||
self._config = config.options
|
||||
|
||||
async def async_get_checker(self) -> ConditionChecker:
|
||||
async def async_get_checker(self) -> condition.ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
platform = await async_get_device_automation_platform(
|
||||
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
||||
)
|
||||
platform_checker = platform.async_condition_from_config(
|
||||
self._hass, self._config
|
||||
return trace_condition_function(
|
||||
platform.async_condition_from_config(self._hass, self._config)
|
||||
)
|
||||
|
||||
def checker(variables: TemplateVarsType = None, **kwargs: Any) -> bool:
|
||||
result = platform_checker(self._hass, variables)
|
||||
return result is not False
|
||||
|
||||
return checker
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"_device": DeviceCondition,
|
||||
|
||||
@@ -11,13 +11,5 @@
|
||||
"see": {
|
||||
"service": "mdi:account-eye"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"entered_home": {
|
||||
"trigger": "mdi:account-arrow-left"
|
||||
},
|
||||
"left_home": {
|
||||
"trigger": "mdi:account-arrow-right"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted device trackers to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"device_automation": {
|
||||
"condition_type": {
|
||||
"is_home": "{entity_name} is home",
|
||||
@@ -48,15 +44,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"see": {
|
||||
"description": "Manually update the records of a seen legacy device tracker in the known_devices.yaml file.",
|
||||
@@ -93,27 +80,5 @@
|
||||
"name": "See"
|
||||
}
|
||||
},
|
||||
"title": "Device tracker",
|
||||
"triggers": {
|
||||
"entered_home": {
|
||||
"description": "Triggers when one or more device trackers enter home.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::device_tracker::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::device_tracker::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Entered home"
|
||||
},
|
||||
"left_home": {
|
||||
"description": "Triggers when one or more device trackers leave home.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::device_tracker::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::device_tracker::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Left home"
|
||||
}
|
||||
}
|
||||
"title": "Device tracker"
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"""Provides triggers for device_trackers."""
|
||||
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import (
|
||||
Trigger,
|
||||
make_entity_origin_state_trigger,
|
||||
make_entity_target_state_trigger,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"entered_home": make_entity_target_state_trigger(DOMAIN, STATE_HOME),
|
||||
"left_home": make_entity_origin_state_trigger(DOMAIN, from_state=STATE_HOME),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for device trackers."""
|
||||
return TRIGGERS
|
||||
@@ -1,18 +0,0 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: device_tracker
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
|
||||
entered_home: *trigger_common
|
||||
left_home: *trigger_common
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/directv",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["directv"],
|
||||
"requirements": ["directv==0.4.0"],
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodns==3.6.1"]
|
||||
"requirements": ["aiodns==3.6.0"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodukeenergy==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Event platform for ekey bionyx integration."""
|
||||
|
||||
from http import HTTPStatus
|
||||
|
||||
from aiohttp.hdrs import METH_POST
|
||||
from aiohttp.web import Request, Response
|
||||
|
||||
@@ -54,18 +52,8 @@ class EkeyEvent(EventEntity):
|
||||
async def async_webhook_handler(
|
||||
hass: HomeAssistant, webhook_id: str, request: Request
|
||||
) -> Response | None:
|
||||
try:
|
||||
payload = await request.json()
|
||||
except ValueError:
|
||||
return Response(status=HTTPStatus.BAD_REQUEST)
|
||||
auth = payload.get("auth")
|
||||
|
||||
if auth is None:
|
||||
return Response(status=HTTPStatus.BAD_REQUEST)
|
||||
if auth != self._auth:
|
||||
return Response(status=HTTPStatus.UNAUTHORIZED)
|
||||
|
||||
self._async_handle_event()
|
||||
if (await request.json())["auth"] == self._auth:
|
||||
self._async_handle_event()
|
||||
return None
|
||||
|
||||
webhook_register(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["ekey-bionyxpy==1.0.1"]
|
||||
"requirements": ["ekey-bionyxpy==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/elkm1",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["elkm1_lib"],
|
||||
"requirements": ["elkm1-lib==2.2.13"]
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/elvia",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["elvia==0.1.0"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@borpin", "@alexandrecuer"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/emoncms",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyemoncms==0.1.3"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/emonitor",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioemonitor"],
|
||||
"requirements": ["aioemonitor==1.0.5"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@bdraco", "@cgarwood", "@catsmanac"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@gwww", "@michaeldavie"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.12.1"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@pszafer"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/epson",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["epson_projector"],
|
||||
"requirements": ["epson-projector==0.6.0"]
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"homekit": {
|
||||
"models": ["Escea"]
|
||||
},
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pescea==1.0.12"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/evil_genius_labs",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyevilgenius==2.0.0"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@ntilley905"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/faa_delays",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["faadelays"],
|
||||
"requirements": ["faadelays==2023.9.1"]
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_state_trigger(DOMAIN, STATE_OFF),
|
||||
"turned_on": make_entity_state_trigger(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@Lorenzo-Gasparini"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fing",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["fing_agent_api==1.0.3"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@erwindouna"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/firefly_iii",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyfirefly==0.1.8"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@cyberjunky"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fireservicerota",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyfireservicerota"],
|
||||
"requirements": ["pyfireservicerota==0.0.46"]
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials", "http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/fitbit",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["fitbit", "fitbit_web_api"],
|
||||
"requirements": ["fitbit==0.3.1", "fitbit-web-api==2.13.5"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@Sander0542"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fivem",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["fivem-api==0.1.2"]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/fjaraskupan",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bleak", "fjaraskupan"],
|
||||
"requirements": ["fjaraskupan==2.3.3"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@cnico"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/flipr",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["flipr_api"],
|
||||
"requirements": ["flipr-api==1.6.1"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@dmulcahey"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/flo",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioflo"],
|
||||
"requirements": ["aioflo==2021.11.0"]
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/flume",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyflume"],
|
||||
"requirements": ["PyFlume==0.6.5"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@Foscam-wangzhengyu"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/foscam",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["libpyfoscamcgi"],
|
||||
"requirements": ["libpyfoscamcgi==0.0.9"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@stefano055415"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/freedompro",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyfreedompro"],
|
||||
"requirements": ["pyfreedompro==1.1.0"]
|
||||
|
||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
|
||||
from pyfritzhome.devicetypes import FritzhomeTemplate
|
||||
from pyfritzhome.devicetypes import FritzhomeTemplate, FritzhomeTrigger
|
||||
from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -27,6 +27,7 @@ class FritzboxCoordinatorData:
|
||||
|
||||
devices: dict[str, FritzhomeDevice]
|
||||
templates: dict[str, FritzhomeTemplate]
|
||||
triggers: dict[str, FritzhomeTrigger]
|
||||
supported_color_properties: dict[str, tuple[dict, list]]
|
||||
|
||||
|
||||
@@ -37,6 +38,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
configuration_url: str
|
||||
fritz: Fritzhome
|
||||
has_templates: bool
|
||||
has_triggers: bool
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: FritzboxConfigEntry) -> None:
|
||||
"""Initialize the Fritzbox Smarthome device coordinator."""
|
||||
@@ -50,8 +52,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
|
||||
self.new_devices: set[str] = set()
|
||||
self.new_templates: set[str] = set()
|
||||
self.new_triggers: set[str] = set()
|
||||
|
||||
self.data = FritzboxCoordinatorData({}, {}, {})
|
||||
self.data = FritzboxCoordinatorData({}, {}, {}, {})
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
@@ -74,6 +77,11 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
)
|
||||
LOGGER.debug("enable smarthome templates: %s", self.has_templates)
|
||||
|
||||
self.has_triggers = await self.hass.async_add_executor_job(
|
||||
self.fritz.has_triggers
|
||||
)
|
||||
LOGGER.debug("enable smarthome triggers: %s", self.has_triggers)
|
||||
|
||||
self.configuration_url = self.fritz.get_prefixed_host()
|
||||
|
||||
await self.async_config_entry_first_refresh()
|
||||
@@ -92,7 +100,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
|
||||
available_main_ains = [
|
||||
ain
|
||||
for ain, dev in data.devices.items() | data.templates.items()
|
||||
for ain, dev in (data.devices | data.templates | data.triggers).items()
|
||||
if dev.device_and_unit_id[1] is None
|
||||
]
|
||||
device_reg = dr.async_get(self.hass)
|
||||
@@ -112,6 +120,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
self.fritz.update_devices(ignore_removed=False)
|
||||
if self.has_templates:
|
||||
self.fritz.update_templates(ignore_removed=False)
|
||||
if self.has_triggers:
|
||||
self.fritz.update_triggers(ignore_removed=False)
|
||||
|
||||
except RequestConnectionError as ex:
|
||||
raise UpdateFailed from ex
|
||||
except HTTPError:
|
||||
@@ -123,6 +134,8 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
self.fritz.update_devices(ignore_removed=False)
|
||||
if self.has_templates:
|
||||
self.fritz.update_templates(ignore_removed=False)
|
||||
if self.has_triggers:
|
||||
self.fritz.update_triggers(ignore_removed=False)
|
||||
|
||||
devices = self.fritz.get_devices()
|
||||
device_data = {}
|
||||
@@ -156,12 +169,20 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
for template in templates:
|
||||
template_data[template.ain] = template
|
||||
|
||||
trigger_data = {}
|
||||
if self.has_triggers:
|
||||
triggers = self.fritz.get_triggers()
|
||||
for trigger in triggers:
|
||||
trigger_data[trigger.ain] = trigger
|
||||
|
||||
self.new_devices = device_data.keys() - self.data.devices.keys()
|
||||
self.new_templates = template_data.keys() - self.data.templates.keys()
|
||||
self.new_triggers = trigger_data.keys() - self.data.triggers.keys()
|
||||
|
||||
return FritzboxCoordinatorData(
|
||||
devices=device_data,
|
||||
templates=template_data,
|
||||
triggers=trigger_data,
|
||||
supported_color_properties=supported_color_properties,
|
||||
)
|
||||
|
||||
@@ -193,6 +214,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
if (
|
||||
self.data.devices.keys() - new_data.devices.keys()
|
||||
or self.data.templates.keys() - new_data.templates.keys()
|
||||
or self.data.triggers.keys() - new_data.triggers.keys()
|
||||
):
|
||||
self.cleanup_removed_devices(new_data)
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyfritzhome.devicetypes import FritzhomeTrigger
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FritzboxConfigEntry
|
||||
from .entity import FritzBoxDeviceEntity
|
||||
from .entity import FritzBoxDeviceEntity, FritzBoxEntity
|
||||
|
||||
# Coordinator handles data updates, so we can allow unlimited parallel updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -26,21 +29,27 @@ async def async_setup_entry(
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
@callback
|
||||
def _add_entities(devices: set[str] | None = None) -> None:
|
||||
"""Add devices."""
|
||||
def _add_entities(
|
||||
devices: set[str] | None = None, triggers: set[str] | None = None
|
||||
) -> None:
|
||||
"""Add devices and triggers."""
|
||||
if devices is None:
|
||||
devices = coordinator.new_devices
|
||||
if not devices:
|
||||
if triggers is None:
|
||||
triggers = coordinator.new_triggers
|
||||
if not devices and not triggers:
|
||||
return
|
||||
async_add_entities(
|
||||
entities = [
|
||||
FritzboxSwitch(coordinator, ain)
|
||||
for ain in devices
|
||||
if coordinator.data.devices[ain].has_switch
|
||||
)
|
||||
] + [FritzboxTrigger(coordinator, ain) for ain in triggers]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
||||
|
||||
_add_entities(set(coordinator.data.devices))
|
||||
_add_entities(set(coordinator.data.devices), set(coordinator.data.triggers))
|
||||
|
||||
|
||||
class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):
|
||||
@@ -70,3 +79,42 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="manual_switching_disabled",
|
||||
)
|
||||
|
||||
|
||||
class FritzboxTrigger(FritzBoxEntity, SwitchEntity):
|
||||
"""The switch class for FRITZ!SmartHome triggers."""
|
||||
|
||||
@property
|
||||
def data(self) -> FritzhomeTrigger:
|
||||
"""Return the trigger data entity."""
|
||||
return self.coordinator.data.triggers[self.ain]
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device specific attributes."""
|
||||
return DeviceInfo(
|
||||
name=self.data.name,
|
||||
identifiers={(DOMAIN, self.ain)},
|
||||
configuration_url=self.coordinator.configuration_url,
|
||||
manufacturer="FRITZ!",
|
||||
model="SmartHome Routine",
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the trigger is active."""
|
||||
return self.data.active # type: ignore [no-any-return]
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Activate the trigger."""
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.fritz.set_trigger_active, self.ain
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Deactivate the trigger."""
|
||||
await self.hass.async_add_executor_job(
|
||||
self.coordinator.fritz.set_trigger_inactive, self.ain
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -4,12 +4,7 @@
|
||||
"codeowners": ["@wlcrs"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontier_silicon",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["afsapi==0.2.7"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-frontier-silicon-com:undok:fsapi:1"
|
||||
}
|
||||
]
|
||||
"ssdp": [{ "st": "urn:schemas-frontier-silicon-com:undok:fsapi:1" }]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@crevetor"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["ayla-iot-unofficial==1.4.7"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/fully_kiosk",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"mqtt": ["fully/deviceInfo/+"],
|
||||
"quality_scale": "bronze",
|
||||
|
||||
@@ -17,7 +17,7 @@ ENTITY_TYPES: tuple[NumberEntityDescription, ...] = (
|
||||
NumberEntityDescription(
|
||||
key="timeToScreensaverV2",
|
||||
translation_key="screensaver_time",
|
||||
native_max_value=86400,
|
||||
native_max_value=9999,
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
@@ -34,7 +34,7 @@ ENTITY_TYPES: tuple[NumberEntityDescription, ...] = (
|
||||
NumberEntityDescription(
|
||||
key="timeToScreenOffV2",
|
||||
translation_key="screen_off_time",
|
||||
native_max_value=86400,
|
||||
native_max_value=9999,
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@klaasnicolaas"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["odp-amsterdam==6.1.2"]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
|
||||
"requirements": ["gardena-bluetooth==1.6.0"]
|
||||
|
||||
@@ -77,7 +77,6 @@ DEFAULT_DATA = {
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE: False,
|
||||
CONF_FRAMERATE: 2,
|
||||
CONF_VERIFY_SSL: True,
|
||||
CONF_RTSP_TRANSPORT: "tcp",
|
||||
}
|
||||
|
||||
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Any
|
||||
|
||||
import botocore.exceptions
|
||||
from homelink.auth.srp_auth import SRPAuth
|
||||
import jwt
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
@@ -39,6 +38,8 @@ class SRPFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Ask for username and password."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
|
||||
|
||||
srp_auth = SRPAuth()
|
||||
try:
|
||||
tokens = await self.hass.async_add_executor_job(
|
||||
@@ -47,17 +48,12 @@ class SRPFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
except botocore.exceptions.ClientError:
|
||||
_LOGGER.exception("Error authenticating homelink account")
|
||||
errors["base"] = "srp_auth_failed"
|
||||
except Exception:
|
||||
_LOGGER.exception("An unexpected error occurred")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
access_token = jwt.decode(
|
||||
tokens["AuthenticationResult"]["AccessToken"],
|
||||
options={"verify_signature": False},
|
||||
)
|
||||
await self.async_set_unique_id(access_token["sub"])
|
||||
self._abort_if_unique_id_configured()
|
||||
self.external_data = {"tokens": tokens}
|
||||
return await self.async_step_creation()
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Establish MQTT connection and listen for event data."""
|
||||
"""Makes requests to the state server and stores the resulting data so that the buttons can access it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TypedDict
|
||||
|
||||
from homelink.model.device import Device
|
||||
@@ -13,6 +14,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type HomeLinkConfigEntry = ConfigEntry[HomeLinkCoordinator]
|
||||
type EventCallback = Callable[[HomeLinkEventData], None]
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/geocaching",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["geocachingapi==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -54,11 +54,7 @@ class GiosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_create_entry(
|
||||
title=gios.station_name,
|
||||
# CONF_NAME is still used, but its value is preserved
|
||||
# primarily for backward compatibility. This allows older
|
||||
# versions of the software to read the entry data without
|
||||
# raising errors.
|
||||
data={**user_input, CONF_NAME: gios.station_name},
|
||||
data=user_input,
|
||||
)
|
||||
except (ApiError, ClientConnectorError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
@@ -83,7 +79,8 @@ class GiosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
sort=True,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_NAME, default=self.hass.config.location_name): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from gios import Gios
|
||||
@@ -13,12 +12,10 @@ from gios.exceptions import GiosError
|
||||
from gios.model import GiosSensors
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import API_TIMEOUT, DOMAIN, MANUFACTURER, SCAN_INTERVAL, URL
|
||||
from .const import API_TIMEOUT, DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,21 +51,6 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator[GiosSensors]):
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
station_id = gios.station_id
|
||||
if TYPE_CHECKING:
|
||||
# Station ID is Optional in the library, but here we know it is set for sure
|
||||
# so we can safely assert it is not None for type checking purposes
|
||||
# Gios instance is created only with a valid station ID in the async_setup_entry.
|
||||
assert station_id is not None
|
||||
|
||||
self.device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, str(station_id))},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=config_entry.data[CONF_NAME],
|
||||
configuration_url=URL.format(station_id=station_id),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> GiosSensors:
|
||||
"""Update data via library."""
|
||||
try:
|
||||
|
||||
@@ -15,9 +15,10 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -35,6 +36,8 @@ from .const import (
|
||||
ATTR_SO2,
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
URL,
|
||||
)
|
||||
from .coordinator import GiosConfigEntry, GiosDataUpdateCoordinator
|
||||
|
||||
@@ -181,6 +184,8 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add a GIOS entities from a config_entry."""
|
||||
name = entry.data[CONF_NAME]
|
||||
|
||||
coordinator = entry.runtime_data.coordinator
|
||||
# Due to the change of the attribute name of one sensor, it is necessary to migrate
|
||||
# the unique_id to the new name.
|
||||
@@ -203,7 +208,7 @@ async def async_setup_entry(
|
||||
for description in SENSOR_TYPES:
|
||||
if getattr(coordinator.data, description.key) is None:
|
||||
continue
|
||||
sensors.append(GiosSensor(coordinator, description))
|
||||
sensors.append(GiosSensor(name, coordinator, description))
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
@@ -217,13 +222,19 @@ class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
coordinator: GiosDataUpdateCoordinator,
|
||||
description: GiosSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, str(coordinator.gios.station_id))},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=name,
|
||||
configuration_url=URL.format(station_id=coordinator.gios.station_id),
|
||||
)
|
||||
if description.subkey:
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.gios.station_id}-{description.key}-{description.subkey}"
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"station_id": "Measuring station"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "Config entry name, by default, this is the name of your Home Assistant instance.",
|
||||
"station_id": "The name of the measuring station where the environmental data is collected."
|
||||
},
|
||||
"title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@timmo001", "@ludeeus"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/github",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiogithubapi"],
|
||||
"requirements": ["aiogithubapi==24.6.0"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@engrbm87"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/glances",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["glances_api"],
|
||||
"requirements": ["glances-api==0.8.0"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@mletenay", "@starkillerOG"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/goodwe",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["goodwe"],
|
||||
"requirements": ["goodwe==0.4.8"]
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==12.1.2"]
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==12.1.1"]
|
||||
}
|
||||
|
||||
@@ -908,21 +908,12 @@ class StartStopTrait(_Trait):
|
||||
}
|
||||
|
||||
if domain in COVER_VALVE_DOMAINS:
|
||||
assumed_state_or_set_position = bool(
|
||||
(
|
||||
self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
& COVER_VALVE_SET_POSITION_FEATURE[domain]
|
||||
)
|
||||
or self.state.attributes.get(ATTR_ASSUMED_STATE)
|
||||
)
|
||||
|
||||
return {
|
||||
"isRunning": state
|
||||
in (
|
||||
COVER_VALVE_STATES[domain]["closing"],
|
||||
COVER_VALVE_STATES[domain]["opening"],
|
||||
)
|
||||
or assumed_state_or_set_position
|
||||
}
|
||||
|
||||
raise NotImplementedError(f"Unsupported domain {domain}")
|
||||
@@ -984,23 +975,11 @@ class StartStopTrait(_Trait):
|
||||
"""Execute a StartStop command."""
|
||||
domain = self.state.domain
|
||||
if command == COMMAND_START_STOP:
|
||||
assumed_state_or_set_position = bool(
|
||||
(
|
||||
self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
& COVER_VALVE_SET_POSITION_FEATURE[domain]
|
||||
)
|
||||
or self.state.attributes.get(ATTR_ASSUMED_STATE)
|
||||
)
|
||||
|
||||
if params["start"] is False:
|
||||
if (
|
||||
self.state.state
|
||||
in (
|
||||
COVER_VALVE_STATES[domain]["closing"],
|
||||
COVER_VALVE_STATES[domain]["opening"],
|
||||
)
|
||||
or assumed_state_or_set_position
|
||||
):
|
||||
if self.state.state in (
|
||||
COVER_VALVE_STATES[domain]["closing"],
|
||||
COVER_VALVE_STATES[domain]["opening"],
|
||||
) or self.state.attributes.get(ATTR_ASSUMED_STATE):
|
||||
await self.hass.services.async_call(
|
||||
domain,
|
||||
SERVICE_STOP_COVER_VALVE[domain],
|
||||
@@ -1013,14 +992,7 @@ class StartStopTrait(_Trait):
|
||||
ERR_ALREADY_STOPPED,
|
||||
f"{FRIENDLY_DOMAIN[domain]} is already stopped",
|
||||
)
|
||||
elif (
|
||||
self.state.state
|
||||
in (
|
||||
COVER_VALVE_STATES[domain]["open"],
|
||||
COVER_VALVE_STATES[domain]["closed"],
|
||||
)
|
||||
or assumed_state_or_set_position
|
||||
):
|
||||
else:
|
||||
await self.hass.services.async_call(
|
||||
domain,
|
||||
SERVICE_TOGGLE_COVER_VALVE[domain],
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_photos",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_photos_library_api"],
|
||||
"requirements": ["google-photos-library-api==0.12.1"]
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_tasks",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["google-api-python-client==2.71.0"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@eifinger"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_travel_time",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google", "homeassistant.helpers.location"],
|
||||
"requirements": ["google-maps-routing==0.6.15"]
|
||||
|
||||
@@ -138,7 +138,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-ble==0.44.0"]
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ from requests import RequestException
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
AUTH_API_TOKEN,
|
||||
@@ -21,25 +19,14 @@ from .const import (
|
||||
DEFAULT_PLANT_ID,
|
||||
DEFAULT_URL,
|
||||
DEPRECATED_URLS,
|
||||
DOMAIN,
|
||||
LOGIN_INVALID_AUTH_CODE,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .coordinator import GrowattConfigEntry, GrowattCoordinator
|
||||
from .models import GrowattRuntimeData
|
||||
from .services import async_register_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Growatt Server component."""
|
||||
# Register services
|
||||
await async_register_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
def get_device_list_classic(
|
||||
api: growattServer.GrowattApi, config: Mapping[str, str]
|
||||
|
||||
@@ -46,8 +46,3 @@ ERROR_INVALID_AUTH = "invalid_auth"
|
||||
|
||||
# Config flow abort reasons
|
||||
ABORT_NO_PLANTS = "no_plants"
|
||||
|
||||
# Battery modes for TOU (Time of Use) settings
|
||||
BATT_MODE_LOAD_FIRST = 0
|
||||
BATT_MODE_BATTERY_FIRST = 1
|
||||
BATT_MODE_GRID_FIRST = 2
|
||||
|
||||
@@ -12,17 +12,10 @@ import growattServer
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
BATT_MODE_BATTERY_FIRST,
|
||||
BATT_MODE_GRID_FIRST,
|
||||
BATT_MODE_LOAD_FIRST,
|
||||
DEFAULT_URL,
|
||||
DOMAIN,
|
||||
)
|
||||
from .const import DEFAULT_URL, DOMAIN
|
||||
from .models import GrowattRuntimeData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -254,134 +247,3 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.previous_values[variable] = return_value
|
||||
|
||||
return return_value
|
||||
|
||||
async def update_time_segment(
|
||||
self, segment_id: int, batt_mode: int, start_time, end_time, enabled: bool
|
||||
) -> None:
|
||||
"""Update an inverter time segment.
|
||||
|
||||
Args:
|
||||
segment_id: Time segment ID (1-9)
|
||||
batt_mode: Battery mode (0=load first, 1=battery first, 2=grid first)
|
||||
start_time: Start time (datetime.time object)
|
||||
end_time: End time (datetime.time object)
|
||||
enabled: Whether the segment is enabled
|
||||
"""
|
||||
_LOGGER.debug(
|
||||
"Updating time segment %d for device %s (mode=%d, %s-%s, enabled=%s)",
|
||||
segment_id,
|
||||
self.device_id,
|
||||
batt_mode,
|
||||
start_time,
|
||||
end_time,
|
||||
enabled,
|
||||
)
|
||||
|
||||
if self.api_version != "v1":
|
||||
raise ServiceValidationError(
|
||||
"Updating time segments requires token authentication"
|
||||
)
|
||||
|
||||
try:
|
||||
# Use V1 API for token authentication
|
||||
# The library's _process_response will raise GrowattV1ApiError if error_code != 0
|
||||
await self.hass.async_add_executor_job(
|
||||
self.api.min_write_time_segment,
|
||||
self.device_id,
|
||||
segment_id,
|
||||
batt_mode,
|
||||
start_time,
|
||||
end_time,
|
||||
enabled,
|
||||
)
|
||||
except growattServer.GrowattV1ApiError as err:
|
||||
raise HomeAssistantError(f"API error updating time segment: {err}") from err
|
||||
|
||||
# Update coordinator's cached data without making an API call (avoids rate limit)
|
||||
if self.data:
|
||||
# Update the time segment data in the cache
|
||||
self.data[f"forcedTimeStart{segment_id}"] = start_time.strftime("%H:%M")
|
||||
self.data[f"forcedTimeStop{segment_id}"] = end_time.strftime("%H:%M")
|
||||
self.data[f"time{segment_id}Mode"] = batt_mode
|
||||
self.data[f"forcedStopSwitch{segment_id}"] = 1 if enabled else 0
|
||||
|
||||
# Notify entities of the updated data (no API call)
|
||||
self.async_set_updated_data(self.data)
|
||||
|
||||
async def read_time_segments(self) -> list[dict]:
|
||||
"""Read time segments from an inverter.
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing segment information
|
||||
"""
|
||||
_LOGGER.debug("Reading time segments for device %s", self.device_id)
|
||||
|
||||
if self.api_version != "v1":
|
||||
raise ServiceValidationError(
|
||||
"Reading time segments requires token authentication"
|
||||
)
|
||||
|
||||
# Ensure we have current data
|
||||
if not self.data:
|
||||
_LOGGER.debug("Coordinator data not available, triggering refresh")
|
||||
await self.async_refresh()
|
||||
|
||||
time_segments = []
|
||||
|
||||
# Extract time segments from coordinator data
|
||||
for i in range(1, 10): # Segments 1-9
|
||||
segment = self._parse_time_segment(i)
|
||||
time_segments.append(segment)
|
||||
|
||||
return time_segments
|
||||
|
||||
def _parse_time_segment(self, segment_id: int) -> dict:
|
||||
"""Parse a single time segment from coordinator data."""
|
||||
# Get raw time values - these should always be present from the API
|
||||
start_time_raw = self.data.get(f"forcedTimeStart{segment_id}")
|
||||
end_time_raw = self.data.get(f"forcedTimeStop{segment_id}")
|
||||
|
||||
# Handle 'null' or empty values from API
|
||||
if start_time_raw in ("null", None, ""):
|
||||
start_time_raw = "0:0"
|
||||
if end_time_raw in ("null", None, ""):
|
||||
end_time_raw = "0:0"
|
||||
|
||||
# Format times with leading zeros (HH:MM)
|
||||
start_time = self._format_time(str(start_time_raw))
|
||||
end_time = self._format_time(str(end_time_raw))
|
||||
|
||||
# Get battery mode
|
||||
batt_mode_int = int(
|
||||
self.data.get(f"time{segment_id}Mode", BATT_MODE_LOAD_FIRST)
|
||||
)
|
||||
|
||||
# Map numeric mode to string key (matches update_time_segment input format)
|
||||
mode_map = {
|
||||
BATT_MODE_LOAD_FIRST: "load_first",
|
||||
BATT_MODE_BATTERY_FIRST: "battery_first",
|
||||
BATT_MODE_GRID_FIRST: "grid_first",
|
||||
}
|
||||
batt_mode = mode_map.get(batt_mode_int, "load_first")
|
||||
|
||||
# Get enabled status
|
||||
enabled = bool(int(self.data.get(f"forcedStopSwitch{segment_id}", 0)))
|
||||
|
||||
return {
|
||||
"segment_id": segment_id,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"batt_mode": batt_mode,
|
||||
"enabled": enabled,
|
||||
}
|
||||
|
||||
def _format_time(self, time_raw: str) -> str:
|
||||
"""Format time string to HH:MM format."""
|
||||
try:
|
||||
parts = str(time_raw).split(":")
|
||||
hour = int(parts[0])
|
||||
minute = int(parts[1])
|
||||
except (ValueError, IndexError):
|
||||
return "00:00"
|
||||
else:
|
||||
return f"{hour:02d}:{minute:02d}"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"services": {
|
||||
"read_time_segments": {
|
||||
"service": "mdi:clock-outline"
|
||||
},
|
||||
"update_time_segment": {
|
||||
"service": "mdi:clock-edit"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@johanzander"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["growattServer"],
|
||||
"requirements": ["growattServer==1.7.1"]
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
"""Service handlers for Growatt Server integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import (
|
||||
BATT_MODE_BATTERY_FIRST,
|
||||
BATT_MODE_GRID_FIRST,
|
||||
BATT_MODE_LOAD_FIRST,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .coordinator import GrowattCoordinator
|
||||
|
||||
|
||||
async def async_register_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for Growatt Server integration."""
|
||||
|
||||
def get_min_coordinators() -> dict[str, GrowattCoordinator]:
|
||||
"""Get all MIN coordinators with V1 API from loaded config entries."""
|
||||
min_coordinators: dict[str, GrowattCoordinator] = {}
|
||||
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
continue
|
||||
|
||||
# Add MIN coordinators from this entry
|
||||
for coord in entry.runtime_data.devices.values():
|
||||
if coord.device_type == "min" and coord.api_version == "v1":
|
||||
min_coordinators[coord.device_id] = coord
|
||||
|
||||
return min_coordinators
|
||||
|
||||
def get_coordinator(device_id: str) -> GrowattCoordinator:
|
||||
"""Get coordinator by device_id.
|
||||
|
||||
Args:
|
||||
device_id: Device registry ID (not serial number)
|
||||
"""
|
||||
# Get current coordinators (they may have changed since service registration)
|
||||
min_coordinators = get_min_coordinators()
|
||||
|
||||
if not min_coordinators:
|
||||
raise ServiceValidationError(
|
||||
"No MIN devices with token authentication are configured. "
|
||||
"Services require MIN devices with V1 API access."
|
||||
)
|
||||
|
||||
# Device registry ID provided - map to serial number
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
|
||||
if not device_entry:
|
||||
raise ServiceValidationError(f"Device '{device_id}' not found")
|
||||
|
||||
# Extract serial number from device identifiers
|
||||
serial_number = None
|
||||
for identifier in device_entry.identifiers:
|
||||
if identifier[0] == DOMAIN:
|
||||
serial_number = identifier[1]
|
||||
break
|
||||
|
||||
if not serial_number:
|
||||
raise ServiceValidationError(
|
||||
f"Device '{device_id}' is not a Growatt device"
|
||||
)
|
||||
|
||||
# Find coordinator by serial number
|
||||
if serial_number not in min_coordinators:
|
||||
raise ServiceValidationError(
|
||||
f"MIN device '{serial_number}' not found or not configured for services"
|
||||
)
|
||||
|
||||
return min_coordinators[serial_number]
|
||||
|
||||
async def handle_update_time_segment(call: ServiceCall) -> None:
|
||||
"""Handle update_time_segment service call."""
|
||||
segment_id: int = int(call.data["segment_id"])
|
||||
batt_mode_str: str = call.data["batt_mode"]
|
||||
start_time_str: str = call.data["start_time"]
|
||||
end_time_str: str = call.data["end_time"]
|
||||
enabled: bool = call.data["enabled"]
|
||||
device_id: str = call.data["device_id"]
|
||||
|
||||
# Validate segment_id range
|
||||
if not 1 <= segment_id <= 9:
|
||||
raise ServiceValidationError(
|
||||
f"segment_id must be between 1 and 9, got {segment_id}"
|
||||
)
|
||||
|
||||
# Validate and convert batt_mode string to integer
|
||||
valid_modes = {
|
||||
"load_first": BATT_MODE_LOAD_FIRST,
|
||||
"battery_first": BATT_MODE_BATTERY_FIRST,
|
||||
"grid_first": BATT_MODE_GRID_FIRST,
|
||||
}
|
||||
if batt_mode_str not in valid_modes:
|
||||
raise ServiceValidationError(
|
||||
f"batt_mode must be one of {list(valid_modes.keys())}, got '{batt_mode_str}'"
|
||||
)
|
||||
batt_mode: int = valid_modes[batt_mode_str]
|
||||
|
||||
# Convert time strings to datetime.time objects
|
||||
# UI time selector sends HH:MM:SS, but we only need HH:MM (strip seconds)
|
||||
try:
|
||||
# Take only HH:MM part (ignore seconds if present)
|
||||
start_parts = start_time_str.split(":")
|
||||
start_time_hhmm = f"{start_parts[0]}:{start_parts[1]}"
|
||||
start_time = datetime.strptime(start_time_hhmm, "%H:%M").time()
|
||||
except (ValueError, IndexError) as err:
|
||||
raise ServiceValidationError(
|
||||
"start_time must be in HH:MM or HH:MM:SS format"
|
||||
) from err
|
||||
|
||||
try:
|
||||
# Take only HH:MM part (ignore seconds if present)
|
||||
end_parts = end_time_str.split(":")
|
||||
end_time_hhmm = f"{end_parts[0]}:{end_parts[1]}"
|
||||
end_time = datetime.strptime(end_time_hhmm, "%H:%M").time()
|
||||
except (ValueError, IndexError) as err:
|
||||
raise ServiceValidationError(
|
||||
"end_time must be in HH:MM or HH:MM:SS format"
|
||||
) from err
|
||||
|
||||
# Get the appropriate MIN coordinator
|
||||
coordinator: GrowattCoordinator = get_coordinator(device_id)
|
||||
|
||||
await coordinator.update_time_segment(
|
||||
segment_id,
|
||||
batt_mode,
|
||||
start_time,
|
||||
end_time,
|
||||
enabled,
|
||||
)
|
||||
|
||||
async def handle_read_time_segments(call: ServiceCall) -> dict[str, Any]:
|
||||
"""Handle read_time_segments service call."""
|
||||
device_id: str = call.data["device_id"]
|
||||
|
||||
# Get the appropriate MIN coordinator
|
||||
coordinator: GrowattCoordinator = get_coordinator(device_id)
|
||||
|
||||
time_segments: list[dict[str, Any]] = await coordinator.read_time_segments()
|
||||
|
||||
return {"time_segments": time_segments}
|
||||
|
||||
# Register services without schema - services.yaml will provide UI definition
|
||||
# Schema validation happens in the handler functions
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"update_time_segment",
|
||||
handle_update_time_segment,
|
||||
supports_response=SupportsResponse.NONE,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"read_time_segments",
|
||||
handle_read_time_segments,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
@@ -1,50 +0,0 @@
|
||||
# Service definitions for Growatt Server integration
|
||||
|
||||
update_time_segment:
|
||||
fields:
|
||||
segment_id:
|
||||
required: true
|
||||
example: 1
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 9
|
||||
mode: box
|
||||
batt_mode:
|
||||
required: true
|
||||
example: "load_first"
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "load_first"
|
||||
- "battery_first"
|
||||
- "grid_first"
|
||||
translation_key: batt_mode
|
||||
start_time:
|
||||
required: true
|
||||
example: "08:00"
|
||||
selector:
|
||||
time:
|
||||
end_time:
|
||||
required: true
|
||||
example: "12:00"
|
||||
selector:
|
||||
time:
|
||||
enabled:
|
||||
required: true
|
||||
example: true
|
||||
selector:
|
||||
boolean:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: growatt_server
|
||||
|
||||
read_time_segments:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: growatt_server
|
||||
@@ -523,56 +523,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"batt_mode": {
|
||||
"options": {
|
||||
"battery_first": "Battery first",
|
||||
"grid_first": "Grid first",
|
||||
"load_first": "Load first"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"read_time_segments": {
|
||||
"description": "Read all time segments from a supported inverter.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"description": "The Growatt device to perform the action on.",
|
||||
"name": "Device"
|
||||
}
|
||||
},
|
||||
"name": "Read time segments"
|
||||
},
|
||||
"update_time_segment": {
|
||||
"description": "Update a time segment for supported inverters.",
|
||||
"fields": {
|
||||
"batt_mode": {
|
||||
"description": "Battery operation mode for this time segment.",
|
||||
"name": "Battery mode"
|
||||
},
|
||||
"device_id": {
|
||||
"description": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::description%]",
|
||||
"name": "[%key:component::growatt_server::services::read_time_segments::fields::device_id::name%]"
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Whether this time segment is active.",
|
||||
"name": "Enabled"
|
||||
},
|
||||
"end_time": {
|
||||
"description": "End time for the segment (HH:MM format).",
|
||||
"name": "End time"
|
||||
},
|
||||
"segment_id": {
|
||||
"description": "Time segment ID (1-9).",
|
||||
"name": "Segment ID"
|
||||
},
|
||||
"start_time": {
|
||||
"description": "Start time for the segment (HH:MM format).",
|
||||
"name": "Start time"
|
||||
}
|
||||
},
|
||||
"name": "Update time segment"
|
||||
}
|
||||
},
|
||||
"title": "Growatt Server"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@bestycame"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hanna",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["hanna-cloud==0.0.7"]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user