forked from home-assistant/core
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8419e6429a | |||
| bbe804cef3 |
@@ -1,100 +0,0 @@
|
||||
# Instructions for GitHub Copilot
|
||||
|
||||
This repository holds the core of Home Assistant, a Python 3 based home
|
||||
automation application.
|
||||
|
||||
- Python code must be compatible with Python 3.13
|
||||
- Use the newest Python language features if possible:
|
||||
- Pattern matching
|
||||
- Type hints
|
||||
- f-strings for string formatting over `%` or `.format()`
|
||||
- Dataclasses
|
||||
- Walrus operator
|
||||
- Code quality tools:
|
||||
- Formatting: Ruff
|
||||
- Linting: PyLint and Ruff
|
||||
- Type checking: MyPy
|
||||
- Testing: pytest with plain functions and fixtures
|
||||
- Inline code documentation:
|
||||
- File headers should be short and concise:
|
||||
```python
|
||||
"""Integration for Peblar EV chargers."""
|
||||
```
|
||||
- Every method and function needs a docstring:
|
||||
```python
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bool:
|
||||
"""Set up Peblar from a config entry."""
|
||||
...
|
||||
```
|
||||
- All code and comments and other text are written in American English
|
||||
- Follow existing code style patterns as much as possible
|
||||
- Core locations:
|
||||
- Shared constants: `homeassistant/const.py`, use them instead of hardcoding
|
||||
strings or creating duplicate integration constants.
|
||||
- Integration files:
|
||||
- Constants: `homeassistant/components/{domain}/const.py`
|
||||
- Models: `homeassistant/components/{domain}/models.py`
|
||||
- Coordinator: `homeassistant/components/{domain}/coordinator.py`
|
||||
- Config flow: `homeassistant/components/{domain}/config_flow.py`
|
||||
- Platform code: `homeassistant/components/{domain}/{platform}.py`
|
||||
- All external I/O operations must be async
|
||||
- Async patterns:
|
||||
- Avoid sleeping in loops
|
||||
- Avoid awaiting in loops, gather instead
|
||||
- No blocking calls
|
||||
- Polling:
|
||||
- Follow update coordinator pattern, when possible
|
||||
- Polling interval may not be configurable by the user
|
||||
- For local network polling, the minimum interval is 5 seconds
|
||||
- For cloud polling, the minimum interval is 60 seconds
|
||||
- Error handling:
|
||||
- Use specific exceptions from `homeassistant.exceptions`
|
||||
- Setup failures:
|
||||
- Temporary: Raise `ConfigEntryNotReady`
|
||||
- Permanent: Use `ConfigEntryError`
|
||||
- Logging:
|
||||
- Message format:
|
||||
- No periods at end
|
||||
- No integration names or domains (added automatically)
|
||||
- No sensitive data (keys, tokens, passwords), even when those are incorrect.
|
||||
- Be very restrictive on the use of logging info messages, use debug for
|
||||
anything which is not targeting the user.
|
||||
- Use lazy logging (no f-strings):
|
||||
```python
|
||||
_LOGGER.debug("This is a log message with %s", variable)
|
||||
```
|
||||
- Entities:
|
||||
- Ensure unique IDs for state persistence:
|
||||
- Unique IDs should not contain values that are subject to user or network change.
|
||||
- An ID needs to be unique per platform, not per integration.
|
||||
- The ID does not have to contain the integration domain or platform.
|
||||
- Acceptable examples:
|
||||
- Serial number of a device
|
||||
- MAC address of a device formatted using `homeassistant.helpers.device_registry.format_mac`
|
||||
Do not obtain the MAC address through arp cache of local network access,
|
||||
only use the MAC address provided by discovery or the device itself.
|
||||
- Unique identifier that is physically printed on the device or burned into an EEPROM
|
||||
- Not acceptable examples:
|
||||
- IP Address
|
||||
- Device name
|
||||
- Hostname
|
||||
- URL
|
||||
- Email address
|
||||
- Username
|
||||
- For entities that are setup by a config entry, the config entry ID
|
||||
can be used as a last resort if no other Unique ID is available.
|
||||
For example: `f"{entry.entry_id}-battery"`
|
||||
- If the state value is unknown, use `None`
|
||||
- Do not use the `unavailable` string as a state value,
|
||||
implement the `available()` property method instead
|
||||
- Do not use the `unknown` string as a state value, use `None` instead
|
||||
- Extra entity state attributes:
|
||||
- The keys of all state attributes should always be present
|
||||
- If the value is unknown, use `None`
|
||||
- Provide descriptive state attributes
|
||||
- Testing:
|
||||
- Test location: `tests/components/{domain}/`
|
||||
- Use pytest fixtures from `tests.common`
|
||||
- Mock external dependencies
|
||||
- Use snapshots for complex data
|
||||
- Follow existing test patterns
|
||||
@@ -324,7 +324,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.8.1
|
||||
uses: sigstore/cosign-installer@v3.8.0
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
+22
-22
@@ -240,7 +240,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.1
|
||||
uses: actions/cache@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.2.1
|
||||
uses: actions/cache@v4.2.0
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -286,7 +286,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -326,7 +326,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -366,7 +366,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -375,7 +375,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -482,7 +482,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.1
|
||||
uses: actions/cache@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -490,7 +490,7 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4.2.1
|
||||
uses: actions/cache@v4.2.0
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@@ -578,7 +578,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -611,7 +611,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -649,7 +649,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -692,7 +692,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -739,7 +739,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -791,7 +791,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -799,7 +799,7 @@ jobs:
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.2.1
|
||||
uses: actions/cache@v4.2.0
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -865,7 +865,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -929,7 +929,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1051,7 +1051,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1181,7 +1181,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1328,7 +1328,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.1
|
||||
uses: actions/cache/restore@v4.2.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.7
|
||||
rev: v0.9.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -438,7 +438,6 @@ homeassistant.components.select.*
|
||||
homeassistant.components.sensibo.*
|
||||
homeassistant.components.sensirion_ble.*
|
||||
homeassistant.components.sensor.*
|
||||
homeassistant.components.sensorpush_cloud.*
|
||||
homeassistant.components.sensoterra.*
|
||||
homeassistant.components.senz.*
|
||||
homeassistant.components.sfr_box.*
|
||||
|
||||
Vendored
+1
-9
@@ -42,14 +42,6 @@
|
||||
"--picked"
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Home Assistant: Debug Current Test File",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "pytest",
|
||||
"console": "integratedTerminal",
|
||||
"args": ["-vv", "${file}"]
|
||||
},
|
||||
{
|
||||
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
|
||||
// See https://www.home-assistant.io/integrations/debugpy/
|
||||
@@ -85,4 +77,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Generated
-2
@@ -1342,8 +1342,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sensorpro/ @bdraco
|
||||
/homeassistant/components/sensorpush/ @bdraco
|
||||
/tests/components/sensorpush/ @bdraco
|
||||
/homeassistant/components/sensorpush_cloud/ @sstallion
|
||||
/tests/components/sensorpush_cloud/ @sstallion
|
||||
/homeassistant/components/sensoterra/ @markruys
|
||||
/tests/components/sensoterra/ @markruys
|
||||
/homeassistant/components/sentry/ @dcramer @frenck
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "sensorpush",
|
||||
"name": "SensorPush",
|
||||
"integrations": ["sensorpush", "sensorpush_cloud"]
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
@@ -28,13 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
|
||||
return True
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["arcam"],
|
||||
"requirements": ["arcam-fmj==1.8.1"],
|
||||
"requirements": ["arcam-fmj==1.8.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
from queue import Empty, Queue
|
||||
from threading import Thread
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
from typing import Any, Literal, cast
|
||||
import wave
|
||||
|
||||
import hass_nabucasa
|
||||
@@ -30,7 +30,7 @@ from homeassistant.components import (
|
||||
from homeassistant.components.tts import (
|
||||
generate_media_source_id as tts_generate_media_source_id,
|
||||
)
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL
|
||||
from homeassistant.const import MATCH_ALL
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import chat_session, intent
|
||||
@@ -81,9 +81,6 @@ from .error import (
|
||||
)
|
||||
from .vad import AudioBuffer, VoiceActivityTimeout, VoiceCommandSegmenter, chunk_samples
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from hassil.recognize import RecognizeResult
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STORAGE_KEY = f"{DOMAIN}.pipelines"
|
||||
@@ -126,12 +123,6 @@ STORED_PIPELINE_RUNS = 10
|
||||
SAVE_DELAY = 10
|
||||
|
||||
|
||||
@callback
|
||||
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
|
||||
"""Filter out intents that are not local fallback."""
|
||||
return result.intent.name in (intent.INTENT_GET_STATE, intent.INTENT_NEVERMIND)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_resolve_default_pipeline_settings(
|
||||
hass: HomeAssistant,
|
||||
@@ -1093,22 +1084,10 @@ class PipelineRun:
|
||||
)
|
||||
intent_response.async_set_speech(trigger_response_text)
|
||||
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None
|
||||
# If the LLM has API access, we filter out some sentences that are
|
||||
# interfering with LLM operation.
|
||||
if (
|
||||
intent_agent_state := self.hass.states.get(self.intent_agent)
|
||||
) and intent_agent_state.attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES, 0
|
||||
) & conversation.ConversationEntityFeature.CONTROL:
|
||||
intent_filter = _async_local_fallback_intent_filter
|
||||
|
||||
# Try local intents first, if preferred.
|
||||
elif self.pipeline.prefer_local_intents and (
|
||||
intent_response := await conversation.async_handle_intents(
|
||||
self.hass,
|
||||
user_input,
|
||||
intent_filter=intent_filter,
|
||||
self.hass, user_input
|
||||
)
|
||||
):
|
||||
# Local intent matched
|
||||
|
||||
@@ -16,7 +16,7 @@ from .agent import (
|
||||
BackupAgentPlatformProtocol,
|
||||
LocalBackupAgent,
|
||||
)
|
||||
from .config import BackupConfig, CreateBackupParametersDict
|
||||
from .config import BackupConfig
|
||||
from .const import DATA_MANAGER, DOMAIN
|
||||
from .http import async_register_http_views
|
||||
from .manager import (
|
||||
@@ -55,7 +55,6 @@ __all__ = [
|
||||
"BackupReaderWriter",
|
||||
"BackupReaderWriterError",
|
||||
"CreateBackupEvent",
|
||||
"CreateBackupParametersDict",
|
||||
"CreateBackupStage",
|
||||
"CreateBackupState",
|
||||
"Folder",
|
||||
|
||||
@@ -154,8 +154,7 @@ class BackupConfig:
|
||||
self.data.retention.apply(self._manager)
|
||||
self.data.schedule.apply(self._manager)
|
||||
|
||||
@callback
|
||||
def update(
|
||||
async def update(
|
||||
self,
|
||||
*,
|
||||
agents: dict[str, AgentParametersDict] | UndefinedType = UNDEFINED,
|
||||
|
||||
@@ -1870,7 +1870,7 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
and "hassio.local" in create_backup.agent_ids
|
||||
):
|
||||
automatic_agents = [self._local_agent_id, *automatic_agents]
|
||||
config.update(
|
||||
await config.update(
|
||||
create_backup=CreateBackupParametersDict(
|
||||
agent_ids=automatic_agents,
|
||||
include_addons=None,
|
||||
|
||||
@@ -104,15 +104,12 @@ def read_backup(backup_path: Path) -> AgentBackup:
|
||||
bool, homeassistant.get("exclude_database", False)
|
||||
)
|
||||
|
||||
extra_metadata = cast(dict[str, bool | str], data.get("extra", {}))
|
||||
date = extra_metadata.get("supervisor.backup_request_date", data["date"])
|
||||
|
||||
return AgentBackup(
|
||||
addons=addons,
|
||||
backup_id=cast(str, data["slug"]),
|
||||
database_included=database_included,
|
||||
date=cast(str, date),
|
||||
extra_metadata=extra_metadata,
|
||||
date=cast(str, data["date"]),
|
||||
extra_metadata=cast(dict[str, bool | str], data.get("extra", {})),
|
||||
folders=folders,
|
||||
homeassistant_included=homeassistant_included,
|
||||
homeassistant_version=homeassistant_version,
|
||||
|
||||
@@ -346,7 +346,6 @@ async def handle_config_info(
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
@@ -388,7 +387,8 @@ async def handle_config_info(
|
||||
),
|
||||
}
|
||||
)
|
||||
def handle_config_update(
|
||||
@websocket_api.async_response
|
||||
async def handle_config_update(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
@@ -398,7 +398,7 @@ def handle_config_update(
|
||||
changes = dict(msg)
|
||||
changes.pop("id")
|
||||
changes.pop("type")
|
||||
manager.config.update(**changes)
|
||||
await manager.config.update(**changes)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.22.3",
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bleak-retry-connector==3.8.1",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.4",
|
||||
"bluetooth-auto-recovery==1.4.2",
|
||||
"bluetooth-data-tools==1.23.4",
|
||||
"dbus-fast==2.33.0",
|
||||
"habluetooth==3.22.1"
|
||||
"habluetooth==3.22.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"services": {
|
||||
"set_fan_speed_tracked_state": {
|
||||
"name": "Set fan speed tracked state",
|
||||
"description": "Sets the tracked fan speed for a Bond fan.",
|
||||
"description": "Sets the tracked fan speed for a bond fan.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"set_switch_power_tracked_state": {
|
||||
"name": "Set switch power tracked state",
|
||||
"description": "Sets the tracked power state of a Bond switch.",
|
||||
"description": "Sets the tracked power state of a bond switch.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"set_light_power_tracked_state": {
|
||||
"name": "Set light power tracked state",
|
||||
"description": "Sets the tracked power state of a Bond light.",
|
||||
"description": "Sets the tracked power state of a bond light.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"set_light_brightness_tracked_state": {
|
||||
"name": "Set light brightness tracked state",
|
||||
"description": "Sets the tracked brightness state of a Bond light.",
|
||||
"description": "Sets the tracked brightness state of a bond light.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
@@ -87,15 +87,15 @@
|
||||
},
|
||||
"start_increasing_brightness": {
|
||||
"name": "Start increasing brightness",
|
||||
"description": "Starts increasing the brightness of the light (deprecated)."
|
||||
"description": "Start increasing the brightness of the light. (deprecated)."
|
||||
},
|
||||
"start_decreasing_brightness": {
|
||||
"name": "Start decreasing brightness",
|
||||
"description": "Starts decreasing the brightness of the light (deprecated)."
|
||||
"description": "Start decreasing the brightness of the light. (deprecated)."
|
||||
},
|
||||
"stop": {
|
||||
"name": "[%key:common::action::stop%]",
|
||||
"description": "Stops any in-progress action and empty the queue (deprecated)."
|
||||
"description": "Stop any in-progress action and empty the queue. (deprecated)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
import re
|
||||
from typing import Literal
|
||||
|
||||
from hassil.recognize import RecognizeResult
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -243,10 +241,7 @@ async def async_handle_sentence_triggers(
|
||||
|
||||
|
||||
async def async_handle_intents(
|
||||
hass: HomeAssistant,
|
||||
user_input: ConversationInput,
|
||||
*,
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None,
|
||||
hass: HomeAssistant, user_input: ConversationInput
|
||||
) -> intent.IntentResponse | None:
|
||||
"""Try to match input against registered intents and return response.
|
||||
|
||||
@@ -255,9 +250,7 @@ async def async_handle_intents(
|
||||
default_agent = async_get_agent(hass)
|
||||
assert isinstance(default_agent, DefaultAgent)
|
||||
|
||||
return await default_agent.async_handle_intents(
|
||||
user_input, intent_filter=intent_filter
|
||||
)
|
||||
return await default_agent.async_handle_intents(user_input)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
@@ -185,6 +185,21 @@ class IntentCache:
|
||||
self.cache.clear()
|
||||
|
||||
|
||||
def _get_language_variations(language: str) -> Iterable[str]:
|
||||
"""Generate language codes with and without region."""
|
||||
yield language
|
||||
|
||||
parts = re.split(r"([-_])", language)
|
||||
if len(parts) == 3:
|
||||
lang, sep, region = parts
|
||||
if sep == "_":
|
||||
# en_US -> en-US
|
||||
yield f"{lang}-{region}"
|
||||
|
||||
# en-US -> en
|
||||
yield lang
|
||||
|
||||
|
||||
async def async_setup_default_agent(
|
||||
hass: core.HomeAssistant,
|
||||
entity_component: EntityComponent[ConversationEntity],
|
||||
@@ -1309,8 +1324,6 @@ class DefaultAgent(ConversationEntity):
|
||||
async def async_handle_intents(
|
||||
self,
|
||||
user_input: ConversationInput,
|
||||
*,
|
||||
intent_filter: Callable[[RecognizeResult], bool] | None = None,
|
||||
) -> intent.IntentResponse | None:
|
||||
"""Try to match sentence against registered intents and return response.
|
||||
|
||||
@@ -1318,9 +1331,7 @@ class DefaultAgent(ConversationEntity):
|
||||
Returns None if no match or a matching error occurred.
|
||||
"""
|
||||
result = await self.async_recognize_intent(user_input, strict_intents_only=True)
|
||||
if not isinstance(result, RecognizeResult) or (
|
||||
intent_filter is not None and intent_filter(result)
|
||||
):
|
||||
if not isinstance(result, RecognizeResult):
|
||||
# No error message on failed match
|
||||
return None
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.2.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.1.0"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
@@ -28,13 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
|
||||
return True
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
|
||||
from .entity import EnvoyBaseEntity, exception_handler
|
||||
from .entity import EnvoyBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -132,7 +132,6 @@ class EnvoyRelayNumberEntity(EnvoyBaseEntity, NumberEntity):
|
||||
self.data.dry_contact_settings[self._relay_id]
|
||||
)
|
||||
|
||||
@exception_handler
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the relay."""
|
||||
await self.envoy.update_dry_contact(
|
||||
@@ -186,7 +185,6 @@ class EnvoyStorageSettingsNumberEntity(EnvoyBaseEntity, NumberEntity):
|
||||
assert self.data.tariff.storage_settings is not None
|
||||
return self.entity_description.value_fn(self.data.tariff.storage_settings)
|
||||
|
||||
@exception_handler
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the storage setting."""
|
||||
await self.entity_description.update_fn(self.envoy, value)
|
||||
|
||||
@@ -5,4 +5,3 @@ ATTR_STATION = "station"
|
||||
CONF_STATION = "station"
|
||||
CONF_TITLE = "title"
|
||||
DOMAIN = "environment_canada"
|
||||
SERVICE_ENVIRONMENT_CANADA_FORECASTS = "get_forecasts"
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"services": {
|
||||
"set_radar_type": {
|
||||
"service": "mdi:radar"
|
||||
},
|
||||
"get_forecasts": {
|
||||
"service": "mdi:weather-cloudy-clock"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
get_forecasts:
|
||||
target:
|
||||
entity:
|
||||
integration: environment_canada
|
||||
domain: weather
|
||||
|
||||
set_radar_type:
|
||||
target:
|
||||
entity:
|
||||
|
||||
@@ -113,10 +113,6 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_forecasts": {
|
||||
"name": "Get forecasts",
|
||||
"description": "Retrieves the forecast from selected weather services."
|
||||
},
|
||||
"set_radar_type": {
|
||||
"name": "Set radar type",
|
||||
"description": "Sets the type of radar image to retrieve.",
|
||||
|
||||
@@ -35,16 +35,11 @@ from homeassistant.const import (
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import entity_platform, entity_registry as er
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, SERVICE_ENVIRONMENT_CANADA_FORECASTS
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ECConfigEntry, ECDataUpdateCoordinator
|
||||
|
||||
# Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/
|
||||
@@ -83,14 +78,6 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities([ECWeatherEntity(config_entry.runtime_data.weather_coordinator)])
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_ENVIRONMENT_CANADA_FORECASTS,
|
||||
None,
|
||||
"_async_environment_canada_forecasts",
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
|
||||
def _calculate_unique_id(config_entry_unique_id: str | None, hourly: bool) -> str:
|
||||
"""Calculate unique ID."""
|
||||
@@ -198,23 +185,6 @@ class ECWeatherEntity(
|
||||
"""Return the hourly forecast in native units."""
|
||||
return get_forecast(self.ec_data, True)
|
||||
|
||||
def _async_environment_canada_forecasts(self) -> ServiceResponse:
|
||||
"""Return the native Environment Canada forecast."""
|
||||
daily = []
|
||||
for f in self.ec_data.daily_forecasts:
|
||||
day = f.copy()
|
||||
day["timestamp"] = day["timestamp"].isoformat()
|
||||
daily.append(day)
|
||||
|
||||
hourly = []
|
||||
for f in self.ec_data.hourly_forecasts:
|
||||
hour = f.copy()
|
||||
hour["timestamp"] = hour["period"].isoformat()
|
||||
del hour["period"]
|
||||
hourly.append(hour)
|
||||
|
||||
return {"daily_forecast": daily, "hourly_forecast": hourly}
|
||||
|
||||
|
||||
def get_forecast(ec_data, hourly) -> list[Forecast] | None:
|
||||
"""Build the forecast array."""
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -108,7 +108,8 @@ class ESPHomeDashboardManager:
|
||||
|
||||
reloads = [
|
||||
hass.config_entries.async_reload(entry.entry_id)
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.state is ConfigEntryState.LOADED
|
||||
]
|
||||
# Re-auth flows will check the dashboard for encryption key when the form is requested
|
||||
# but we only trigger reauth if the dashboard is available.
|
||||
|
||||
@@ -111,13 +111,7 @@ class FlexitClimateEntity(FlexitEntity, ClimateEntity):
|
||||
else:
|
||||
await self.device.set_air_temp_setpoint_home(temperature)
|
||||
except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_temperature",
|
||||
translation_placeholders={
|
||||
"temperature": str(temperature),
|
||||
},
|
||||
) from exc
|
||||
raise HomeAssistantError from exc
|
||||
finally:
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flexit_bacnet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["flexit_bacnet==2.2.3"]
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration doesn't require any form of authentication.
|
||||
test-coverage: done
|
||||
test-coverage: todo
|
||||
# Gold
|
||||
entity-translations: done
|
||||
entity-device-class: done
|
||||
|
||||
@@ -130,9 +130,6 @@
|
||||
"set_preset_mode": {
|
||||
"message": "Failed to set preset mode {preset}."
|
||||
},
|
||||
"set_temperature": {
|
||||
"message": "Failed to set temperature {temperature}."
|
||||
},
|
||||
"set_hvac_mode": {
|
||||
"message": "Failed to set HVAC mode {mode}."
|
||||
},
|
||||
|
||||
@@ -85,8 +85,6 @@ async def async_setup_entry(
|
||||
class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
"""The thermostat class for FRITZ!SmartHome thermostats."""
|
||||
|
||||
_attr_max_temp = MAX_TEMPERATURE
|
||||
_attr_min_temp = MIN_TEMPERATURE
|
||||
_attr_precision = PRECISION_HALVES
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_translation_key = "thermostat"
|
||||
@@ -137,13 +135,11 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is HVACMode.OFF:
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
if target_temp == OFF_API_TEMPERATURE:
|
||||
target_temp = OFF_REPORT_SET_TEMPERATURE
|
||||
elif target_temp == ON_API_TEMPERATURE:
|
||||
target_temp = ON_REPORT_SET_TEMPERATURE
|
||||
elif target_temp is not None:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.data.set_target_temperature, target_temp, True
|
||||
)
|
||||
@@ -173,12 +169,12 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_hvac_while_active_mode",
|
||||
)
|
||||
if self.hvac_mode is hvac_mode:
|
||||
if self.hvac_mode == hvac_mode:
|
||||
LOGGER.debug(
|
||||
"%s is already in requested hvac mode %s", self.name, hvac_mode
|
||||
)
|
||||
return
|
||||
if hvac_mode is HVACMode.OFF:
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
|
||||
else:
|
||||
if value_scheduled_preset(self.data) == PRESET_ECO:
|
||||
@@ -212,6 +208,16 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
elif preset_mode == PRESET_ECO:
|
||||
await self.async_set_temperature(temperature=self.data.eco_temperature)
|
||||
|
||||
@property
|
||||
def min_temp(self) -> int:
|
||||
"""Return the minimum temperature."""
|
||||
return MIN_TEMPERATURE
|
||||
|
||||
@property
|
||||
def max_temp(self) -> int:
|
||||
"""Return the maximum temperature."""
|
||||
return MAX_TEMPERATURE
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
||||
"""Return the device specific state attributes."""
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyfritzhome"],
|
||||
"requirements": ["pyfritzhome==0.6.15"],
|
||||
"requirements": ["pyfritzhome==0.6.14"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:fritzbox:1"
|
||||
|
||||
@@ -341,7 +341,7 @@ def get_next_departure(
|
||||
{tomorrow_order}
|
||||
origin_stop_time.departure_time
|
||||
LIMIT :limit
|
||||
""" # noqa: S608
|
||||
"""
|
||||
result = schedule.engine.connect().execute(
|
||||
text(sql_query),
|
||||
{
|
||||
|
||||
@@ -119,13 +119,12 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
assert self.todo_items
|
||||
|
||||
if previous_uid:
|
||||
pos = self.todo_items.index(
|
||||
next(item for item in self.todo_items if item.uid == previous_uid)
|
||||
pos = (
|
||||
self.todo_items.index(
|
||||
next(item for item in self.todo_items if item.uid == previous_uid)
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
if pos < self.todo_items.index(
|
||||
next(item for item in self.todo_items if item.uid == uid)
|
||||
):
|
||||
pos += 1
|
||||
else:
|
||||
pos = 0
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ from homeassistant.components.backup import (
|
||||
BackupReaderWriter,
|
||||
BackupReaderWriterError,
|
||||
CreateBackupEvent,
|
||||
CreateBackupParametersDict,
|
||||
CreateBackupStage,
|
||||
CreateBackupState,
|
||||
Folder,
|
||||
@@ -636,25 +635,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
||||
unsub()
|
||||
|
||||
async def async_validate_config(self, *, config: BackupConfig) -> None:
|
||||
"""Validate backup config.
|
||||
|
||||
Replace the core backup agent with the hassio default agent.
|
||||
"""
|
||||
core_agent_id = "backup.local"
|
||||
create_backup = config.data.create_backup
|
||||
if core_agent_id not in create_backup.agent_ids:
|
||||
_LOGGER.debug("Backup settings don't need to be adjusted")
|
||||
return
|
||||
|
||||
default_agent = await _default_agent(self._client)
|
||||
_LOGGER.info("Adjusting backup settings to not include core backup location")
|
||||
automatic_agents = [
|
||||
agent_id if agent_id != core_agent_id else default_agent
|
||||
for agent_id in create_backup.agent_ids
|
||||
]
|
||||
config.update(
|
||||
create_backup=CreateBackupParametersDict(agent_ids=automatic_agents)
|
||||
)
|
||||
"""Validate backup config."""
|
||||
|
||||
@callback
|
||||
def _async_listen_job_events(
|
||||
|
||||
@@ -5,22 +5,24 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pyheos import (
|
||||
CommandAuthenticationError,
|
||||
ConnectionState,
|
||||
Heos,
|
||||
HeosError,
|
||||
HeosOptions,
|
||||
)
|
||||
from pyheos import CommandAuthenticationError, Heos, HeosError, HeosOptions
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntryState,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
|
||||
from homeassistant.helpers.service_info.ssdp import (
|
||||
ATTR_UPNP_FRIENDLY_NAME,
|
||||
SsdpServiceInfo,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, ENTRY_TITLE
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HeosConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,6 +37,11 @@ AUTH_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def format_title(host: str) -> str:
|
||||
"""Format the title for config entries."""
|
||||
return f"HEOS System (via {host})"
|
||||
|
||||
|
||||
async def _validate_host(host: str, errors: dict[str, str]) -> bool:
|
||||
"""Validate host is reachable, return True, otherwise populate errors and return False."""
|
||||
heos = Heos(HeosOptions(host, events=False, heart_beat=False))
|
||||
@@ -49,19 +56,13 @@ async def _validate_host(host: str, errors: dict[str, str]) -> bool:
|
||||
|
||||
|
||||
async def _validate_auth(
|
||||
user_input: dict[str, str], entry: HeosConfigEntry, errors: dict[str, str]
|
||||
user_input: dict[str, str], heos: Heos, errors: dict[str, str]
|
||||
) -> bool:
|
||||
"""Validate authentication by signing in or out, otherwise populate errors if needed."""
|
||||
can_validate = (
|
||||
hasattr(entry, "runtime_data")
|
||||
and entry.runtime_data.heos.connection_state is ConnectionState.CONNECTED
|
||||
)
|
||||
if not user_input:
|
||||
# Log out (neither username nor password provided)
|
||||
if not can_validate:
|
||||
return True
|
||||
try:
|
||||
await entry.runtime_data.heos.sign_out()
|
||||
await heos.sign_out()
|
||||
except HeosError:
|
||||
errors["base"] = "unknown"
|
||||
_LOGGER.exception("Unexpected error occurred during sign-out")
|
||||
@@ -80,12 +81,8 @@ async def _validate_auth(
|
||||
return False
|
||||
|
||||
# Attempt to login (both username and password provided)
|
||||
if not can_validate:
|
||||
return True
|
||||
try:
|
||||
await entry.runtime_data.heos.sign_in(
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
)
|
||||
await heos.sign_in(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
|
||||
except CommandAuthenticationError as err:
|
||||
errors["base"] = "invalid_auth"
|
||||
_LOGGER.warning("Failed to sign-in to HEOS Account: %s", err)
|
||||
@@ -97,32 +94,16 @@ async def _validate_auth(
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Successfully signed-in to HEOS Account: %s",
|
||||
entry.runtime_data.heos.signed_in_username,
|
||||
heos.signed_in_username,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _get_current_hosts(entry: HeosConfigEntry) -> set[str]:
|
||||
"""Get a set of current hosts from the entry."""
|
||||
hosts = set(entry.data[CONF_HOST])
|
||||
if hasattr(entry, "runtime_data"):
|
||||
hosts.update(
|
||||
player.ip_address
|
||||
for player in entry.runtime_data.heos.players.values()
|
||||
if player.ip_address is not None
|
||||
)
|
||||
return hosts
|
||||
|
||||
|
||||
class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Define a flow for HEOS."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the HEOS flow."""
|
||||
self._discovered_host: str | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: HeosConfigEntry) -> OptionsFlow:
|
||||
@@ -136,84 +117,40 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# Store discovered host
|
||||
if TYPE_CHECKING:
|
||||
assert discovery_info.ssdp_location
|
||||
|
||||
entry: HeosConfigEntry | None = await self.async_set_unique_id(DOMAIN)
|
||||
hostname = urlparse(discovery_info.ssdp_location).hostname
|
||||
assert hostname is not None
|
||||
|
||||
# Abort early when discovered host is part of the current system
|
||||
if entry and hostname in _get_current_hosts(entry):
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
# Connect to discovered host and get system information
|
||||
heos = Heos(HeosOptions(hostname, events=False, heart_beat=False))
|
||||
try:
|
||||
await heos.connect()
|
||||
system_info = await heos.get_system_info()
|
||||
except HeosError as error:
|
||||
_LOGGER.debug(
|
||||
"Failed to retrieve system information from discovered HEOS device %s",
|
||||
hostname,
|
||||
exc_info=error,
|
||||
)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
finally:
|
||||
await heos.disconnect()
|
||||
|
||||
# Select the preferred host, if available
|
||||
if system_info.preferred_hosts:
|
||||
hostname = system_info.preferred_hosts[0].ip_address
|
||||
|
||||
# Move to confirmation when not configured
|
||||
if entry is None:
|
||||
self._discovered_host = hostname
|
||||
return await self.async_step_confirm_discovery()
|
||||
|
||||
# Only update if the configured host isn't part of the discovered hosts to ensure new players that come online don't trigger a reload
|
||||
if entry.data[CONF_HOST] not in [host.ip_address for host in system_info.hosts]:
|
||||
_LOGGER.debug(
|
||||
"Updated host %s to discovered host %s", entry.data[CONF_HOST], hostname
|
||||
)
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data_updates={CONF_HOST: hostname},
|
||||
reason="reconfigure_successful",
|
||||
)
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
async def async_step_confirm_discovery(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovered HEOS system."""
|
||||
if user_input is not None:
|
||||
assert self._discovered_host is not None
|
||||
return self.async_create_entry(
|
||||
title=ENTRY_TITLE, data={CONF_HOST: self._discovered_host}
|
||||
)
|
||||
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(step_id="confirm_discovery")
|
||||
friendly_name = f"{discovery_info.upnp[ATTR_UPNP_FRIENDLY_NAME]} ({hostname})"
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
self.hass.data[DOMAIN][friendly_name] = hostname
|
||||
await self.async_set_unique_id(DOMAIN)
|
||||
# Show selection form
|
||||
return self.async_show_form(step_id="user")
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Obtain host and validate connection."""
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
await self.async_set_unique_id(DOMAIN)
|
||||
self._abort_if_unique_id_configured(error="single_instance_allowed")
|
||||
# Try connecting to host if provided
|
||||
errors: dict[str, str] = {}
|
||||
host = None
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
# Map host from friendly name if in discovered hosts
|
||||
host = self.hass.data[DOMAIN].get(host, host)
|
||||
if await _validate_host(host, errors):
|
||||
self.hass.data.pop(DOMAIN) # Remove discovery data
|
||||
return self.async_create_entry(
|
||||
title=ENTRY_TITLE, data={CONF_HOST: host}
|
||||
title=format_title(host), data={CONF_HOST: host}
|
||||
)
|
||||
|
||||
# Return form
|
||||
host_type = (
|
||||
str if not self.hass.data[DOMAIN] else vol.In(list(self.hass.data[DOMAIN]))
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}),
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): host_type}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -249,7 +186,8 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
entry: HeosConfigEntry = self._get_reauth_entry()
|
||||
if user_input is not None:
|
||||
if await _validate_auth(user_input, entry, errors):
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
if await _validate_auth(user_input, entry.runtime_data.heos, errors):
|
||||
return self.async_update_reload_and_abort(entry, options=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -270,7 +208,8 @@ class HeosOptionsFlowHandler(OptionsFlow):
|
||||
"""Manage the options."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if await _validate_auth(user_input, self.config_entry, errors):
|
||||
entry: HeosConfigEntry = self.config_entry
|
||||
if await _validate_auth(user_input, entry.runtime_data.heos, errors):
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_USERNAME = "username"
|
||||
DOMAIN = "heos"
|
||||
ENTRY_TITLE = "HEOS System"
|
||||
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
||||
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
|
||||
SERVICE_GROUP_VOLUME_UP = "group_volume_up"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"loggers": ["pyheos"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyheos==1.0.2"],
|
||||
"single_config_entry": true,
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
|
||||
|
||||
@@ -38,7 +38,9 @@ rules:
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery-update-info:
|
||||
status: todo
|
||||
comment: Explore if this is possible.
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
|
||||
@@ -11,10 +11,6 @@
|
||||
"host": "Host name or IP address of a HEOS-capable product (preferably one connected via wire to the network)."
|
||||
}
|
||||
},
|
||||
"confirm_discovery": {
|
||||
"title": "Discovered HEOS System",
|
||||
"description": "Do you want to add your HEOS devices to Home Assistant?"
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure HEOS",
|
||||
"description": "Change the host name or IP address of the HEOS-capable product used to access your HEOS System.",
|
||||
@@ -47,7 +43,6 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
|
||||
@@ -124,6 +124,9 @@ class ExposedEntities:
|
||||
websocket_api.async_register_command(self._hass, ws_expose_new_entities_get)
|
||||
websocket_api.async_register_command(self._hass, ws_expose_new_entities_set)
|
||||
websocket_api.async_register_command(self._hass, ws_list_exposed_entities)
|
||||
websocket_api.async_register_command(
|
||||
self._hass, ws_list_entities_exposed_to_assistant
|
||||
)
|
||||
await self._async_load_data()
|
||||
|
||||
@callback
|
||||
@@ -452,6 +455,30 @@ def ws_list_exposed_entities(
|
||||
connection.send_result(msg["id"], {"exposed_entities": result})
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "homeassistant/expose_entity/list_exposed",
|
||||
vol.Required("assistant"): vol.In(KNOWN_ASSISTANTS),
|
||||
}
|
||||
)
|
||||
def ws_list_entities_exposed_to_assistant(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""List entities which are exposed to an assistant."""
|
||||
exposed_entities = hass.data[DATA_EXPOSED_ENTITIES]
|
||||
assistant = msg.get("assistant")
|
||||
entity_registry = er.async_get(hass)
|
||||
result = [
|
||||
entity_id
|
||||
for entity_id in chain(exposed_entities.entities, entity_registry.entities)
|
||||
if assistant in (entity_settings := async_get_entity_settings(hass, entity_id))
|
||||
and entity_settings[assistant].get("should_expose")
|
||||
]
|
||||
connection.send_result(msg["id"], {"exposed_entities": result})
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
|
||||
@@ -28,13 +28,12 @@ from . import silabs_multiprotocol_addon
|
||||
from .const import OTBR_DOMAIN, ZHA_DOMAIN
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
OwningAddon,
|
||||
OwningIntegration,
|
||||
get_otbr_addon_manager,
|
||||
get_zigbee_flasher_addon_manager,
|
||||
guess_hardware_owners,
|
||||
probe_silabs_firmware_info,
|
||||
probe_silabs_firmware_type,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -53,7 +52,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Instantiate base flow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._probed_firmware_info: FirmwareInfo | None = None
|
||||
self._probed_firmware_type: ApplicationType | None = None
|
||||
self._device: str | None = None # To be set in a subclass
|
||||
self._hardware_name: str = "unknown" # To be set in a subclass
|
||||
|
||||
@@ -65,8 +64,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Shared translation placeholders."""
|
||||
placeholders = {
|
||||
"firmware_type": (
|
||||
self._probed_firmware_info.firmware_type.value
|
||||
if self._probed_firmware_info is not None
|
||||
self._probed_firmware_type.value
|
||||
if self._probed_firmware_type is not None
|
||||
else "unknown"
|
||||
),
|
||||
"model": self._hardware_name,
|
||||
@@ -121,49 +120,39 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
async def _probe_firmware_info(
|
||||
self,
|
||||
probe_methods: tuple[ApplicationType, ...] = (
|
||||
# We probe in order of frequency: Zigbee, Thread, then multi-PAN
|
||||
ApplicationType.GECKO_BOOTLOADER,
|
||||
ApplicationType.EZSP,
|
||||
ApplicationType.SPINEL,
|
||||
ApplicationType.CPC,
|
||||
),
|
||||
) -> bool:
|
||||
async def _probe_firmware_type(self) -> bool:
|
||||
"""Probe the firmware currently on the device."""
|
||||
assert self._device is not None
|
||||
|
||||
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||
self._probed_firmware_type = await probe_silabs_firmware_type(
|
||||
self._device,
|
||||
probe_methods=probe_methods,
|
||||
)
|
||||
|
||||
return (
|
||||
self._probed_firmware_info is not None
|
||||
and self._probed_firmware_info.firmware_type
|
||||
in (
|
||||
probe_methods=(
|
||||
# We probe in order of frequency: Zigbee, Thread, then multi-PAN
|
||||
ApplicationType.GECKO_BOOTLOADER,
|
||||
ApplicationType.EZSP,
|
||||
ApplicationType.SPINEL,
|
||||
ApplicationType.CPC,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
return self._probed_firmware_type in (
|
||||
ApplicationType.EZSP,
|
||||
ApplicationType.SPINEL,
|
||||
ApplicationType.CPC,
|
||||
)
|
||||
|
||||
async def async_step_pick_firmware_zigbee(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Pick Zigbee firmware."""
|
||||
if not await self._probe_firmware_info():
|
||||
if not await self._probe_firmware_type():
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
# Allow the stick to be used with ZHA without flashing
|
||||
if (
|
||||
self._probed_firmware_info is not None
|
||||
and self._probed_firmware_info.firmware_type == ApplicationType.EZSP
|
||||
):
|
||||
if self._probed_firmware_type == ApplicationType.EZSP:
|
||||
return await self.async_step_confirm_zigbee()
|
||||
|
||||
if not is_hassio(self.hass):
|
||||
@@ -349,12 +338,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Confirm Zigbee setup."""
|
||||
assert self._device is not None
|
||||
assert self._hardware_name is not None
|
||||
|
||||
if not await self._probe_firmware_info(probe_methods=(ApplicationType.EZSP,)):
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
self._probed_firmware_type = ApplicationType.EZSP
|
||||
|
||||
if user_input is not None:
|
||||
await self.hass.config_entries.flow.async_init(
|
||||
@@ -382,7 +366,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Pick Thread firmware."""
|
||||
if not await self._probe_firmware_info():
|
||||
if not await self._probe_firmware_type():
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
@@ -474,11 +458,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Confirm OTBR setup."""
|
||||
assert self._device is not None
|
||||
|
||||
if not await self._probe_firmware_info(probe_methods=(ApplicationType.SPINEL,)):
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
self._probed_firmware_type = ApplicationType.SPINEL
|
||||
|
||||
if user_input is not None:
|
||||
# OTBR discovery is done automatically via hassio
|
||||
@@ -517,14 +497,14 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow):
|
||||
class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow):
|
||||
"""Zigbee and Thread options flow handlers."""
|
||||
|
||||
_probed_firmware_info: FirmwareInfo
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry, *args: Any, **kwargs: Any) -> None:
|
||||
"""Instantiate options flow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._config_entry = config_entry
|
||||
|
||||
self._probed_firmware_type = ApplicationType(self.config_entry.data["firmware"])
|
||||
|
||||
# Make `context` a regular dictionary
|
||||
self.context = {}
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": ["universal-silabs-flasher==0.0.29"]
|
||||
"requirements": ["universal-silabs-flasher==0.0.25"]
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ class ApplicationType(StrEnum):
|
||||
CPC = "cpc"
|
||||
EZSP = "ezsp"
|
||||
SPINEL = "spinel"
|
||||
ROUTER = "router"
|
||||
|
||||
@classmethod
|
||||
def from_flasher_application_type(
|
||||
@@ -249,10 +248,10 @@ async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> Firmware
|
||||
return guesses[-1][0]
|
||||
|
||||
|
||||
async def probe_silabs_firmware_info(
|
||||
async def probe_silabs_firmware_type(
|
||||
device: str, *, probe_methods: Iterable[ApplicationType] | None = None
|
||||
) -> FirmwareInfo | None:
|
||||
"""Probe the running firmware on a SiLabs device."""
|
||||
) -> ApplicationType | None:
|
||||
"""Probe the running firmware on a Silabs device."""
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
**(
|
||||
@@ -270,26 +269,4 @@ async def probe_silabs_firmware_info(
|
||||
if flasher.app_type is None:
|
||||
return None
|
||||
|
||||
return FirmwareInfo(
|
||||
device=device,
|
||||
firmware_type=ApplicationType.from_flasher_application_type(flasher.app_type),
|
||||
firmware_version=(
|
||||
flasher.app_version.orig_version
|
||||
if flasher.app_version is not None
|
||||
else None
|
||||
),
|
||||
source="probe",
|
||||
owners=[],
|
||||
)
|
||||
|
||||
|
||||
async def probe_silabs_firmware_type(
|
||||
device: str, *, probe_methods: Iterable[ApplicationType] | None = None
|
||||
) -> ApplicationType | None:
|
||||
"""Probe the running firmware type on a SiLabs device."""
|
||||
|
||||
fw_info = await probe_silabs_firmware_info(device, probe_methods=probe_methods)
|
||||
if fw_info is None:
|
||||
return None
|
||||
|
||||
return fw_info.firmware_type
|
||||
return ApplicationType.from_flasher_application_type(flasher.app_type)
|
||||
|
||||
@@ -10,10 +10,7 @@ from homeassistant.components.homeassistant_hardware import (
|
||||
firmware_config_flow,
|
||||
silabs_multiprotocol_addon,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigEntryBaseFlow,
|
||||
@@ -121,7 +118,7 @@ class HomeAssistantSkyConnectConfigFlow(
|
||||
"""Create the config entry."""
|
||||
assert self._usb_info is not None
|
||||
assert self._hw_variant is not None
|
||||
assert self._probed_firmware_info is not None
|
||||
assert self._probed_firmware_type is not None
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self._hw_variant.full_name,
|
||||
@@ -133,7 +130,7 @@ class HomeAssistantSkyConnectConfigFlow(
|
||||
"description": self._usb_info.description, # For backwards compatibility
|
||||
"product": self._usb_info.description,
|
||||
"device": self._usb_info.device,
|
||||
"firmware": self._probed_firmware_info.firmware_type.value,
|
||||
"firmware": self._probed_firmware_type.value,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -206,26 +203,18 @@ class HomeAssistantSkyConnectOptionsFlowHandler(
|
||||
self._hardware_name = self._hw_variant.full_name
|
||||
self._device = self._usb_info.device
|
||||
|
||||
self._probed_firmware_info = FirmwareInfo(
|
||||
device=self._device,
|
||||
firmware_type=ApplicationType(self.config_entry.data["firmware"]),
|
||||
firmware_version=None,
|
||||
source="guess",
|
||||
owners=[],
|
||||
)
|
||||
|
||||
# Regenerate the translation placeholders
|
||||
self._get_translation_placeholders()
|
||||
|
||||
def _async_flow_finished(self) -> ConfigFlowResult:
|
||||
"""Create the config entry."""
|
||||
assert self._probed_firmware_info is not None
|
||||
assert self._probed_firmware_type is not None
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry=self.config_entry,
|
||||
data={
|
||||
**self.config_entry.data,
|
||||
"firmware": self._probed_firmware_info.firmware_type.value,
|
||||
"firmware": self._probed_firmware_type.value,
|
||||
},
|
||||
options=self.config_entry.options,
|
||||
)
|
||||
|
||||
@@ -24,10 +24,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon
|
||||
OptionsFlowHandler as MultiprotocolOptionsFlowHandler,
|
||||
SerialPortSettings as MultiprotocolSerialPortSettings,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_HARDWARE,
|
||||
ConfigEntry,
|
||||
@@ -82,13 +79,10 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
|
||||
await self._probe_firmware_info()
|
||||
await self._probe_firmware_type()
|
||||
|
||||
# Kick off ZHA hardware discovery automatically if Zigbee firmware is running
|
||||
if (
|
||||
self._probed_firmware_info is not None
|
||||
and self._probed_firmware_info.firmware_type is ApplicationType.EZSP
|
||||
):
|
||||
if self._probed_firmware_type is ApplicationType.EZSP:
|
||||
discovery_flow.async_create_flow(
|
||||
self.hass,
|
||||
ZHA_DOMAIN,
|
||||
@@ -104,11 +98,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
||||
title=BOARD_NAME,
|
||||
data={
|
||||
# Assume the firmware type is EZSP if we cannot probe it
|
||||
FIRMWARE: (
|
||||
self._probed_firmware_info.firmware_type
|
||||
if self._probed_firmware_info is not None
|
||||
else ApplicationType.EZSP
|
||||
).value,
|
||||
FIRMWARE: (self._probed_firmware_type or ApplicationType.EZSP).value,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -274,14 +264,6 @@ class HomeAssistantYellowOptionsFlowHandler(
|
||||
self._hardware_name = BOARD_NAME
|
||||
self._device = RADIO_DEVICE
|
||||
|
||||
self._probed_firmware_info = FirmwareInfo(
|
||||
device=self._device,
|
||||
firmware_type=ApplicationType(self.config_entry.data["firmware"]),
|
||||
firmware_version=None,
|
||||
source="guess",
|
||||
owners=[],
|
||||
)
|
||||
|
||||
# Regenerate the translation placeholders
|
||||
self._get_translation_placeholders()
|
||||
|
||||
@@ -303,13 +285,13 @@ class HomeAssistantYellowOptionsFlowHandler(
|
||||
|
||||
def _async_flow_finished(self) -> ConfigFlowResult:
|
||||
"""Create the config entry."""
|
||||
assert self._probed_firmware_info is not None
|
||||
assert self._probed_firmware_type is not None
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry=self.config_entry,
|
||||
data={
|
||||
**self.config_entry.data,
|
||||
FIRMWARE: self._probed_firmware_info.firmware_type.value,
|
||||
FIRMWARE: self._probed_firmware_type.value,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -14,13 +14,7 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
PLATFORMS = [Platform.COVER, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
type HomeeConfigEntry = ConfigEntry[Homee]
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
"""The homee button platform."""
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
|
||||
BUTTON_DESCRIPTIONS: dict[AttributeType, ButtonEntityDescription] = {
|
||||
AttributeType.AUTOMATIC_MODE_IMPULSE: ButtonEntityDescription(key="automatic_mode"),
|
||||
AttributeType.BRIEFLY_OPEN_IMPULSE: ButtonEntityDescription(key="briefly_open"),
|
||||
AttributeType.IDENTIFICATION_MODE: ButtonEntityDescription(
|
||||
key="identification_mode",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=ButtonDeviceClass.IDENTIFY,
|
||||
),
|
||||
AttributeType.IMPULSE: ButtonEntityDescription(key="impulse"),
|
||||
AttributeType.LIGHT_IMPULSE: ButtonEntityDescription(key="light"),
|
||||
AttributeType.OPEN_PARTIAL_IMPULSE: ButtonEntityDescription(key="open_partial"),
|
||||
AttributeType.PERMANENTLY_OPEN_IMPULSE: ButtonEntityDescription(
|
||||
key="permanently_open"
|
||||
),
|
||||
AttributeType.RESET_METER: ButtonEntityDescription(
|
||||
key="reset_meter",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
AttributeType.VENTILATE_IMPULSE: ButtonEntityDescription(key="ventilate"),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the button component."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeButton(attribute, config_entry, BUTTON_DESCRIPTIONS[attribute.type])
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in BUTTON_DESCRIPTIONS and attribute.editable
|
||||
)
|
||||
|
||||
|
||||
class HomeeButton(HomeeEntity, ButtonEntity):
|
||||
"""Representation of a Homee button."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attribute: HomeeAttribute,
|
||||
entry: HomeeConfigEntry,
|
||||
description: ButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Homee button entity."""
|
||||
super().__init__(attribute, entry)
|
||||
self.entity_description = description
|
||||
if attribute.instance == 0:
|
||||
if attribute.type == AttributeType.IMPULSE:
|
||||
self._attr_name = None
|
||||
else:
|
||||
self._attr_translation_key = description.key
|
||||
else:
|
||||
self._attr_translation_key = f"{description.key}_instance"
|
||||
self._attr_translation_placeholders = {"instance": str(attribute.instance)}
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
await self.async_set_value(1)
|
||||
@@ -76,7 +76,6 @@ CLIMATE_PROFILES = [
|
||||
NodeProfile.WIFI_RADIATOR_THERMOSTAT,
|
||||
NodeProfile.WIFI_ROOM_THERMOSTAT,
|
||||
]
|
||||
|
||||
LIGHT_PROFILES = [
|
||||
NodeProfile.DIMMABLE_COLOR_LIGHT,
|
||||
NodeProfile.DIMMABLE_COLOR_METERING_PLUG,
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
"""The Homee light platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
from pyHomee.model import HomeeAttribute, HomeeNode
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_HS_COLOR,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.color import (
|
||||
brightness_to_value,
|
||||
color_hs_to_RGB,
|
||||
color_RGB_to_hs,
|
||||
value_to_brightness,
|
||||
)
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .const import LIGHT_PROFILES
|
||||
from .entity import HomeeNodeEntity
|
||||
|
||||
LIGHT_ATTRIBUTES = [
|
||||
AttributeType.COLOR,
|
||||
AttributeType.COLOR_MODE,
|
||||
AttributeType.COLOR_TEMPERATURE,
|
||||
AttributeType.DIMMING_LEVEL,
|
||||
]
|
||||
|
||||
|
||||
def is_light_node(node: HomeeNode) -> bool:
|
||||
"""Determine if a node is controllable as a homee light based on its profile and attributes."""
|
||||
assert node.attribute_map is not None
|
||||
return node.profile in LIGHT_PROFILES and AttributeType.ON_OFF in node.attribute_map
|
||||
|
||||
|
||||
def get_color_mode(supported_modes: set[ColorMode]) -> ColorMode:
|
||||
"""Determine the color mode from the supported modes."""
|
||||
if ColorMode.HS in supported_modes:
|
||||
return ColorMode.HS
|
||||
if ColorMode.COLOR_TEMP in supported_modes:
|
||||
return ColorMode.COLOR_TEMP
|
||||
if ColorMode.BRIGHTNESS in supported_modes:
|
||||
return ColorMode.BRIGHTNESS
|
||||
|
||||
return ColorMode.ONOFF
|
||||
|
||||
|
||||
def get_light_attribute_sets(
|
||||
node: HomeeNode,
|
||||
) -> list[dict[AttributeType, HomeeAttribute]]:
|
||||
"""Return the lights with their attributes as found in the node."""
|
||||
lights: list[dict[AttributeType, HomeeAttribute]] = []
|
||||
on_off_attributes = [
|
||||
i for i in node.attributes if i.type == AttributeType.ON_OFF and i.editable
|
||||
]
|
||||
for a in on_off_attributes:
|
||||
attribute_dict: dict[AttributeType, HomeeAttribute] = {a.type: a}
|
||||
for attribute in node.attributes:
|
||||
if attribute.instance == a.instance and attribute.type in LIGHT_ATTRIBUTES:
|
||||
attribute_dict[attribute.type] = attribute
|
||||
lights.append(attribute_dict)
|
||||
|
||||
return lights
|
||||
|
||||
|
||||
def rgb_list_to_decimal(color: tuple[int, int, int]) -> int:
|
||||
"""Convert an rgb color from list to decimal representation."""
|
||||
return int(int(color[0]) << 16) + (int(color[1]) << 8) + (int(color[2]))
|
||||
|
||||
|
||||
def decimal_to_rgb_list(color: float) -> list[int]:
|
||||
"""Convert an rgb color from decimal to list representation."""
|
||||
return [
|
||||
(int(color) & 0xFF0000) >> 16,
|
||||
(int(color) & 0x00FF00) >> 8,
|
||||
(int(color) & 0x0000FF),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the light entity."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeLight(node, light, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for light in get_light_attribute_sets(node)
|
||||
if is_light_node(node)
|
||||
)
|
||||
|
||||
|
||||
class HomeeLight(HomeeNodeEntity, LightEntity):
|
||||
"""Representation of a Homee light."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
node: HomeeNode,
|
||||
light: dict[AttributeType, HomeeAttribute],
|
||||
entry: HomeeConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize a Homee light."""
|
||||
super().__init__(node, entry)
|
||||
|
||||
self._on_off_attr: HomeeAttribute = light[AttributeType.ON_OFF]
|
||||
self._dimmer_attr: HomeeAttribute | None = light.get(
|
||||
AttributeType.DIMMING_LEVEL
|
||||
)
|
||||
self._col_attr: HomeeAttribute | None = light.get(AttributeType.COLOR)
|
||||
self._temp_attr: HomeeAttribute | None = light.get(
|
||||
AttributeType.COLOR_TEMPERATURE
|
||||
)
|
||||
self._mode_attr: HomeeAttribute | None = light.get(AttributeType.COLOR_MODE)
|
||||
|
||||
self._attr_supported_color_modes = self._get_supported_color_modes()
|
||||
self._attr_color_mode = get_color_mode(self._attr_supported_color_modes)
|
||||
|
||||
if self._temp_attr is not None:
|
||||
self._attr_min_color_temp_kelvin = int(self._temp_attr.minimum)
|
||||
self._attr_max_color_temp_kelvin = int(self._temp_attr.maximum)
|
||||
|
||||
if self._on_off_attr.instance > 0:
|
||||
self._attr_translation_key = "light_instance"
|
||||
self._attr_translation_placeholders = {
|
||||
"instance": str(self._on_off_attr.instance)
|
||||
}
|
||||
else:
|
||||
# If a device has only one light, it will get its name.
|
||||
self._attr_name = None
|
||||
self._attr_unique_id = (
|
||||
f"{entry.runtime_data.settings.uid}-{self._node.id}-{self._on_off_attr.id}"
|
||||
)
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness of the light."""
|
||||
assert self._dimmer_attr is not None
|
||||
return value_to_brightness(
|
||||
(self._dimmer_attr.minimum + 1, self._dimmer_attr.maximum),
|
||||
self._dimmer_attr.current_value,
|
||||
)
|
||||
|
||||
@property
|
||||
def hs_color(self) -> tuple[float, float] | None:
|
||||
"""Return the color of the light."""
|
||||
assert self._col_attr is not None
|
||||
rgb = decimal_to_rgb_list(self._col_attr.current_value)
|
||||
return color_RGB_to_hs(*rgb)
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int:
|
||||
"""Return the color temperature of the light."""
|
||||
assert self._temp_attr is not None
|
||||
return int(self._temp_attr.current_value)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
return bool(self._on_off_attr.current_value)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on."""
|
||||
if ATTR_BRIGHTNESS in kwargs and self._dimmer_attr is not None:
|
||||
target_value = round(
|
||||
brightness_to_value(
|
||||
(self._dimmer_attr.minimum, self._dimmer_attr.maximum),
|
||||
kwargs[ATTR_BRIGHTNESS],
|
||||
)
|
||||
)
|
||||
await self.async_set_value(self._dimmer_attr, target_value)
|
||||
else:
|
||||
# If no brightness value is given, just turn on.
|
||||
await self.async_set_value(self._on_off_attr, 1)
|
||||
|
||||
if ATTR_COLOR_TEMP_KELVIN in kwargs and self._temp_attr is not None:
|
||||
await self.async_set_value(self._temp_attr, kwargs[ATTR_COLOR_TEMP_KELVIN])
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
color = kwargs[ATTR_HS_COLOR]
|
||||
if self._col_attr is not None:
|
||||
await self.async_set_value(
|
||||
self._col_attr,
|
||||
rgb_list_to_decimal(color_hs_to_RGB(*color)),
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn off."""
|
||||
await self.async_set_value(self._on_off_attr, 0)
|
||||
|
||||
def _get_supported_color_modes(self) -> set[ColorMode]:
|
||||
"""Determine the supported color modes from the available attributes."""
|
||||
color_modes: set[ColorMode] = set()
|
||||
|
||||
if self._temp_attr is not None and self._temp_attr.editable:
|
||||
color_modes.add(ColorMode.COLOR_TEMP)
|
||||
if self._col_attr is not None:
|
||||
color_modes.add(ColorMode.HS)
|
||||
|
||||
# If no other color modes are available, set one of those.
|
||||
if len(color_modes) == 0:
|
||||
if self._dimmer_attr is not None:
|
||||
color_modes.add(ColorMode.BRIGHTNESS)
|
||||
else:
|
||||
color_modes.add(ColorMode.ONOFF)
|
||||
|
||||
return color_modes
|
||||
@@ -157,7 +157,7 @@ SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
|
||||
AttributeType.RAIN_FALL_TODAY: HomeeSensorEntityDescription(
|
||||
key="rainfall_day",
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AttributeType.RELATIVE_HUMIDITY: HomeeSensorEntityDescription(
|
||||
key="humidity",
|
||||
|
||||
@@ -26,46 +26,6 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"automatic_mode": {
|
||||
"name": "Automatic mode"
|
||||
},
|
||||
"briefly_open": {
|
||||
"name": "Briefly open"
|
||||
},
|
||||
"identification_mode": {
|
||||
"name": "Identification mode"
|
||||
},
|
||||
"impulse_instance": {
|
||||
"name": "Impulse {instance}"
|
||||
},
|
||||
"light": {
|
||||
"name": "Light"
|
||||
},
|
||||
"light_instance": {
|
||||
"name": "Light {instance}"
|
||||
},
|
||||
"open_partial": {
|
||||
"name": "Open partially"
|
||||
},
|
||||
"permanently_open": {
|
||||
"name": "Open permanently"
|
||||
},
|
||||
"reset_meter": {
|
||||
"name": "Reset meter"
|
||||
},
|
||||
"reset_meter_instance": {
|
||||
"name": "Reset meter {instance}"
|
||||
},
|
||||
"ventilate": {
|
||||
"name": "Ventilate"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"light_instance": {
|
||||
"name": "Light {instance}"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"brightness_instance": {
|
||||
"name": "Illuminance {instance}"
|
||||
|
||||
@@ -4,20 +4,17 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from inkbird_ble import INKBIRDBluetoothDeviceData, SensorUpdate
|
||||
from inkbird_ble import INKBIRDBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfo,
|
||||
)
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_DEVICE_TYPE, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
@@ -28,33 +25,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up INKBIRD BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
device_type: str | None = entry.data.get(CONF_DEVICE_TYPE)
|
||||
data = INKBIRDBluetoothDeviceData(device_type)
|
||||
|
||||
@callback
|
||||
def _async_on_update(service_info: BluetoothServiceInfo) -> SensorUpdate:
|
||||
"""Handle update callback from the passive BLE processor."""
|
||||
nonlocal device_type
|
||||
update = data.update(service_info)
|
||||
if device_type is None and data.device_type is not None:
|
||||
device_type_str = str(data.device_type)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, CONF_DEVICE_TYPE: device_type_str}
|
||||
)
|
||||
device_type = device_type_str
|
||||
return update
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
update_method=_async_on_update,
|
||||
data = INKBIRDBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
||||
PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
# only start after all platforms have had a chance to subscribe
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
entry.async_on_unload(
|
||||
coordinator.async_start()
|
||||
) # only start after all platforms have had a chance to subscribe
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"""Constants for the INKBIRD Bluetooth integration."""
|
||||
|
||||
DOMAIN = "inkbird"
|
||||
|
||||
CONF_DEVICE_TYPE = "device_type"
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["inkbird-ble==0.7.0"]
|
||||
"requirements": ["inkbird-ble==0.5.8"]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"services": {
|
||||
"select_next": {
|
||||
"name": "Next",
|
||||
"description": "Selects the next option.",
|
||||
"description": "Select the next option.",
|
||||
"fields": {
|
||||
"cycle": {
|
||||
"name": "Cycle",
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.util.dt import parse_datetime
|
||||
|
||||
from .browse_media import build_item_response, build_root_response
|
||||
from .client_wrapper import get_artwork_url
|
||||
from .const import CONTENT_TYPE_MAP, LOGGER, MAX_IMAGE_WIDTH
|
||||
from .const import CONTENT_TYPE_MAP, LOGGER
|
||||
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
|
||||
from .entity import JellyfinClientEntity
|
||||
|
||||
@@ -169,9 +169,7 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
|
||||
if self.now_playing is None:
|
||||
return None
|
||||
|
||||
return get_artwork_url(
|
||||
self.coordinator.api_client, self.now_playing, MAX_IMAGE_WIDTH
|
||||
)
|
||||
return get_artwork_url(self.coordinator.api_client, self.now_playing, 150)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
@@ -26,14 +26,11 @@ async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
"""Unload config entry."""
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
return True
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""LINAK virtual integration."""
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"domain": "linak",
|
||||
"name": "LINAK",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "idasen_desk"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
@@ -43,16 +43,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
|
||||
@@ -277,21 +277,6 @@ FOUR_GROUP_REMOTE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
|
||||
}
|
||||
)
|
||||
|
||||
# See mappings at https://github.com/home-assistant/core/issues/137548#issuecomment-2643440119
|
||||
PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LIP = {
|
||||
"on": 2, # 'Number': 2 in LIP
|
||||
"off": 4, # 'Number': 4 in LIP
|
||||
}
|
||||
PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LEAP = {
|
||||
"on": 0, # 'ButtonNumber': 0 in LEAP
|
||||
"off": 2, # 'ButtonNumber': 2 in LEAP
|
||||
}
|
||||
PADDLE_SWITCH_PICO_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_SUBTYPE): vol.In(PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LIP),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
DEVICE_TYPE_SCHEMA_MAP = {
|
||||
"Pico2Button": PICO_2_BUTTON_TRIGGER_SCHEMA,
|
||||
@@ -303,7 +288,6 @@ DEVICE_TYPE_SCHEMA_MAP = {
|
||||
"Pico4ButtonZone": PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA,
|
||||
"Pico4Button2Group": PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA,
|
||||
"FourGroupRemote": FOUR_GROUP_REMOTE_TRIGGER_SCHEMA,
|
||||
"PaddleSwitchPico": PADDLE_SWITCH_PICO_TRIGGER_SCHEMA,
|
||||
}
|
||||
|
||||
DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = {
|
||||
@@ -316,7 +300,6 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = {
|
||||
"Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LIP,
|
||||
"Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP,
|
||||
"FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP,
|
||||
"PaddleSwitchPico": PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LIP,
|
||||
}
|
||||
|
||||
DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = {
|
||||
@@ -329,7 +312,6 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = {
|
||||
"Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP,
|
||||
"Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP,
|
||||
"FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP,
|
||||
"PaddleSwitchPico": PADDLE_SWITCH_PICO_BUTTON_TYPES_TO_LEAP,
|
||||
}
|
||||
|
||||
LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP: dict[str, dict[int, str]] = {
|
||||
@@ -344,7 +326,6 @@ TRIGGER_SCHEMA = vol.Any(
|
||||
PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA,
|
||||
PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA,
|
||||
FOUR_GROUP_REMOTE_TRIGGER_SCHEMA,
|
||||
PADDLE_SWITCH_PICO_TRIGGER_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
@@ -29,13 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
|
||||
return True
|
||||
|
||||
@@ -25,6 +25,7 @@ from mcp import types
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_LLM_HASS_API
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import llm
|
||||
@@ -55,9 +56,11 @@ def async_get_config_entry(hass: HomeAssistant) -> MCPServerConfigEntry:
|
||||
|
||||
Will raise an HTTP error if the expected configuration is not present.
|
||||
"""
|
||||
config_entries: list[MCPServerConfigEntry] = (
|
||||
hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
)
|
||||
config_entries: list[MCPServerConfigEntry] = [
|
||||
config_entry
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.state == ConfigEntryState.LOADED
|
||||
]
|
||||
if not config_entries:
|
||||
raise HTTPNotFound(text="Model Context Protocol server is not configured")
|
||||
if len(config_entries) > 1:
|
||||
|
||||
@@ -299,22 +299,22 @@
|
||||
"description": "Removes all items from the playlist."
|
||||
},
|
||||
"shuffle_set": {
|
||||
"name": "Set shuffle",
|
||||
"description": "Enables or disables the shuffle mode.",
|
||||
"name": "Shuffle",
|
||||
"description": "Playback mode that selects the media in randomized order.",
|
||||
"fields": {
|
||||
"shuffle": {
|
||||
"name": "Shuffle mode",
|
||||
"description": "Whether the media should be played in randomized order or not."
|
||||
"name": "Shuffle",
|
||||
"description": "Whether or not shuffle mode is enabled."
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeat_set": {
|
||||
"name": "Set repeat",
|
||||
"description": "Sets the repeat mode.",
|
||||
"name": "Repeat",
|
||||
"description": "Playback mode that plays the media in a loop.",
|
||||
"fields": {
|
||||
"repeat": {
|
||||
"name": "Repeat mode",
|
||||
"description": "Whether the media (one or all) should be played in a loop or not."
|
||||
"description": "Repeat mode to set."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"services": {
|
||||
"reload": {
|
||||
"name": "[%key:common::action::reload%]",
|
||||
"description": "Reloads all Modbus entities."
|
||||
"description": "Reloads all modbus entities."
|
||||
},
|
||||
"write_coil": {
|
||||
"name": "Write coil",
|
||||
"description": "Writes to a Modbus coil.",
|
||||
"description": "Writes to a modbus coil.",
|
||||
"fields": {
|
||||
"address": {
|
||||
"name": "Address",
|
||||
@@ -17,8 +17,8 @@
|
||||
"description": "State to write."
|
||||
},
|
||||
"slave": {
|
||||
"name": "Server",
|
||||
"description": "Address of the Modbus unit/server."
|
||||
"name": "Slave",
|
||||
"description": "Address of the modbus unit/slave."
|
||||
},
|
||||
"hub": {
|
||||
"name": "Hub",
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"write_register": {
|
||||
"name": "Write register",
|
||||
"description": "Writes to a Modbus holding register.",
|
||||
"description": "Writes to a modbus holding register.",
|
||||
"fields": {
|
||||
"address": {
|
||||
"name": "[%key:component::modbus::services::write_coil::fields::address::name%]",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
@@ -29,13 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
|
||||
return True
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"services": {
|
||||
"aux": {
|
||||
"name": "Aux",
|
||||
"description": "Changes the state of an aux output.",
|
||||
"description": "Trigger an aux output.",
|
||||
"fields": {
|
||||
"output_id": {
|
||||
"name": "Output ID",
|
||||
@@ -10,17 +10,17 @@
|
||||
},
|
||||
"state": {
|
||||
"name": "State",
|
||||
"description": "The on/off state of the output. If P14xE 8E is enabled then turning on will pulse the output for the time specified in P14(x+4)E."
|
||||
"description": "The On/Off State. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E."
|
||||
}
|
||||
}
|
||||
},
|
||||
"panic": {
|
||||
"name": "Panic",
|
||||
"description": "Triggers a panic alarm.",
|
||||
"description": "Triggers a panic.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "Code",
|
||||
"description": "The user code to use to trigger the panic alarm."
|
||||
"description": "The user code to use to trigger the panic."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ from collections.abc import Mapping
|
||||
from google_nest_sdm.device import Device
|
||||
from google_nest_sdm.device_traits import ConnectivityTrait, InfoTrait
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -83,7 +84,8 @@ def async_nest_devices(hass: HomeAssistant) -> Mapping[str, Device]:
|
||||
"""Return a mapping of all nest devices for all config entries."""
|
||||
return {
|
||||
device.name: device
|
||||
for config_entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.state == ConfigEntryState.LOADED
|
||||
for device in config_entry.runtime_data.device_manager.devices.values()
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ set_preset_mode_with_end_datetime:
|
||||
select:
|
||||
options:
|
||||
- "away"
|
||||
- "frost_guard"
|
||||
- "Frost Guard"
|
||||
end_datetime:
|
||||
required: true
|
||||
example: '"2019-04-20 05:04:20"'
|
||||
|
||||
@@ -99,11 +99,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) ->
|
||||
_async_notify_backup_listeners_soon(hass)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: OneDriveConfigEntry) -> None:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ from homeassistant.components.backup import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_DELETE_PERMANENTLY, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .coordinator import OneDriveConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -205,12 +205,8 @@ class OneDriveBackupAgent(BackupAgent):
|
||||
|
||||
backup = backups[backup_id]
|
||||
|
||||
delete_permanently = self._entry.options.get(CONF_DELETE_PERMANENTLY, False)
|
||||
|
||||
await self._client.delete_drive_item(backup.backup_file_id, delete_permanently)
|
||||
await self._client.delete_drive_item(
|
||||
backup.metadata_file_id, delete_permanently
|
||||
)
|
||||
await self._client.delete_drive_item(backup.backup_file_id)
|
||||
await self._client.delete_drive_item(backup.metadata_file_id)
|
||||
self._cache_expiration = time()
|
||||
|
||||
@handle_backup_errors
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
"""Config flow for OneDrive."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from onedrive_personal_sdk.clients.client import OneDriveClient
|
||||
from onedrive_personal_sdk.exceptions import OneDriveException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
||||
|
||||
from .const import CONF_DELETE_PERMANENTLY, DOMAIN, OAUTH_SCOPES
|
||||
from .coordinator import OneDriveConfigEntry
|
||||
from .const import DOMAIN, OAUTH_SCOPES
|
||||
|
||||
|
||||
class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
@@ -91,38 +86,3 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: OneDriveConfigEntry,
|
||||
) -> OneDriveOptionsFlowHandler:
|
||||
"""Create the options flow."""
|
||||
return OneDriveOptionsFlowHandler()
|
||||
|
||||
|
||||
class OneDriveOptionsFlowHandler(OptionsFlow):
|
||||
"""Handles options flow for the component."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options for OneDrive."""
|
||||
if user_input:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
options_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_DELETE_PERMANENTLY,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_DELETE_PERMANENTLY, False
|
||||
),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=options_schema,
|
||||
)
|
||||
|
||||
@@ -7,8 +7,6 @@ from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
DOMAIN: Final = "onedrive"
|
||||
|
||||
CONF_DELETE_PERMANENTLY: Final = "delete_permanently"
|
||||
|
||||
# replace "consumers" with "common", when adding SharePoint or OneDrive for Business support
|
||||
OAUTH2_AUTHORIZE: Final = (
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["onedrive-personal-sdk==0.0.11"]
|
||||
"requirements": ["onedrive-personal-sdk==0.0.10"]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ rules:
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
No Options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
|
||||
@@ -29,19 +29,6 @@
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "By default, files are put into the Recycle Bin when deleted, where they remain available for another 30 days. If you enable this option, files will be deleted immediately when they are cleaned up by the backup system.",
|
||||
"data": {
|
||||
"delete_permanently": "Delete files permanently"
|
||||
},
|
||||
"data_description": {
|
||||
"delete_permanently": "Delete files without moving them to the Recycle Bin"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"drive_full": {
|
||||
"title": "OneDrive data cap exceeded",
|
||||
|
||||
@@ -235,7 +235,7 @@ class ONVIFDevice:
|
||||
LOGGER.debug("%s: Retrieving current device date/time", self.name)
|
||||
try:
|
||||
device_time = await device_mgmt.GetSystemDateAndTime()
|
||||
except (RequestError, Fault) as err:
|
||||
except RequestError as err:
|
||||
LOGGER.warning(
|
||||
"Couldn't get device '%s' date/time. Error: %s", self.name, err
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
|
||||
from opower import Forecast, MeterType, UnitOfMeasure
|
||||
|
||||
@@ -29,7 +28,7 @@ from .coordinator import OpowerConfigEntry, OpowerCoordinator
|
||||
class OpowerEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Opower sensors entities."""
|
||||
|
||||
value_fn: Callable[[Forecast], str | float | date]
|
||||
value_fn: Callable[[Forecast], str | float]
|
||||
|
||||
|
||||
# suggested_display_precision=0 for all sensors since
|
||||
@@ -97,7 +96,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.start_date,
|
||||
value_fn=lambda data: str(data.start_date),
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="elec_end_date",
|
||||
@@ -105,7 +104,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.end_date,
|
||||
value_fn=lambda data: str(data.end_date),
|
||||
),
|
||||
)
|
||||
GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
@@ -169,7 +168,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.start_date,
|
||||
value_fn=lambda data: str(data.start_date),
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="gas_end_date",
|
||||
@@ -177,7 +176,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.end_date,
|
||||
value_fn=lambda data: str(data.end_date),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -247,7 +246,7 @@ class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
||||
self.utility_account_id = utility_account_id
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | date:
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state."""
|
||||
if self.coordinator.data is not None:
|
||||
return self.entity_description.value_fn(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/prosegur",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyprosegur"],
|
||||
"requirements": ["pyprosegur==0.0.13"]
|
||||
"requirements": ["pyprosegur==0.0.9"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from aiohttp import CookieJar
|
||||
from pyloadapi.api import PyLoadAPI
|
||||
from pyloadapi.exceptions import CannotConnect, InvalidAuth, ParserError
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
@@ -15,8 +16,10 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PyLoadConfigEntry, PyLoadCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
|
||||
@@ -42,6 +45,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: PyLoadConfigEntry) -> bo
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
await pyloadapi.login()
|
||||
except CannotConnect as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_request_exception",
|
||||
) from e
|
||||
except ParserError as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_parse_exception",
|
||||
) from e
|
||||
except InvalidAuth as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_authentication_exception",
|
||||
translation_placeholders={CONF_USERNAME: entry.data[CONF_USERNAME]},
|
||||
) from e
|
||||
coordinator = PyLoadCoordinator(hass, entry, pyloadapi)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -18,8 +18,6 @@ from .const import DOMAIN
|
||||
from .coordinator import PyLoadConfigEntry
|
||||
from .entity import BasePyLoadEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class PyLoadButtonEntityDescription(ButtonEntityDescription):
|
||||
|
||||
@@ -9,7 +9,7 @@ from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -59,11 +59,14 @@ class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]):
|
||||
async def _async_update_data(self) -> PyLoadData:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
if not self.version:
|
||||
self.version = await self.pyload.version()
|
||||
return PyLoadData(
|
||||
**await self.pyload.get_status(),
|
||||
free_space=await self.pyload.free_space(),
|
||||
)
|
||||
except InvalidAuth:
|
||||
|
||||
except InvalidAuth as e:
|
||||
try:
|
||||
await self.pyload.login()
|
||||
except InvalidAuth as exc:
|
||||
@@ -72,38 +75,13 @@ class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]):
|
||||
translation_key="setup_authentication_exception",
|
||||
translation_placeholders={CONF_USERNAME: self.pyload.username},
|
||||
) from exc
|
||||
_LOGGER.debug(
|
||||
"Unable to retrieve data due to cookie expiration, retrying after 20 seconds"
|
||||
)
|
||||
return self.data
|
||||
|
||||
raise UpdateFailed(
|
||||
"Unable to retrieve data due to cookie expiration"
|
||||
) from e
|
||||
except CannotConnect as e:
|
||||
raise UpdateFailed(
|
||||
"Unable to connect and retrieve data from pyLoad API"
|
||||
) from e
|
||||
except ParserError as e:
|
||||
raise UpdateFailed("Unable to parse data from pyLoad API") from e
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
|
||||
try:
|
||||
await self.pyload.login()
|
||||
self.version = await self.pyload.version()
|
||||
except CannotConnect as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_request_exception",
|
||||
) from e
|
||||
except ParserError as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_parse_exception",
|
||||
) from e
|
||||
except InvalidAuth as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_authentication_exception",
|
||||
translation_placeholders={
|
||||
CONF_USERNAME: self.config_entry.data[CONF_USERNAME]
|
||||
},
|
||||
) from e
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyloadapi"],
|
||||
"requirements": ["PyLoadAPI==1.4.1"]
|
||||
"requirements": ["PyLoadAPI==1.3.2"]
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ from .const import UNIT_DOWNLOADS
|
||||
from .coordinator import PyLoadConfigEntry, PyLoadData
|
||||
from .entity import BasePyLoadEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class PyLoadSensorEntity(StrEnum):
|
||||
"""pyLoad Sensor Entities."""
|
||||
|
||||
@@ -22,8 +22,6 @@ from .const import DOMAIN
|
||||
from .coordinator import PyLoadConfigEntry, PyLoadData
|
||||
from .entity import BasePyLoadEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
class PyLoadSwitch(StrEnum):
|
||||
"""PyLoad Switch Entities."""
|
||||
|
||||
@@ -71,14 +71,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: QbusConfigEntry) -> boo
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
entry.runtime_data.shutdown()
|
||||
_cleanup(hass, entry)
|
||||
cleanup(hass, entry)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
def _cleanup(hass: HomeAssistant, entry: QbusConfigEntry) -> None:
|
||||
def cleanup(hass: HomeAssistant, entry: QbusConfigEntry) -> None:
|
||||
"""Shutdown if no more entries are loaded."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN) and (
|
||||
config_coordinator := hass.data.get(QBUS_KEY)
|
||||
):
|
||||
entries = hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
count = len(entries)
|
||||
|
||||
# During unloading of the entry, it is not marked as unloaded yet. So
|
||||
# count can be 1 if it is the last one.
|
||||
if count <= 1 and (config_coordinator := hass.data.get(QBUS_KEY)):
|
||||
config_coordinator.shutdown()
|
||||
|
||||
@@ -5,10 +5,7 @@ from typing import Final
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN: Final = "qbus"
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.LIGHT,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
PLATFORMS: list[Platform] = [Platform.SWITCH]
|
||||
|
||||
CONF_SERIAL_NUMBER: Final = "serial"
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""Base class for Qbus entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
import re
|
||||
|
||||
from qbusmqttapi.discovery import QbusMqttOutput
|
||||
@@ -13,36 +10,12 @@ from qbusmqttapi.state import QbusMqttState
|
||||
from homeassistant.components.mqtt import ReceiveMessage, client as mqtt
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import QbusControllerCoordinator
|
||||
|
||||
_REFID_REGEX = re.compile(r"^\d+\/(\d+(?:\/\d+)?)$")
|
||||
|
||||
|
||||
def add_new_outputs(
|
||||
coordinator: QbusControllerCoordinator,
|
||||
added_outputs: list[QbusMqttOutput],
|
||||
filter_fn: Callable[[QbusMqttOutput], bool],
|
||||
entity_type: type[QbusEntity],
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Call async_add_entities for new outputs."""
|
||||
|
||||
added_ref_ids = {k.ref_id for k in added_outputs}
|
||||
|
||||
new_outputs = [
|
||||
output
|
||||
for output in coordinator.data
|
||||
if filter_fn(output) and output.ref_id not in added_ref_ids
|
||||
]
|
||||
|
||||
if new_outputs:
|
||||
added_outputs.extend(new_outputs)
|
||||
async_add_entities([entity_type(output) for output in new_outputs])
|
||||
|
||||
|
||||
def format_ref_id(ref_id: str) -> str | None:
|
||||
"""Format the Qbus ref_id."""
|
||||
matches: list[str] = re.findall(_REFID_REGEX, ref_id)
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
"""Support for Qbus light."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from qbusmqttapi.discovery import QbusMqttOutput
|
||||
from qbusmqttapi.state import QbusMqttAnalogState, StateType
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.components.mqtt import ReceiveMessage
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.color import brightness_to_value, value_to_brightness
|
||||
|
||||
from .coordinator import QbusConfigEntry
|
||||
from .entity import QbusEntity, add_new_outputs
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: QbusConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up light entities."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
added_outputs: list[QbusMqttOutput] = []
|
||||
|
||||
def _check_outputs() -> None:
|
||||
add_new_outputs(
|
||||
coordinator,
|
||||
added_outputs,
|
||||
lambda output: output.type == "analog",
|
||||
QbusLight,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
_check_outputs()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))
|
||||
|
||||
|
||||
class QbusLight(QbusEntity, LightEntity):
|
||||
"""Representation of a Qbus light entity."""
|
||||
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
|
||||
def __init__(self, mqtt_output: QbusMqttOutput) -> None:
|
||||
"""Initialize light entity."""
|
||||
|
||||
super().__init__(mqtt_output)
|
||||
|
||||
self._set_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
|
||||
percentage: int | None = None
|
||||
on: bool | None = None
|
||||
|
||||
state = QbusMqttAnalogState(id=self._mqtt_output.id)
|
||||
|
||||
if brightness is None:
|
||||
on = True
|
||||
|
||||
state.type = StateType.ACTION
|
||||
state.write_on_off(on)
|
||||
else:
|
||||
percentage = round(brightness_to_value((1, 100), brightness))
|
||||
|
||||
state.type = StateType.STATE
|
||||
state.write_percentage(percentage)
|
||||
|
||||
await self._async_publish_output_state(state)
|
||||
self._set_state(percentage=percentage, on=on)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
state = QbusMqttAnalogState(id=self._mqtt_output.id, type=StateType.ACTION)
|
||||
state.write_on_off(on=False)
|
||||
|
||||
await self._async_publish_output_state(state)
|
||||
self._set_state(on=False)
|
||||
|
||||
async def _state_received(self, msg: ReceiveMessage) -> None:
|
||||
output = self._message_factory.parse_output_state(
|
||||
QbusMqttAnalogState, msg.payload
|
||||
)
|
||||
|
||||
if output is not None:
|
||||
percentage = round(output.read_percentage())
|
||||
self._set_state(percentage=percentage)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def _set_state(
|
||||
self, *, percentage: int | None = None, on: bool | None = None
|
||||
) -> None:
|
||||
if percentage is None:
|
||||
# When turning on without brightness, we don't know the desired
|
||||
# brightness. It will be set during _state_received().
|
||||
if on is True:
|
||||
self._attr_is_on = True
|
||||
else:
|
||||
self._attr_is_on = False
|
||||
self._attr_brightness = 0
|
||||
else:
|
||||
self._attr_is_on = percentage > 0
|
||||
self._attr_brightness = value_to_brightness((1, 100), percentage)
|
||||
@@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import QbusConfigEntry
|
||||
from .entity import QbusEntity, add_new_outputs
|
||||
from .entity import QbusEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -19,21 +19,26 @@ PARALLEL_UPDATES = 0
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: QbusConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switch entities."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
added_outputs: list[QbusMqttOutput] = []
|
||||
|
||||
# Local function that calls add_entities for new entities
|
||||
def _check_outputs() -> None:
|
||||
add_new_outputs(
|
||||
coordinator,
|
||||
added_outputs,
|
||||
lambda output: output.type == "onoff",
|
||||
QbusSwitch,
|
||||
async_add_entities,
|
||||
)
|
||||
added_output_ids = {k.id for k in added_outputs}
|
||||
|
||||
new_outputs = [
|
||||
item
|
||||
for item in coordinator.data
|
||||
if item.type == "onoff" and item.id not in added_output_ids
|
||||
]
|
||||
|
||||
if new_outputs:
|
||||
added_outputs.extend(new_outputs)
|
||||
add_entities([QbusSwitch(output) for output in new_outputs])
|
||||
|
||||
_check_outputs()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))
|
||||
@@ -44,7 +49,10 @@ class QbusSwitch(QbusEntity, SwitchEntity):
|
||||
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
|
||||
def __init__(self, mqtt_output: QbusMqttOutput) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
mqtt_output: QbusMqttOutput,
|
||||
) -> None:
|
||||
"""Initialize switch entity."""
|
||||
|
||||
super().__init__(mqtt_output)
|
||||
|
||||
@@ -43,7 +43,6 @@ from homeassistant.helpers.event import (
|
||||
async_track_time_interval,
|
||||
async_track_utc_time_change,
|
||||
)
|
||||
from homeassistant.helpers.recorder import DATA_RECORDER
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -184,7 +183,7 @@ class Recorder(threading.Thread):
|
||||
self.db_retry_wait = db_retry_wait
|
||||
self.database_engine: DatabaseEngine | None = None
|
||||
# Database connection is ready, but non-live migration may be in progress
|
||||
db_connected: asyncio.Future[bool] = hass.data[DATA_RECORDER].db_connected
|
||||
db_connected: asyncio.Future[bool] = hass.data[DOMAIN].db_connected
|
||||
self.async_db_connected: asyncio.Future[bool] = db_connected
|
||||
# Database is ready to use but live migration may be in progress
|
||||
self.async_db_ready: asyncio.Future[bool] = hass.loop.create_future()
|
||||
|
||||
@@ -24,7 +24,6 @@ import voluptuous as vol
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
|
||||
from homeassistant.core import HomeAssistant, callback, valid_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.recorder import DATA_RECORDER
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -562,9 +561,7 @@ def _compile_statistics(
|
||||
platform_stats: list[StatisticResult] = []
|
||||
current_metadata: dict[str, tuple[int, StatisticMetaData]] = {}
|
||||
# Collect statistics from all platforms implementing support
|
||||
for domain, platform in instance.hass.data[
|
||||
DATA_RECORDER
|
||||
].recorder_platforms.items():
|
||||
for domain, platform in instance.hass.data[DOMAIN].recorder_platforms.items():
|
||||
if not (
|
||||
platform_compile_statistics := getattr(
|
||||
platform, INTEGRATION_PLATFORM_COMPILE_STATISTICS, None
|
||||
@@ -602,7 +599,7 @@ def _compile_statistics(
|
||||
|
||||
if start.minute == 50:
|
||||
# Once every hour, update issues
|
||||
for platform in instance.hass.data[DATA_RECORDER].recorder_platforms.values():
|
||||
for platform in instance.hass.data[DOMAIN].recorder_platforms.values():
|
||||
if not (
|
||||
platform_update_issues := getattr(
|
||||
platform, INTEGRATION_PLATFORM_UPDATE_STATISTICS_ISSUES, None
|
||||
@@ -885,7 +882,7 @@ def list_statistic_ids(
|
||||
# the integrations for the missing ones.
|
||||
#
|
||||
# Query all integrations with a registered recorder platform
|
||||
for platform in hass.data[DATA_RECORDER].recorder_platforms.values():
|
||||
for platform in hass.data[DOMAIN].recorder_platforms.values():
|
||||
if not (
|
||||
platform_list_statistic_ids := getattr(
|
||||
platform, INTEGRATION_PLATFORM_LIST_STATISTIC_IDS, None
|
||||
@@ -2235,7 +2232,7 @@ def _sorted_statistics_to_dict(
|
||||
def validate_statistics(hass: HomeAssistant) -> dict[str, list[ValidationIssue]]:
|
||||
"""Validate statistics."""
|
||||
platform_validation: dict[str, list[ValidationIssue]] = {}
|
||||
for platform in hass.data[DATA_RECORDER].recorder_platforms.values():
|
||||
for platform in hass.data[DOMAIN].recorder_platforms.values():
|
||||
if platform_validate_statistics := getattr(
|
||||
platform, INTEGRATION_PLATFORM_VALIDATE_STATISTICS, None
|
||||
):
|
||||
@@ -2246,7 +2243,7 @@ def validate_statistics(hass: HomeAssistant) -> dict[str, list[ValidationIssue]]
|
||||
def update_statistics_issues(hass: HomeAssistant) -> None:
|
||||
"""Update statistics issues."""
|
||||
with session_scope(hass=hass, read_only=True) as session:
|
||||
for platform in hass.data[DATA_RECORDER].recorder_platforms.values():
|
||||
for platform in hass.data[DOMAIN].recorder_platforms.values():
|
||||
if platform_update_statistics_issues := getattr(
|
||||
platform, INTEGRATION_PLATFORM_UPDATE_STATISTICS_ISSUES, None
|
||||
):
|
||||
|
||||
@@ -11,11 +11,11 @@ import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.helpers.recorder import DATA_RECORDER
|
||||
from homeassistant.helpers.typing import UndefinedType
|
||||
from homeassistant.util.event_type import EventType
|
||||
|
||||
from . import entity_registry, purge, statistics
|
||||
from .const import DOMAIN
|
||||
from .db_schema import Statistics, StatisticsShortTerm
|
||||
from .models import StatisticData, StatisticMetaData
|
||||
from .util import periodic_db_cleanups, session_scope
|
||||
@@ -308,7 +308,7 @@ class AddRecorderPlatformTask(RecorderTask):
|
||||
hass = instance.hass
|
||||
domain = self.domain
|
||||
platform = self.platform
|
||||
platforms: dict[str, Any] = hass.data[DATA_RECORDER].recorder_platforms
|
||||
platforms: dict[str, Any] = hass.data[DOMAIN].recorder_platforms
|
||||
platforms[domain] = platform
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
"""Support to interact with Remember The Milk."""
|
||||
|
||||
from aiortm import AioRTMClient, Auth, AuthError
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from rtmapi import Rtm
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import configurator
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .entity import RememberTheMilkEntity
|
||||
from .storage import RememberTheMilkConfiguration
|
||||
|
||||
# httplib2 is a transitive dependency from RtmAPI. If this dependency is not
|
||||
# set explicitly, the library does not work.
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "remember_the_milk"
|
||||
DEFAULT_NAME = DOMAIN
|
||||
|
||||
CONF_SHARED_SECRET = "shared_secret"
|
||||
CONF_ID_MAP = "id_map"
|
||||
CONF_LIST_ID = "list_id"
|
||||
CONF_TIMESERIES_ID = "timeseries_id"
|
||||
CONF_TASK_ID = "task_id"
|
||||
|
||||
RTM_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -32,6 +41,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.All(cv.ensure_list, [RTM_SCHEMA])}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
CONFIG_FILE_NAME = ".remember_the_milk.conf"
|
||||
SERVICE_CREATE_TASK = "create_task"
|
||||
SERVICE_COMPLETE_TASK = "complete_task"
|
||||
|
||||
@@ -42,21 +52,20 @@ SERVICE_SCHEMA_CREATE_TASK = vol.Schema(
|
||||
SERVICE_SCHEMA_COMPLETE_TASK = vol.Schema({vol.Required(CONF_ID): cv.string})
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Remember the milk component."""
|
||||
component = EntityComponent[RememberTheMilkEntity](LOGGER, DOMAIN, hass)
|
||||
component = EntityComponent[RememberTheMilkEntity](_LOGGER, DOMAIN, hass)
|
||||
|
||||
stored_rtm_config = RememberTheMilkConfiguration(hass)
|
||||
await stored_rtm_config.setup()
|
||||
for rtm_config in config[DOMAIN]:
|
||||
account_name = rtm_config[CONF_NAME]
|
||||
LOGGER.debug("Adding Remember the milk account %s", account_name)
|
||||
_LOGGER.debug("Adding Remember the milk account %s", account_name)
|
||||
api_key = rtm_config[CONF_API_KEY]
|
||||
shared_secret = rtm_config[CONF_SHARED_SECRET]
|
||||
token = stored_rtm_config.get_token(account_name)
|
||||
if token:
|
||||
LOGGER.debug("found token for account %s", account_name)
|
||||
await _create_instance(
|
||||
_LOGGER.debug("found token for account %s", account_name)
|
||||
_create_instance(
|
||||
hass,
|
||||
account_name,
|
||||
api_key,
|
||||
@@ -66,43 +75,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
component,
|
||||
)
|
||||
else:
|
||||
await _register_new_account(
|
||||
_register_new_account(
|
||||
hass, account_name, api_key, shared_secret, stored_rtm_config, component
|
||||
)
|
||||
|
||||
LOGGER.debug("Finished adding all Remember the milk accounts")
|
||||
_LOGGER.debug("Finished adding all Remember the milk accounts")
|
||||
return True
|
||||
|
||||
|
||||
async def _create_instance(
|
||||
hass: HomeAssistant,
|
||||
account_name: str,
|
||||
api_key: str,
|
||||
shared_secret: str,
|
||||
token: str,
|
||||
stored_rtm_config: RememberTheMilkConfiguration,
|
||||
component: EntityComponent[RememberTheMilkEntity],
|
||||
) -> None:
|
||||
client = AioRTMClient(
|
||||
Auth(
|
||||
async_get_clientsession(hass),
|
||||
api_key,
|
||||
shared_secret,
|
||||
token,
|
||||
permission="delete",
|
||||
)
|
||||
def _create_instance(
|
||||
hass, account_name, api_key, shared_secret, token, stored_rtm_config, component
|
||||
):
|
||||
entity = RememberTheMilkEntity(
|
||||
account_name, api_key, shared_secret, token, stored_rtm_config
|
||||
)
|
||||
entity = RememberTheMilkEntity(account_name, client, stored_rtm_config)
|
||||
LOGGER.debug("Instance created for account %s", entity.name)
|
||||
await entity.check_token()
|
||||
await component.async_add_entities([entity])
|
||||
hass.services.async_register(
|
||||
component.add_entities([entity])
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
f"{account_name}_create_task",
|
||||
entity.create_task,
|
||||
schema=SERVICE_SCHEMA_CREATE_TASK,
|
||||
)
|
||||
hass.services.async_register(
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
f"{account_name}_complete_task",
|
||||
entity.complete_task,
|
||||
@@ -110,42 +104,29 @@ async def _create_instance(
|
||||
)
|
||||
|
||||
|
||||
async def _register_new_account(
|
||||
hass: HomeAssistant,
|
||||
account_name: str,
|
||||
api_key: str,
|
||||
shared_secret: str,
|
||||
stored_rtm_config: RememberTheMilkConfiguration,
|
||||
component: EntityComponent[RememberTheMilkEntity],
|
||||
) -> None:
|
||||
"""Register a new account."""
|
||||
auth = Auth(
|
||||
async_get_clientsession(hass), api_key, shared_secret, permission="write"
|
||||
)
|
||||
url, frob = await auth.authenticate_desktop()
|
||||
LOGGER.debug("Sent authentication request to server")
|
||||
def _register_new_account(
|
||||
hass, account_name, api_key, shared_secret, stored_rtm_config, component
|
||||
):
|
||||
request_id = None
|
||||
api = Rtm(api_key, shared_secret, "write", None)
|
||||
url, frob = api.authenticate_desktop()
|
||||
_LOGGER.debug("Sent authentication request to server")
|
||||
|
||||
@callback
|
||||
def register_account_callback(fields: list[dict[str, str]]) -> None:
|
||||
"""Call for register the configurator."""
|
||||
hass.async_create_task(handle_token(auth, frob))
|
||||
|
||||
async def handle_token(auth: Auth, frob: str) -> None:
|
||||
"""Handle token."""
|
||||
try:
|
||||
auth_data = await auth.get_token(frob)
|
||||
except AuthError:
|
||||
LOGGER.error("Failed to register, please try again")
|
||||
configurator.async_notify_errors(
|
||||
api.retrieve_token(frob)
|
||||
token = api.token
|
||||
if api.token is None:
|
||||
_LOGGER.error("Failed to register, please try again")
|
||||
configurator.notify_errors(
|
||||
hass, request_id, "Failed to register, please try again."
|
||||
)
|
||||
return
|
||||
|
||||
token: str = auth_data["token"]
|
||||
stored_rtm_config.set_token(account_name, token)
|
||||
LOGGER.debug("Retrieved new token from server")
|
||||
_LOGGER.debug("Retrieved new token from server")
|
||||
|
||||
await _create_instance(
|
||||
_create_instance(
|
||||
hass,
|
||||
account_name,
|
||||
api_key,
|
||||
@@ -155,9 +136,9 @@ async def _register_new_account(
|
||||
component,
|
||||
)
|
||||
|
||||
configurator.async_request_done(hass, request_id)
|
||||
configurator.request_done(hass, request_id)
|
||||
|
||||
request_id = configurator.async_request_config(
|
||||
request_id = configurator.request_config(
|
||||
hass,
|
||||
f"{DOMAIN} - {account_name}",
|
||||
callback=register_account_callback,
|
||||
@@ -171,3 +152,104 @@ async def _register_new_account(
|
||||
link_url=url,
|
||||
submit_caption="login completed",
|
||||
)
|
||||
|
||||
|
||||
class RememberTheMilkConfiguration:
|
||||
"""Internal configuration data for RememberTheMilk class.
|
||||
|
||||
This class stores the authentication token it get from the backend.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Create new instance of configuration."""
|
||||
self._config_file_path = hass.config.path(CONFIG_FILE_NAME)
|
||||
self._config = {}
|
||||
_LOGGER.debug("Loading configuration from file: %s", self._config_file_path)
|
||||
try:
|
||||
self._config = json.loads(
|
||||
Path(self._config_file_path).read_text(encoding="utf8")
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.debug("Missing configuration file: %s", self._config_file_path)
|
||||
except OSError:
|
||||
_LOGGER.debug(
|
||||
"Failed to read from configuration file, %s, using empty configuration",
|
||||
self._config_file_path,
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.error(
|
||||
"Failed to parse configuration file, %s, using empty configuration",
|
||||
self._config_file_path,
|
||||
)
|
||||
|
||||
def _save_config(self) -> None:
|
||||
"""Write the configuration to a file."""
|
||||
Path(self._config_file_path).write_text(
|
||||
json.dumps(self._config), encoding="utf8"
|
||||
)
|
||||
|
||||
def get_token(self, profile_name: str) -> str | None:
|
||||
"""Get the server token for a profile."""
|
||||
if profile_name in self._config:
|
||||
return self._config[profile_name][CONF_TOKEN]
|
||||
return None
|
||||
|
||||
def set_token(self, profile_name: str, token: str) -> None:
|
||||
"""Store a new server token for a profile."""
|
||||
self._initialize_profile(profile_name)
|
||||
self._config[profile_name][CONF_TOKEN] = token
|
||||
self._save_config()
|
||||
|
||||
def delete_token(self, profile_name: str) -> None:
|
||||
"""Delete a token for a profile.
|
||||
|
||||
Usually called when the token has expired.
|
||||
"""
|
||||
self._config.pop(profile_name, None)
|
||||
self._save_config()
|
||||
|
||||
def _initialize_profile(self, profile_name: str) -> None:
|
||||
"""Initialize the data structures for a profile."""
|
||||
if profile_name not in self._config:
|
||||
self._config[profile_name] = {}
|
||||
if CONF_ID_MAP not in self._config[profile_name]:
|
||||
self._config[profile_name][CONF_ID_MAP] = {}
|
||||
|
||||
def get_rtm_id(
|
||||
self, profile_name: str, hass_id: str
|
||||
) -> tuple[str, str, str] | None:
|
||||
"""Get the RTM ids for a Home Assistant task ID.
|
||||
|
||||
The id of a RTM tasks consists of the tuple:
|
||||
list id, timeseries id and the task id.
|
||||
"""
|
||||
self._initialize_profile(profile_name)
|
||||
ids = self._config[profile_name][CONF_ID_MAP].get(hass_id)
|
||||
if ids is None:
|
||||
return None
|
||||
return ids[CONF_LIST_ID], ids[CONF_TIMESERIES_ID], ids[CONF_TASK_ID]
|
||||
|
||||
def set_rtm_id(
|
||||
self,
|
||||
profile_name: str,
|
||||
hass_id: str,
|
||||
list_id: str,
|
||||
time_series_id: str,
|
||||
rtm_task_id: str,
|
||||
) -> None:
|
||||
"""Add/Update the RTM task ID for a Home Assistant task IS."""
|
||||
self._initialize_profile(profile_name)
|
||||
id_tuple = {
|
||||
CONF_LIST_ID: list_id,
|
||||
CONF_TIMESERIES_ID: time_series_id,
|
||||
CONF_TASK_ID: rtm_task_id,
|
||||
}
|
||||
self._config[profile_name][CONF_ID_MAP][hass_id] = id_tuple
|
||||
self._save_config()
|
||||
|
||||
def delete_rtm_id(self, profile_name: str, hass_id: str) -> None:
|
||||
"""Delete a key mapping."""
|
||||
self._initialize_profile(profile_name)
|
||||
if hass_id in self._config[profile_name][CONF_ID_MAP]:
|
||||
del self._config[profile_name][CONF_ID_MAP][hass_id]
|
||||
self._save_config()
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Constants for the Remember The Milk integration."""
|
||||
|
||||
import logging
|
||||
|
||||
DOMAIN = "remember_the_milk"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
@@ -1,64 +1,50 @@
|
||||
"""Support to interact with Remember The Milk."""
|
||||
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from aiortm import AioRTMClient, AioRTMError, AuthError
|
||||
from rtmapi import Rtm, RtmRequestFailedException
|
||||
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, STATE_OK
|
||||
from homeassistant.core import ServiceCall
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import LOGGER
|
||||
from .storage import RememberTheMilkConfiguration
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RememberTheMilkEntity(Entity):
|
||||
"""Representation of an interface to Remember The Milk."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
client: AioRTMClient,
|
||||
rtm_config: RememberTheMilkConfiguration,
|
||||
) -> None:
|
||||
def __init__(self, name, api_key, shared_secret, token, rtm_config):
|
||||
"""Create new instance of Remember The Milk component."""
|
||||
self._name = name
|
||||
self._api_key = api_key
|
||||
self._shared_secret = shared_secret
|
||||
self._token = token
|
||||
self._rtm_config = rtm_config
|
||||
self._client = client
|
||||
self._token_valid = False
|
||||
self._rtm_api = Rtm(api_key, shared_secret, "delete", token)
|
||||
self._token_valid = None
|
||||
self._check_token()
|
||||
_LOGGER.debug("Instance created for account %s", self._name)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await self.check_token()
|
||||
|
||||
async def check_token(self) -> None:
|
||||
def _check_token(self):
|
||||
"""Check if the API token is still valid.
|
||||
|
||||
If it is not valid any more, delete it from the configuration. This
|
||||
will trigger a new authentication process.
|
||||
"""
|
||||
try:
|
||||
await self._client.rtm.api.check_token()
|
||||
except AuthError as err:
|
||||
LOGGER.error(
|
||||
"Token for account %s is invalid. You need to register again: %s",
|
||||
valid = self._rtm_api.token_valid()
|
||||
if not valid:
|
||||
_LOGGER.error(
|
||||
"Token for account %s is invalid. You need to register again!",
|
||||
self.name,
|
||||
err,
|
||||
)
|
||||
except AioRTMError as err:
|
||||
LOGGER.error(
|
||||
"Error checking token for account %s. You need to register again: %s",
|
||||
self.name,
|
||||
err,
|
||||
)
|
||||
self._rtm_config.delete_token(self._name)
|
||||
self._token_valid = False
|
||||
else:
|
||||
self._token_valid = True
|
||||
return
|
||||
return self._token_valid
|
||||
|
||||
self._rtm_config.delete_token(self._name)
|
||||
self._token_valid = False
|
||||
|
||||
async def create_task(self, call: ServiceCall) -> None:
|
||||
def create_task(self, call: ServiceCall) -> None:
|
||||
"""Create a new task on Remember The Milk.
|
||||
|
||||
You can use the smart syntax to define the attributes of a new task,
|
||||
@@ -71,70 +57,50 @@ class RememberTheMilkEntity(Entity):
|
||||
rtm_id = None
|
||||
if hass_id is not None:
|
||||
rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id)
|
||||
result = self._rtm_api.rtm.timelines.create()
|
||||
timeline = result.timeline.value
|
||||
|
||||
if rtm_id is None:
|
||||
rtm_id = await self._add_task(task_name)
|
||||
LOGGER.debug(
|
||||
if hass_id is None or rtm_id is None:
|
||||
result = self._rtm_api.rtm.tasks.add(
|
||||
timeline=timeline, name=task_name, parse="1"
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Created new task '%s' in account %s", task_name, self.name
|
||||
)
|
||||
if hass_id is not None:
|
||||
self._rtm_config.set_rtm_id(
|
||||
self._name,
|
||||
hass_id,
|
||||
rtm_id[0],
|
||||
rtm_id[1],
|
||||
rtm_id[2],
|
||||
)
|
||||
self._rtm_config.set_rtm_id(
|
||||
self._name,
|
||||
hass_id,
|
||||
result.list.id,
|
||||
result.list.taskseries.id,
|
||||
result.list.taskseries.task.id,
|
||||
)
|
||||
else:
|
||||
await self._rename_task(rtm_id, task_name)
|
||||
LOGGER.debug(
|
||||
self._rtm_api.rtm.tasks.setName(
|
||||
name=task_name,
|
||||
list_id=rtm_id[0],
|
||||
taskseries_id=rtm_id[1],
|
||||
task_id=rtm_id[2],
|
||||
timeline=timeline,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Updated task with id '%s' in account %s to name %s",
|
||||
hass_id,
|
||||
self.name,
|
||||
task_name,
|
||||
)
|
||||
except AioRTMError as err:
|
||||
LOGGER.error(
|
||||
except RtmRequestFailedException as rtm_exception:
|
||||
_LOGGER.error(
|
||||
"Error creating new Remember The Milk task for account %s: %s",
|
||||
self._name,
|
||||
err,
|
||||
rtm_exception,
|
||||
)
|
||||
|
||||
async def _add_task(self, task_name: str) -> tuple[str, str, str]:
|
||||
"""Add a task."""
|
||||
timeline_response = await self._client.rtm.timelines.create()
|
||||
timeline = timeline_response.timeline
|
||||
task_response = await self._client.rtm.tasks.add(
|
||||
timeline=timeline,
|
||||
name=task_name,
|
||||
parse=True,
|
||||
)
|
||||
task_list = task_response.task_list
|
||||
task_list_id = task_list.id
|
||||
task_series = task_list.taskseries[0]
|
||||
task_series_id = task_series.id
|
||||
task = task_series.task[0]
|
||||
task_id = task.id
|
||||
return (str(task_list_id), str(task_series_id), str(task_id))
|
||||
|
||||
async def _rename_task(self, rtm_id: tuple[str, str, str], task_name: str) -> None:
|
||||
"""Rename a task."""
|
||||
result = await self._client.rtm.timelines.create()
|
||||
timeline = result.timeline
|
||||
await self._client.rtm.tasks.set_name(
|
||||
name=task_name,
|
||||
list_id=int(rtm_id[0]),
|
||||
taskseries_id=int(rtm_id[1]),
|
||||
task_id=int(rtm_id[2]),
|
||||
timeline=timeline,
|
||||
)
|
||||
|
||||
async def complete_task(self, call: ServiceCall) -> None:
|
||||
def complete_task(self, call: ServiceCall) -> None:
|
||||
"""Complete a task that was previously created by this component."""
|
||||
hass_id = call.data[CONF_ID]
|
||||
rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id)
|
||||
if rtm_id is None:
|
||||
LOGGER.error(
|
||||
_LOGGER.error(
|
||||
(
|
||||
"Could not find task with ID %s in account %s. "
|
||||
"So task could not be closed"
|
||||
@@ -144,36 +110,32 @@ class RememberTheMilkEntity(Entity):
|
||||
)
|
||||
return
|
||||
try:
|
||||
await self._complete_task(rtm_id)
|
||||
except AioRTMError as err:
|
||||
LOGGER.error(
|
||||
result = self._rtm_api.rtm.timelines.create()
|
||||
timeline = result.timeline.value
|
||||
self._rtm_api.rtm.tasks.complete(
|
||||
list_id=rtm_id[0],
|
||||
taskseries_id=rtm_id[1],
|
||||
task_id=rtm_id[2],
|
||||
timeline=timeline,
|
||||
)
|
||||
self._rtm_config.delete_rtm_id(self._name, hass_id)
|
||||
_LOGGER.debug(
|
||||
"Completed task with id %s in account %s", hass_id, self._name
|
||||
)
|
||||
except RtmRequestFailedException as rtm_exception:
|
||||
_LOGGER.error(
|
||||
"Error creating new Remember The Milk task for account %s: %s",
|
||||
self._name,
|
||||
err,
|
||||
rtm_exception,
|
||||
)
|
||||
return
|
||||
|
||||
self._rtm_config.delete_rtm_id(self._name, hass_id)
|
||||
LOGGER.debug("Completed task with id %s in account %s", hass_id, self._name)
|
||||
|
||||
async def _complete_task(self, rtm_id: tuple[str, str, str]) -> None:
|
||||
"""Complete a task."""
|
||||
result = await self._client.rtm.timelines.create()
|
||||
timeline = result.timeline
|
||||
await self._client.rtm.tasks.complete(
|
||||
list_id=int(rtm_id[0]),
|
||||
taskseries_id=int(rtm_id[1]),
|
||||
task_id=int(rtm_id[2]),
|
||||
timeline=timeline,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if not self._token_valid:
|
||||
return "API token invalid"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user