mirror of
https://github.com/home-assistant/core.git
synced 2026-06-18 09:52:57 +02:00
Compare commits
47 Commits
marantz-rs232
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 409ac3fd82 | |||
| 605f69f056 | |||
| 2f9d39827d | |||
| c268610f7d | |||
| c6de1e9c5d | |||
| cd872c4b1c | |||
| c7dd266d07 | |||
| 0adc26cec8 | |||
| 645caea76e | |||
| 944301a9e0 | |||
| 7a1e91e47a | |||
| 64c6ef4a74 | |||
| 4d7a2358fa | |||
| 9430e84506 | |||
| ebe80afbb8 | |||
| 8bd19b6a30 | |||
| 34434e8508 | |||
| b26fcc523e | |||
| f045b68493 | |||
| 48ba38f5f5 | |||
| 44f5ad84b9 | |||
| 7330c25685 | |||
| e59086d299 | |||
| 27f44f83fb | |||
| 05c94fa578 | |||
| 64b608b439 | |||
| c5b90cf8d1 | |||
| 31259725ec | |||
| 8ad7c12405 | |||
| 66f0f170b7 | |||
| da91865130 | |||
| 3437bcfb42 | |||
| 1fd5d0a5fd | |||
| 404c58435a | |||
| 7aba1daa16 | |||
| 12397cc4c1 | |||
| 73cdf7e067 | |||
| 4e2cfecd96 | |||
| 4625f7de27 | |||
| 53a1db405c | |||
| ff7262d36f | |||
| 54feb95b76 | |||
| d9e2b49c0c | |||
| 4f9051464d | |||
| 87894fd623 | |||
| 34a70a9210 | |||
| c9fb6a13fb |
@@ -24,7 +24,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
## Development Commands
|
||||
|
||||
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
|
||||
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing. If uv reports that no download was found for the required Python version, the environment is running an outdated version of uv; upgrade it with `curl -LsSf https://astral.sh/uv/install.sh | sh` and run `script/setup` again.
|
||||
- .vscode/tasks.json contains useful commands used for development.
|
||||
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
|
||||
|
||||
|
||||
@@ -164,6 +164,9 @@ Read the JSON directly for the full schema. Key fields:
|
||||
- `{{CHECK_DETAIL:<pkg>:<kind>}}` → `<icon> <one-line explanation>`
|
||||
(the bullet's `- **<label>**:` prefix is already rendered; replace
|
||||
only the placeholder).
|
||||
- `{{SUMMARY}}` → the single top-of-comment summary line, present only
|
||||
when at least one check needed resolving. Fill it **after** resolving
|
||||
every check, based on the final cell verdicts (see Step 3).
|
||||
|
||||
Do not modify other content in `rendered_comment`, do not re-evaluate
|
||||
deterministic checks, do not add or remove packages. If `needs_agent`
|
||||
@@ -192,6 +195,13 @@ Replace every placeholder with the resolved value and emit
|
||||
`<!-- requirements-check -->` marker. The PR target is already wired;
|
||||
do not pass `item_number`.
|
||||
|
||||
If a `{{SUMMARY}}` placeholder is present, replace it last, once every
|
||||
`{{CHECK_CELL:…}}` is resolved:
|
||||
- `All requirements checks passed. ✅` — when every check cell across all
|
||||
packages is `✅` or `☑️` (treat `—`/skipped as not a problem).
|
||||
- `⚠️ Some checks require attention — see the details below.` — when any
|
||||
cell is `⚠️` or `❌`.
|
||||
|
||||
## Check instructions
|
||||
|
||||
### Check kind: `repo_public`
|
||||
|
||||
@@ -13,7 +13,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
## Development Commands
|
||||
|
||||
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
|
||||
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing. If uv reports that no download was found for the required Python version, the environment is running an outdated version of uv; upgrade it with `curl -LsSf https://astral.sh/uv/install.sh | sh` and run `script/setup` again.
|
||||
- .vscode/tasks.json contains useful commands used for development.
|
||||
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
|
||||
|
||||
|
||||
Generated
+2
-4
@@ -1063,8 +1063,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/madvr/ @iloveicedgreentea
|
||||
/homeassistant/components/marantz_infrared/ @balloob
|
||||
/tests/components/marantz_infrared/ @balloob
|
||||
/homeassistant/components/marantz_rs232/ @balloob
|
||||
/tests/components/marantz_rs232/ @balloob
|
||||
/homeassistant/components/mastodon/ @fabaff @andrew-codechimp
|
||||
/tests/components/mastodon/ @fabaff @andrew-codechimp
|
||||
/homeassistant/components/matrix/ @PaarthShah
|
||||
@@ -1169,8 +1167,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/mutesync/ @currentoor
|
||||
/homeassistant/components/my/ @home-assistant/core
|
||||
/tests/components/my/ @home-assistant/core
|
||||
/homeassistant/components/myneomitis/ @l-pr
|
||||
/tests/components/myneomitis/ @l-pr
|
||||
/homeassistant/components/myneomitis/ @Epyes
|
||||
/tests/components/myneomitis/ @Epyes
|
||||
/homeassistant/components/mysensors/ @MartinHjelmare @functionpointer
|
||||
/tests/components/mysensors/ @MartinHjelmare @functionpointer
|
||||
/homeassistant/components/mystrom/ @fabaff
|
||||
|
||||
@@ -210,7 +210,7 @@ async def async_generate_image(
|
||||
|
||||
source = hass.data[DATA_MEDIA_SOURCE]
|
||||
|
||||
current_time = datetime.now()
|
||||
current_time = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
ext = mimetypes.guess_extension(task_result.mime_type, False) or ".png"
|
||||
sanitized_task_name = RE_SANITIZE_FILENAME.sub("", slugify(task_name))
|
||||
|
||||
|
||||
@@ -190,8 +190,11 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
await self._async_remove_device_stale(stale_devices)
|
||||
self.previous_devices = current_devices
|
||||
|
||||
current_routines = {slugify(routine) for routine in self.api.routines}
|
||||
if stale_routines := self.previous_routines - current_routines:
|
||||
current_routines = {
|
||||
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}"
|
||||
for routine in self.api.routines
|
||||
}
|
||||
if stale_routines := (self.previous_routines - current_routines):
|
||||
await self._async_remove_routine_stale(stale_routines)
|
||||
self.previous_routines = current_routines
|
||||
|
||||
@@ -225,17 +228,19 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
"""Remove stale routine."""
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
for routine in stale_routines:
|
||||
_LOGGER.debug(
|
||||
"Detected change in routines: routine %s removed",
|
||||
routine,
|
||||
)
|
||||
for routine_unique_id in stale_routines:
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.BUTTON,
|
||||
DOMAIN,
|
||||
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}",
|
||||
routine_unique_id,
|
||||
)
|
||||
if entity_id:
|
||||
_LOGGER.debug(
|
||||
"Detected change in routines: routine %s removed",
|
||||
routine_unique_id.replace(
|
||||
f"{slugify(self.config_entry.unique_id)}-", ""
|
||||
),
|
||||
)
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
async def sync_history_state(self) -> None:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==14.0.4"]
|
||||
"requirements": ["aioamazondevices==14.1.3"]
|
||||
}
|
||||
|
||||
@@ -34,11 +34,13 @@ def generate_site_selector_name(site: Site) -> str:
|
||||
|
||||
|
||||
def filter_sites(sites: list[Site]) -> list[Site]:
|
||||
"""Deduplicates the list of sites."""
|
||||
"""Filter out closed sites and deduplicate the list of sites."""
|
||||
filtered: list[Site] = []
|
||||
filtered_nmi: set[str] = set()
|
||||
|
||||
for site in sorted(sites, key=lambda site: site.status):
|
||||
if site.status == SiteStatus.CLOSED:
|
||||
continue
|
||||
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
|
||||
filtered.append(site)
|
||||
filtered_nmi.add(site.nmi)
|
||||
|
||||
@@ -75,7 +75,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
**user_input,
|
||||
CONF_BRAND: user_input[CONF_BRAND],
|
||||
CONF_REFRESH_TOKEN: refresh_token,
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
},
|
||||
)
|
||||
|
||||
@@ -119,7 +119,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_updates={
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
CONF_REFRESH_TOKEN: refresh_token,
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
|
||||
+ REFRESH_TOKEN_EXPIRY_TIME.total_seconds()
|
||||
)
|
||||
try:
|
||||
if datetime.now().timestamp() >= expiry_time:
|
||||
if datetime.now().timestamp() >= expiry_time: # pylint: disable=home-assistant-enforce-naive-now
|
||||
await self._reauthenticate()
|
||||
else:
|
||||
await self.aquacell_api.authenticate_refresh(self.refresh_token)
|
||||
@@ -92,7 +92,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
|
||||
data = {
|
||||
**self.config_entry.data,
|
||||
CONF_REFRESH_TOKEN: self.refresh_token,
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
}
|
||||
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyaqvify"],
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyaqvify==0.0.11"]
|
||||
}
|
||||
|
||||
@@ -53,29 +53,43 @@ rules:
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: todo
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
Discovery not possible, as device is connected via 4G only. No LAN connection.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
Discovery not possible, as device is connected via 4G only. No LAN connection.
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations:
|
||||
status: done
|
||||
comment: |
|
||||
No known limitations
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category:
|
||||
status: done
|
||||
comment: |
|
||||
None of current sensors should be set as diagnostic
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
No repair issues are created.
|
||||
stale-devices: done
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
|
||||
@@ -16,7 +16,11 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceInfo,
|
||||
format_mac,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
@@ -392,6 +396,8 @@ class AsusWrtRouter:
|
||||
serial_number=self._api.serial_number,
|
||||
manufacturer="Asus",
|
||||
)
|
||||
if label_mac := self._api.label_mac:
|
||||
info["connections"] = {(CONNECTION_NETWORK_MAC, label_mac)}
|
||||
if self._api.firmware:
|
||||
info["sw_version"] = self._api.firmware
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def _activity_time_based(latest: Activity) -> Activity | None:
|
||||
"""Get the latest state of the sensor."""
|
||||
start = latest.activity_start_time
|
||||
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
|
||||
if start <= datetime.now() <= end:
|
||||
if start <= datetime.now() <= end: # pylint: disable=home-assistant-enforce-naive-now
|
||||
return latest
|
||||
return None
|
||||
|
||||
|
||||
@@ -239,11 +239,11 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
|
||||
self.source = None
|
||||
if start_datetime := playing_info.get("startDateTime"):
|
||||
start_datetime = datetime.fromisoformat(start_datetime)
|
||||
current_datetime = datetime.now().replace(tzinfo=start_datetime.tzinfo)
|
||||
current_datetime = datetime.now().replace(tzinfo=start_datetime.tzinfo) # pylint: disable=home-assistant-enforce-naive-now
|
||||
self.media_position = int(
|
||||
(current_datetime - start_datetime).total_seconds()
|
||||
)
|
||||
self.media_position_updated_at = datetime.now()
|
||||
self.media_position_updated_at = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
else:
|
||||
self.media_position = None
|
||||
self.media_position_updated_at = None
|
||||
|
||||
@@ -31,7 +31,7 @@ class BroadlinkHeartbeat:
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the heartbeat."""
|
||||
if self._unsubscribe is None:
|
||||
await self.async_heartbeat(dt.datetime.now())
|
||||
await self.async_heartbeat(dt.datetime.now()) # pylint: disable=home-assistant-enforce-naive-now
|
||||
self._unsubscribe = event.async_track_time_interval(
|
||||
self._hass, self.async_heartbeat, self.HEARTBEAT_INTERVAL
|
||||
)
|
||||
|
||||
@@ -158,7 +158,7 @@ class BrData:
|
||||
|
||||
_LOGGER.debug("Buienradar parsed data: %s", result)
|
||||
if result.get(SUCCESS) is not True:
|
||||
if int(datetime.now().strftime("%H")) > 0:
|
||||
if int(datetime.now().strftime("%H")) > 0: # pylint: disable=home-assistant-enforce-naive-now
|
||||
_LOGGER.warning(
|
||||
"Unable to parse data from Buienradar. (Msg: %s)",
|
||||
result.get(MESSAGE),
|
||||
|
||||
@@ -5,6 +5,6 @@ from datetime import timedelta
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "duco"
|
||||
PLATFORMS = [Platform.FAN, Platform.SENSOR]
|
||||
PLATFORMS = [Platform.FAN, Platform.SELECT, Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
BOX_NODE_ID = 1
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from duco_connectivity import DucoClient
|
||||
from duco_connectivity.exceptions import (
|
||||
@@ -9,7 +10,7 @@ from duco_connectivity.exceptions import (
|
||||
DucoError,
|
||||
DucoResponseError,
|
||||
)
|
||||
from duco_connectivity.models import BoardInfo, Node
|
||||
from duco_connectivity.models import BoardInfo, Node, NodeListActionItemList
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -29,6 +30,7 @@ class DucoData:
|
||||
"""Data returned by the Duco coordinator."""
|
||||
|
||||
nodes: dict[int, Node]
|
||||
node_actions: NodeListActionItemList
|
||||
rssi_wifi: int | None
|
||||
|
||||
|
||||
@@ -67,19 +69,16 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except DucoConnectionError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except DucoError as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
async def _async_update_data(self) -> DucoData:
|
||||
@@ -90,15 +89,30 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except DucoError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
try:
|
||||
node_actions = await self.client.async_get_node_actions()
|
||||
except DucoError as err:
|
||||
previous_data = cast(DucoData | None, self.data)
|
||||
node_actions = (
|
||||
previous_data.node_actions
|
||||
if previous_data is not None
|
||||
else NodeListActionItemList(nodes=[])
|
||||
)
|
||||
_LOGGER.warning(
|
||||
"Could not fetch Duco node actions; %s",
|
||||
"keeping previous select discovery data"
|
||||
if previous_data is not None
|
||||
else "starting with empty select discovery data",
|
||||
exc_info=err,
|
||||
)
|
||||
|
||||
# LAN info only backs the diagnostic RSSI sensor, so failures on this
|
||||
# supplemental endpoint, including connection failures, should not make
|
||||
# the primary node entities unavailable.
|
||||
@@ -112,5 +126,6 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
|
||||
return DucoData(
|
||||
nodes={node.node_id: node for node in nodes},
|
||||
node_actions=node_actions,
|
||||
rssi_wifi=rssi_wifi,
|
||||
)
|
||||
|
||||
@@ -64,7 +64,6 @@ async def async_get_config_entry_diagnostics(
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
api_info: dict[str, Any] = {"public_api_version": api_info_obj.public_api_version}
|
||||
|
||||
@@ -132,6 +132,5 @@ class DucoVentilationFanEntity(DucoEntity, FanEntity):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="failed_to_set_state",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Select platform for the Duco integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from duco_connectivity import (
|
||||
ActionItem,
|
||||
DucoError,
|
||||
DucoRateLimitError,
|
||||
KnownActionName,
|
||||
Node,
|
||||
NodeListActionItemList,
|
||||
NodeType,
|
||||
VentilationState,
|
||||
)
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import DucoConfigEntry, DucoCoordinator
|
||||
from .entity import DucoEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
def _get_ventilation_options(action: ActionItem) -> tuple[str, ...] | None:
|
||||
"""Return ventilation options advertised by a node action."""
|
||||
if action.action.known_value is not KnownActionName.SET_VENTILATION_STATE:
|
||||
return None
|
||||
|
||||
options = tuple(str(value) for value in action.enum_values if value)
|
||||
return options or None
|
||||
|
||||
|
||||
def _discover_ventilation_options(
|
||||
node_actions: NodeListActionItemList,
|
||||
) -> dict[int, tuple[str, ...]]:
|
||||
"""Build a node-to-options map from the node action metadata."""
|
||||
options_by_node: dict[int, tuple[str, ...]] = {}
|
||||
|
||||
for node_action in node_actions.nodes:
|
||||
for action in node_action.actions:
|
||||
if options := _get_ventilation_options(action):
|
||||
options_by_node[node_action.node_id] = options
|
||||
break
|
||||
|
||||
return options_by_node
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: DucoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Duco select entities."""
|
||||
coordinator = entry.runtime_data
|
||||
known_nodes: set[int] = set()
|
||||
|
||||
@callback
|
||||
def _async_add_new_entities() -> None:
|
||||
"""Add select entities for newly discovered controllable nodes."""
|
||||
options_by_node = _discover_ventilation_options(coordinator.data.node_actions)
|
||||
new_entities: list[DucoVentilationStateSelect] = []
|
||||
|
||||
for node in coordinator.data.nodes.values():
|
||||
if node.node_id in known_nodes:
|
||||
continue
|
||||
|
||||
if node.general.node_type is not NodeType.BOX:
|
||||
continue
|
||||
|
||||
options = options_by_node.get(node.node_id)
|
||||
if options is None:
|
||||
continue
|
||||
|
||||
known_nodes.add(node.node_id)
|
||||
new_entities.append(DucoVentilationStateSelect(coordinator, node, options))
|
||||
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_entities))
|
||||
_async_add_new_entities()
|
||||
|
||||
|
||||
class DucoVentilationStateSelect(DucoEntity, SelectEntity):
|
||||
"""Select entity for node ventilation states."""
|
||||
|
||||
_attr_translation_key = "ventilation_state"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DucoCoordinator,
|
||||
node: Node,
|
||||
options: tuple[str, ...],
|
||||
) -> None:
|
||||
"""Initialize the select entity."""
|
||||
super().__init__(coordinator, node)
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.unique_id}_{node.node_id}_ventilation_state"
|
||||
)
|
||||
self._attr_options = list(options)
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the current ventilation state when it is selectable."""
|
||||
if (ventilation := self._node.ventilation) is None:
|
||||
return None
|
||||
|
||||
if ventilation.state is VentilationState.UNKNOWN:
|
||||
return None
|
||||
|
||||
state = ventilation.state.value
|
||||
if state not in self.options:
|
||||
return None
|
||||
|
||||
return state
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set a new ventilation state on the node."""
|
||||
try:
|
||||
# SelectEntity exposes string options, and passing the raw API value
|
||||
# through keeps newly added Duco states forward-compatible.
|
||||
await self.coordinator.client.async_set_ventilation_state(
|
||||
self._node_id, option
|
||||
)
|
||||
except DucoRateLimitError as err:
|
||||
_LOGGER.warning("Duco write rate limit exceeded for node %s", self._node_id)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="rate_limit_exceeded",
|
||||
) from err
|
||||
except DucoError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="failed_to_set_state",
|
||||
) from err
|
||||
|
||||
# Duco may normalize the requested action on readback, such as
|
||||
# MAN1x2 -> MAN1 or AUTO -> CNT1, so refresh the authoritative state.
|
||||
await self.coordinator.async_refresh()
|
||||
@@ -48,6 +48,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"ventilation_state": {
|
||||
"name": "Ventilation state",
|
||||
"state": {
|
||||
"aut1": "AUT1",
|
||||
"aut2": "AUT2",
|
||||
"aut3": "AUT3",
|
||||
"auto": "AUTO",
|
||||
"cnt1": "CNT1",
|
||||
"cnt2": "CNT2",
|
||||
"cnt3": "CNT3",
|
||||
"empt": "EMPT",
|
||||
"man1": "MAN1",
|
||||
"man1x2": "MAN1x2",
|
||||
"man1x3": "MAN1x3",
|
||||
"man2": "MAN2",
|
||||
"man2x2": "MAN2x2",
|
||||
"man2x3": "MAN2x3",
|
||||
"man3": "MAN3",
|
||||
"man3x2": "MAN3x2",
|
||||
"man3x3": "MAN3x3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"iaq_co2": {
|
||||
"name": "CO2 air quality index"
|
||||
@@ -87,16 +111,16 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"api_error": {
|
||||
"message": "Unexpected error from the Duco API: {error}"
|
||||
"message": "Unexpected error from the Duco API."
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "An error occurred while trying to connect to the Duco instance: {error}"
|
||||
"message": "Could not connect to the Duco device."
|
||||
},
|
||||
"connection_error": {
|
||||
"message": "Could not connect to the Duco device."
|
||||
},
|
||||
"failed_to_set_state": {
|
||||
"message": "Failed to set ventilation state: {error}"
|
||||
"message": "Failed to set ventilation state."
|
||||
},
|
||||
"rate_limit_exceeded": {
|
||||
"message": "The Duco device has reached its daily write limit. Try again tomorrow."
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["elevenlabs"],
|
||||
"requirements": ["elevenlabs==2.3.0", "sentence-stream==1.2.0"]
|
||||
"requirements": ["elevenlabs==2.51.0", "sentence-stream==1.3.0"]
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class FireflyDataUpdateCoordinator(DataUpdateCoordinator[FireflyCoordinatorData]
|
||||
|
||||
async def _async_update_data(self) -> FireflyCoordinatorData:
|
||||
"""Fetch data from Firefly III API."""
|
||||
now = datetime.now()
|
||||
now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
end_date = now
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ class FreeboxRouter:
|
||||
"IPv6": connection_datas.get("ipv6"),
|
||||
"connection_type": connection_datas["media"],
|
||||
"uptime": datetime.fromtimestamp(
|
||||
round(datetime.now().timestamp()) - syst_datas["uptime_val"]
|
||||
round(datetime.now().timestamp()) - syst_datas["uptime_val"] # pylint: disable=home-assistant-enforce-naive-now
|
||||
),
|
||||
"firmware_version": self._sw_v,
|
||||
"serial": syst_datas["serial"],
|
||||
|
||||
@@ -54,7 +54,7 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
|
||||
|
||||
if (
|
||||
self.fyta.expiration is None
|
||||
or self.fyta.expiration.timestamp() < datetime.now().timestamp()
|
||||
or self.fyta.expiration.timestamp() < datetime.now().timestamp() # pylint: disable=home-assistant-enforce-naive-now
|
||||
):
|
||||
await self.renew_authentication()
|
||||
|
||||
|
||||
@@ -117,5 +117,5 @@ class FytaPlantImageEntity(FytaPlantEntity, ImageEntity):
|
||||
|
||||
if url != self._attr_image_url:
|
||||
self._cached_image = None
|
||||
self._attr_image_last_updated = datetime.now()
|
||||
self._attr_image_last_updated = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
return url
|
||||
|
||||
@@ -154,12 +154,12 @@ class GenericCamera(Camera):
|
||||
self._last_image is not None
|
||||
and url == self._last_url
|
||||
and self._last_update + timedelta(0, self._attr_frame_interval)
|
||||
> datetime.now()
|
||||
> datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
):
|
||||
return self._last_image
|
||||
|
||||
try:
|
||||
update_time = datetime.now()
|
||||
update_time = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
async_client = get_async_client(self.hass, verify_ssl=self.verify_ssl)
|
||||
response = await async_client.get(
|
||||
url,
|
||||
|
||||
@@ -584,7 +584,7 @@ async def ws_start_preview(
|
||||
if user_input.get(CONF_STILL_IMAGE_URL):
|
||||
ha_still_url = (
|
||||
"/api/generic/preview_flow_image"
|
||||
f"/{msg['flow_id']}?t={datetime.now().isoformat()}"
|
||||
f"/{msg['flow_id']}?t={datetime.now().isoformat()}" # pylint: disable=home-assistant-enforce-naive-now
|
||||
)
|
||||
_LOGGER.debug("Got preview still URL: %s", ha_still_url)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ SYNCHRONIZE_CLOCK = GoodweButtonEntityDescription(
|
||||
key="synchronize_clock",
|
||||
translation_key="synchronize_clock",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
action=lambda inv: inv.write_setting("time", datetime.now()),
|
||||
action=lambda inv: inv.write_setting("time", datetime.now()), # pylint: disable=home-assistant-enforce-naive-now
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bo
|
||||
# Force a token refresh to fix a bug where tokens were persisted with
|
||||
# expires_in (relative time delta) and expires_at (absolute time) swapped.
|
||||
# A google session token typically only lasts a few days between refresh.
|
||||
now = datetime.now()
|
||||
now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
if session.token["expires_at"] >= (now + timedelta(days=365)).timestamp():
|
||||
session.token["expires_in"] = 0
|
||||
session.token["expires_at"] = now.timestamp()
|
||||
|
||||
@@ -45,7 +45,7 @@ async def async_get_config_entry_diagnostics(
|
||||
payload: dict[str, Any] = {
|
||||
"now": dt_util.now().isoformat(),
|
||||
"timezone": str(dt_util.get_default_time_zone()),
|
||||
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo),
|
||||
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo), # pylint: disable=home-assistant-enforce-naive-now
|
||||
}
|
||||
|
||||
store = config_entry.runtime_data.store
|
||||
|
||||
@@ -434,49 +434,56 @@ async def google_generative_ai_config_option_schema(
|
||||
description={"suggested_value": options.get(CONF_TEMPERATURE)},
|
||||
default=RECOMMENDED_TEMPERATURE,
|
||||
): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)),
|
||||
vol.Optional(
|
||||
CONF_TOP_P,
|
||||
description={"suggested_value": options.get(CONF_TOP_P)},
|
||||
default=RECOMMENDED_TOP_P,
|
||||
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
|
||||
vol.Optional(
|
||||
CONF_TOP_K,
|
||||
description={"suggested_value": options.get(CONF_TOP_K)},
|
||||
default=RECOMMENDED_TOP_K,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
description={"suggested_value": options.get(CONF_MAX_TOKENS)},
|
||||
default=RECOMMENDED_MAX_TOKENS,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD,
|
||||
description={
|
||||
"suggested_value": options.get(CONF_HARASSMENT_BLOCK_THRESHOLD)
|
||||
},
|
||||
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
): harm_block_thresholds_selector,
|
||||
vol.Optional(
|
||||
CONF_HATE_BLOCK_THRESHOLD,
|
||||
description={"suggested_value": options.get(CONF_HATE_BLOCK_THRESHOLD)},
|
||||
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
): harm_block_thresholds_selector,
|
||||
vol.Optional(
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD,
|
||||
description={
|
||||
"suggested_value": options.get(CONF_SEXUAL_BLOCK_THRESHOLD)
|
||||
},
|
||||
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
): harm_block_thresholds_selector,
|
||||
vol.Optional(
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD,
|
||||
description={
|
||||
"suggested_value": options.get(CONF_DANGEROUS_BLOCK_THRESHOLD)
|
||||
},
|
||||
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
): harm_block_thresholds_selector,
|
||||
}
|
||||
)
|
||||
if subentry_type != "tts":
|
||||
schema.update(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_TOP_P,
|
||||
description={"suggested_value": options.get(CONF_TOP_P)},
|
||||
default=RECOMMENDED_TOP_P,
|
||||
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
|
||||
vol.Optional(
|
||||
CONF_TOP_K,
|
||||
description={"suggested_value": options.get(CONF_TOP_K)},
|
||||
default=RECOMMENDED_TOP_K,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
description={"suggested_value": options.get(CONF_MAX_TOKENS)},
|
||||
default=RECOMMENDED_MAX_TOKENS,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD,
|
||||
description={
|
||||
"suggested_value": options.get(CONF_HARASSMENT_BLOCK_THRESHOLD)
|
||||
},
|
||||
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
): harm_block_thresholds_selector,
|
||||
vol.Optional(
|
||||
CONF_HATE_BLOCK_THRESHOLD,
|
||||
description={
|
||||
"suggested_value": options.get(CONF_HATE_BLOCK_THRESHOLD)
|
||||
},
|
||||
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
): harm_block_thresholds_selector,
|
||||
vol.Optional(
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD,
|
||||
description={
|
||||
"suggested_value": options.get(CONF_SEXUAL_BLOCK_THRESHOLD)
|
||||
},
|
||||
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
): harm_block_thresholds_selector,
|
||||
vol.Optional(
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD,
|
||||
description={
|
||||
"suggested_value": options.get(CONF_DANGEROUS_BLOCK_THRESHOLD)
|
||||
},
|
||||
default=RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
): harm_block_thresholds_selector,
|
||||
}
|
||||
)
|
||||
if subentry_type == "conversation":
|
||||
schema.update(
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ CONF_RECOMMENDED = "recommended"
|
||||
CONF_CHAT_MODEL = "chat_model"
|
||||
RECOMMENDED_CHAT_MODEL = "models/gemini-3.1-flash-lite"
|
||||
RECOMMENDED_STT_MODEL = RECOMMENDED_CHAT_MODEL
|
||||
RECOMMENDED_TTS_MODEL = "models/gemini-2.5-flash-preview-tts"
|
||||
RECOMMENDED_TTS_MODEL = "models/gemini-3.1-flash-tts-preview"
|
||||
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image"
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
RECOMMENDED_TEMPERATURE = 1.0
|
||||
|
||||
@@ -18,7 +18,13 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONF_CHAT_MODEL, LOGGER, RECOMMENDED_TTS_MODEL
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_TEMPERATURE,
|
||||
LOGGER,
|
||||
RECOMMENDED_TEMPERATURE,
|
||||
RECOMMENDED_TTS_MODEL,
|
||||
)
|
||||
from .entity import GoogleGenerativeAILLMBaseEntity
|
||||
from .helpers import convert_to_wav
|
||||
|
||||
@@ -191,7 +197,10 @@ class GoogleGenerativeAITextToSpeechEntity(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
"""Load tts audio file from the engine."""
|
||||
config = self.create_generate_content_config()
|
||||
config = types.GenerateContentConfig()
|
||||
config.temperature = self.subentry.data.get(
|
||||
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
||||
)
|
||||
config.response_modalities = ["AUDIO"]
|
||||
config.speech_config = types.SpeechConfig(
|
||||
voice_config=types.VoiceConfig(
|
||||
|
||||
@@ -69,7 +69,7 @@ def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
add_created_column = call.data[ADD_CREATED_COLUMN]
|
||||
now = str(datetime.now())
|
||||
now = str(datetime.now()) # pylint: disable=home-assistant-enforce-naive-now
|
||||
rows = []
|
||||
for d in call.data[DATA]:
|
||||
row_data = ({"created": now} | d) if add_created_column else d
|
||||
|
||||
@@ -128,7 +128,7 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
||||
"""
|
||||
if not super().available:
|
||||
return False
|
||||
return datetime.now() - self._device.lastseen < DEVICE_TIMEOUT
|
||||
return datetime.now() - self._device.lastseen < DEVICE_TIMEOUT # pylint: disable=home-assistant-enforce-naive-now
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -387,6 +387,6 @@ def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:
|
||||
def next_datetime(simple_time: time) -> datetime:
|
||||
"""Take a time like 08:00:00 and combine it with the current date."""
|
||||
combined = datetime.combine(dt_util.start_of_local_day(), simple_time)
|
||||
if combined < datetime.now():
|
||||
if combined < datetime.now(): # pylint: disable=home-assistant-enforce-naive-now
|
||||
combined = combined + timedelta(days=1)
|
||||
return combined
|
||||
|
||||
@@ -135,6 +135,10 @@
|
||||
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove the {domain} entry from your configuration.yaml file and restart Home Assistant.",
|
||||
"title": "The {integration_title} integration is being removed"
|
||||
},
|
||||
"deprecated_trigger_behavior": {
|
||||
"description": "An automation, script or template entity uses the trigger behavior option `{deprecated_behavior}`, which has been renamed to `{new_behavior}`. The old value still works for now, but support for it will be removed in a future release.\n\nTo fix this issue, edit the affected automations and scripts and change the behavior option from `behavior: {deprecated_behavior}` to `behavior: {new_behavior}`, then restart Home Assistant.",
|
||||
"title": "Deprecated trigger behavior option in use"
|
||||
},
|
||||
"deprecated_yaml": {
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
|
||||
"title": "The {integration_title} YAML configuration is being removed"
|
||||
|
||||
@@ -5,17 +5,21 @@ from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import RaspberryPiFirmwareInfo
|
||||
from ha_silabs_firmware_client import FirmwareManifest, FirmwareMetadata
|
||||
from universal_silabs_flasher.flasher import DeviceSpecificFlasher
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.update import (
|
||||
UpdateDeviceClass,
|
||||
UpdateEntity,
|
||||
UpdateEntityDescription,
|
||||
UpdateEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.restore_state import ExtraStoredData
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -26,6 +30,10 @@ from .util import (
|
||||
FirmwareInfo,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
async_get_raspberry_pi_firmware_info,
|
||||
async_update_raspberry_pi_firmware,
|
||||
humanize_rpi_firmware_version,
|
||||
rpi_firmware_release_url,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -288,3 +296,91 @@ class BaseFirmwareUpdateEntity(
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._firmware_info_callback(firmware_info)
|
||||
|
||||
|
||||
class RaspberryPiFirmwareUpdateEntity(UpdateEntity):
|
||||
"""Update entity for the Raspberry Pi firmware (bootloader EEPROM and VL805).
|
||||
|
||||
There is no coordinator. The firmware state only changes after a reboot
|
||||
(which restarts Core and re-fetches at setup) or right after the install
|
||||
action (re-fetched in async_install), so polling would never show anything
|
||||
new. The board integration passes in the DeviceInfo so the entity ends up
|
||||
on that board's device.
|
||||
"""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL | UpdateEntityFeature.RELEASE_NOTES
|
||||
)
|
||||
_attr_translation_key = "rpi_firmware"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
firmware: RaspberryPiFirmwareInfo,
|
||||
device_info: DeviceInfo,
|
||||
unique_id: str,
|
||||
board: str,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
self._firmware = firmware
|
||||
self._attr_device_info = device_info
|
||||
self._attr_unique_id = unique_id
|
||||
self._board = board
|
||||
|
||||
@property
|
||||
def installed_version(self) -> str | None:
|
||||
"""Composite installed firmware version.
|
||||
|
||||
Once an update is applied (update_pending), report the new version as
|
||||
installed so the entity reads "up to date". The running firmware only
|
||||
changes after the reboot, which the Supervisor flags with a
|
||||
REBOOT_REQUIRED repair.
|
||||
"""
|
||||
if self._firmware.update_pending:
|
||||
return humanize_rpi_firmware_version(self._firmware.latest_version)
|
||||
return humanize_rpi_firmware_version(self._firmware.current_version)
|
||||
|
||||
@property
|
||||
def latest_version(self) -> str | None:
|
||||
"""Composite available firmware version."""
|
||||
return humanize_rpi_firmware_version(self._firmware.latest_version)
|
||||
|
||||
@property
|
||||
def release_url(self) -> str | None:
|
||||
"""Return the EEPROM release notes for this board's SoC."""
|
||||
return rpi_firmware_release_url(self._board)
|
||||
|
||||
async def async_release_notes(self) -> str | None:
|
||||
"""Return the pre-install warning and reboot notice as ha-alert boxes."""
|
||||
return (
|
||||
"<ha-alert alert-type='warning'>"
|
||||
"Do not interrupt the firmware flash. "
|
||||
"Power loss during the EEPROM update can render your device "
|
||||
"inoperable."
|
||||
"</ha-alert>\n\n"
|
||||
"<ha-alert alert-type='info'>"
|
||||
"A reboot is required after install for the new firmware to "
|
||||
"take effect."
|
||||
"</ha-alert>\n"
|
||||
)
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
"""Install an update."""
|
||||
await async_update_raspberry_pi_firmware(self.hass)
|
||||
# Re-fetch so the entity picks up update_pending and reads "up to date".
|
||||
try:
|
||||
refreshed = await async_get_raspberry_pi_firmware_info(self.hass)
|
||||
# pylint: disable-next=home-assistant-action-swallowed-exception
|
||||
except SupervisorError:
|
||||
# The update succeeded; keep the previous info until the next fetch.
|
||||
_LOGGER.exception(
|
||||
"Failed to refresh Raspberry Pi firmware info after update"
|
||||
)
|
||||
refreshed = None
|
||||
if refreshed is not None:
|
||||
self._firmware = refreshed
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -8,6 +8,8 @@ from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
|
||||
from aiohasupervisor import SupervisorError, SupervisorNotFoundError
|
||||
from aiohasupervisor.models import RaspberryPiFirmwareInfo
|
||||
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
|
||||
from universal_silabs_flasher.firmware import parse_firmware_image
|
||||
from universal_silabs_flasher.flasher import BaseFlasher, DeviceSpecificFlasher, Flasher
|
||||
@@ -18,12 +20,14 @@ from homeassistant.components.hassio import (
|
||||
AddonState,
|
||||
HassioNotReadyError,
|
||||
get_apps_list,
|
||||
get_supervisor_client,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import DATA_COMPONENT
|
||||
from .const import (
|
||||
@@ -515,3 +519,81 @@ async def async_flash_silabs_firmware(
|
||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
||||
|
||||
return probed_firmware_info
|
||||
|
||||
|
||||
# Supervisor os_info.board values whose os-agent exposes
|
||||
# io.hass.os.Boards.RaspberryPi.Firmware (Raspberry Pi 4/5 and Yellow CM4/CM5).
|
||||
# RPi 2/3 have no SPI EEPROM bootloader. These boards belong to different
|
||||
# integrations (raspberry_pi and homeassistant_yellow), so the shared Supervisor
|
||||
# plumbing lives here.
|
||||
BOARDS_WITH_RASPBERRYPI_FIRMWARE = frozenset({"rpi4-64", "rpi5-64", "yellow"})
|
||||
|
||||
RPI_FIRMWARE_RELEASE_URL_DEFAULT = (
|
||||
"https://github.com/raspberrypi/rpi-eeprom/blob/master/releases.md"
|
||||
)
|
||||
|
||||
# Per-SoC release notes. The Yellow can run both with a CM4 or CM5, so we don't
|
||||
# know the exact SoC.
|
||||
_RPI_FIRMWARE_RELEASE_URLS = {
|
||||
"rpi4-64": "https://github.com/raspberrypi/rpi-eeprom/blob/master/firmware-2711/release-notes.md",
|
||||
"rpi5-64": "https://github.com/raspberrypi/rpi-eeprom/blob/master/firmware-2712/release-notes.md",
|
||||
}
|
||||
|
||||
|
||||
def rpi_firmware_release_url(board: str) -> str:
|
||||
"""Return the RPi firmware release notes URL for a board."""
|
||||
return _RPI_FIRMWARE_RELEASE_URLS.get(board, RPI_FIRMWARE_RELEASE_URL_DEFAULT)
|
||||
|
||||
|
||||
def humanize_rpi_firmware_version(version: str | None) -> str | None:
|
||||
"""Turn a raw firmware version into a human-readable string.
|
||||
|
||||
The Supervisor reports the bootloader EEPROM build as a Unix timestamp,
|
||||
optionally suffixed with the VL805 EEPROM revision (timestamp-hexstring).
|
||||
Render the timestamp as a UTC YYYY-MM-DD date and append (VL805 hexstring)
|
||||
when a VL805 revision is present.
|
||||
"""
|
||||
if version is None:
|
||||
return None
|
||||
timestamp, _, vl805 = version.partition("-")
|
||||
try:
|
||||
date = dt_util.utc_from_timestamp(int(timestamp)).strftime("%Y-%m-%d")
|
||||
except ValueError:
|
||||
return version
|
||||
if vl805:
|
||||
return f"{date} (VL805 {vl805})"
|
||||
return date
|
||||
|
||||
|
||||
async def async_get_raspberry_pi_firmware_info(
|
||||
hass: HomeAssistant,
|
||||
) -> RaspberryPiFirmwareInfo | None:
|
||||
"""Return the firmware info, or None if the Supervisor doesn't expose it.
|
||||
|
||||
A 404 (SupervisorNotFoundError) means the endpoint is unavailable (older
|
||||
Supervisor, pre OS 18) and the feature is skipped. Any other SupervisorError
|
||||
is a real communication failure and is left to propagate so the caller can
|
||||
retry.
|
||||
"""
|
||||
client = get_supervisor_client(hass)
|
||||
try:
|
||||
return await client.os.raspberry_pi_firmware_info()
|
||||
except SupervisorNotFoundError:
|
||||
_LOGGER.debug("Raspberry Pi firmware endpoint unavailable")
|
||||
return None
|
||||
|
||||
|
||||
async def async_update_raspberry_pi_firmware(hass: HomeAssistant) -> None:
|
||||
"""Trigger the Raspberry Pi firmware (bootloader EEPROM and VL805) update.
|
||||
|
||||
The Supervisor always raises a reboot-required issue and suggestion on
|
||||
success. The new firmware only runs after the next reboot, whether the
|
||||
flash was live (RPi5/CM5) or staged (RPi4/CM4/Yellow).
|
||||
"""
|
||||
client = get_supervisor_client(hass)
|
||||
try:
|
||||
await client.os.update_raspberry_pi_firmware()
|
||||
except SupervisorError as err:
|
||||
raise HomeAssistantError(
|
||||
f"Error updating Raspberry Pi firmware: {err}"
|
||||
) from err
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"update": {
|
||||
"radio_firmware": {
|
||||
"name": "Radio firmware"
|
||||
},
|
||||
"rpi_firmware": {
|
||||
"name": "Firmware"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from universal_silabs_flasher.flasher import YellowFlasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
@@ -10,15 +11,17 @@ from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
from homeassistant.components.homeassistant_hardware.update import (
|
||||
BaseFirmwareUpdateEntity,
|
||||
FirmwareUpdateEntityDescription,
|
||||
RaspberryPiFirmwareUpdateEntity,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
async_get_raspberry_pi_firmware_info,
|
||||
)
|
||||
from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -141,9 +144,34 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the firmware update config entry."""
|
||||
entity = _async_create_update_entity(hass, config_entry, async_add_entities)
|
||||
entities: list[UpdateEntity] = [
|
||||
_async_create_update_entity(hass, config_entry, async_add_entities)
|
||||
]
|
||||
|
||||
async_add_entities([entity])
|
||||
try:
|
||||
rpi_firmware = await async_get_raspberry_pi_firmware_info(hass)
|
||||
except SupervisorError as err:
|
||||
# async_get_raspberry_pi_firmware_info handles 404 gracefully, anything
|
||||
# else is a genuine Supervisor error that we should log.
|
||||
_LOGGER.warning("Raspberry Pi firmware info unavailable: %s", err)
|
||||
rpi_firmware = None
|
||||
|
||||
if rpi_firmware is not None and not rpi_firmware.update_blocked:
|
||||
entities.append(
|
||||
RaspberryPiFirmwareUpdateEntity(
|
||||
rpi_firmware,
|
||||
DeviceInfo(
|
||||
identifiers={(DOMAIN, "yellow")},
|
||||
name=MODEL,
|
||||
model=MODEL,
|
||||
manufacturer=MANUFACTURER,
|
||||
),
|
||||
unique_id="yellow_rpi_firmware",
|
||||
board="yellow",
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
@@ -166,6 +194,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
name=MODEL,
|
||||
model=MODEL,
|
||||
manufacturer=MANUFACTURER,
|
||||
sw_version=None, # Radio FW exposed by the update entity, removed in 2026.7.0
|
||||
)
|
||||
|
||||
# Use the cached firmware info if it exists
|
||||
@@ -178,20 +207,6 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
source="homeassistant_yellow",
|
||||
)
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
super()._update_attributes()
|
||||
|
||||
assert self.device_entry is not None
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_registry.async_update_device(
|
||||
device_id=self.device_entry.id,
|
||||
sw_version=(
|
||||
f"{self.entity_description.firmware_name}"
|
||||
f" {self._attr_installed_version}"
|
||||
),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None:
|
||||
"""Handle updated firmware info being pushed by an integration."""
|
||||
|
||||
@@ -75,7 +75,7 @@ def format_last_reset_elapsed_seconds(value: str | None) -> datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
last_reset = datetime.now() - timedelta(seconds=int(value))
|
||||
last_reset = datetime.now() - timedelta(seconds=int(value)) # pylint: disable=home-assistant-enforce-naive-now
|
||||
last_reset.replace(microsecond=0)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@@ -87,11 +87,6 @@ class IcloudTrackerEntity(TrackerEntity):
|
||||
assert self._device.location is not None
|
||||
return self._device.location[DEVICE_LOCATION_LONGITUDE]
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int | None:
|
||||
"""Return the battery level of the device."""
|
||||
return self._device.battery_level
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
|
||||
@@ -342,7 +342,7 @@ class iOSIdentifyDeviceView(HomeAssistantView):
|
||||
|
||||
hass = request.app[KEY_HASS]
|
||||
|
||||
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat()
|
||||
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat() # pylint: disable=home-assistant-enforce-naive-now
|
||||
|
||||
device_id = data[ATTR_DEVICE_ID]
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ class IsraelRailDataUpdateCoordinator(DataUpdateCoordinator[list[DataConnection]
|
||||
self._train_schedule.query,
|
||||
self._start,
|
||||
self._destination,
|
||||
datetime.now().strftime("%Y-%m-%d"),
|
||||
datetime.now().strftime("%H:%M"),
|
||||
datetime.now().strftime("%Y-%m-%d"), # pylint: disable=home-assistant-enforce-naive-now
|
||||
datetime.now().strftime("%H:%M"), # pylint: disable=home-assistant-enforce-naive-now
|
||||
)
|
||||
except Exception as e:
|
||||
raise UpdateFailed(
|
||||
|
||||
@@ -214,7 +214,7 @@ class LgTVDevice(MediaPlayerEntity):
|
||||
def media_image_url(self):
|
||||
"""URL for obtaining a screen capture."""
|
||||
return (
|
||||
f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}"
|
||||
f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}" # pylint: disable=home-assistant-enforce-naive-now
|
||||
)
|
||||
|
||||
def turn_off(self) -> None:
|
||||
|
||||
@@ -18,7 +18,7 @@ async def async_get_config_entry_diagnostics(
|
||||
payload: dict[str, Any] = {
|
||||
"now": dt_util.now().isoformat(),
|
||||
"timezone": str(dt_util.get_default_time_zone()),
|
||||
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo),
|
||||
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo), # pylint: disable=home-assistant-enforce-naive-now
|
||||
}
|
||||
store = config_entry.runtime_data
|
||||
ics = await store.async_load()
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
"""The Marantz RS-232 integration."""
|
||||
|
||||
from marantz_rs232 import (
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
V2003ReceiverState,
|
||||
V2007ReceiverState,
|
||||
V2015ReceiverState,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .config_flow import MODEL_MODERN, V2003_MODELS, V2007_MODELS
|
||||
from .const import LOGGER, MarantzReceiver, MarantzRS232ConfigEntry
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: MarantzRS232ConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Marantz RS-232 from a config entry."""
|
||||
port = entry.data[CONF_DEVICE]
|
||||
model_key = entry.data[CONF_MODEL]
|
||||
|
||||
receiver: MarantzReceiver
|
||||
if model_key == MODEL_MODERN:
|
||||
receiver = MarantzV2015Receiver(port)
|
||||
elif model_key in V2003_MODELS:
|
||||
receiver = MarantzV2003Receiver(port)
|
||||
else:
|
||||
receiver = MarantzV2007Receiver(port, model=V2007_MODELS[model_key])
|
||||
|
||||
try:
|
||||
await receiver.connect()
|
||||
await receiver.query_state()
|
||||
except (ConnectionError, OSError, TimeoutError) as err:
|
||||
LOGGER.error("Error connecting to Marantz receiver at %s: %s", port, err)
|
||||
if receiver.connected:
|
||||
await receiver.disconnect()
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
entry.runtime_data = receiver
|
||||
|
||||
@callback
|
||||
def _on_disconnect(
|
||||
state: V2015ReceiverState | V2007ReceiverState | V2003ReceiverState | None,
|
||||
) -> None:
|
||||
# Only reload if the entry is still loaded. During entry removal,
|
||||
# disconnect() fires this callback but the entry is already gone.
|
||||
if state is None and entry.state is ConfigEntryState.LOADED:
|
||||
LOGGER.warning("Marantz receiver disconnected, reloading config entry")
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(receiver.subscribe(_on_disconnect))
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: MarantzRS232ConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
await entry.runtime_data.disconnect()
|
||||
|
||||
return unload_ok
|
||||
@@ -1,122 +0,0 @@
|
||||
"""Config flow for the Marantz RS-232 integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from marantz_rs232 import (
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
V2007Model,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
SerialPortSelector,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
MODEL_MODERN = "modern"
|
||||
MODEL_SR7002 = "sr7002"
|
||||
MODEL_SR8002 = "sr8002"
|
||||
MODEL_SR9300 = "sr9300"
|
||||
MODEL_SR8300 = "sr8300"
|
||||
|
||||
MODEL_NAMES: dict[str, str] = {
|
||||
MODEL_MODERN: "Modern",
|
||||
MODEL_SR7002: "SR7002",
|
||||
MODEL_SR8002: "SR8002",
|
||||
MODEL_SR9300: "SR9300",
|
||||
MODEL_SR8300: "SR8300",
|
||||
}
|
||||
|
||||
V2007_MODELS: dict[str, V2007Model] = {
|
||||
MODEL_SR7002: V2007Model.SR7002,
|
||||
MODEL_SR8002: V2007Model.SR8002,
|
||||
}
|
||||
|
||||
V2003_MODELS = frozenset({MODEL_SR9300, MODEL_SR8300})
|
||||
|
||||
|
||||
async def _async_attempt_connect(port: str, model_key: str) -> str | None:
|
||||
"""Attempt to connect to the receiver at the given port.
|
||||
|
||||
Returns None on success, error on failure.
|
||||
"""
|
||||
receiver: MarantzV2015Receiver | MarantzV2007Receiver | MarantzV2003Receiver
|
||||
if model_key == MODEL_MODERN:
|
||||
receiver = MarantzV2015Receiver(port)
|
||||
elif model_key in V2003_MODELS:
|
||||
receiver = MarantzV2003Receiver(port)
|
||||
else:
|
||||
receiver = MarantzV2007Receiver(port, model=V2007_MODELS[model_key])
|
||||
|
||||
try:
|
||||
await receiver.connect()
|
||||
except (
|
||||
# When the port contains invalid connection data
|
||||
ValueError,
|
||||
# If it is a remote port, and we cannot connect
|
||||
ConnectionError,
|
||||
OSError,
|
||||
TimeoutError,
|
||||
):
|
||||
return "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
return "unknown"
|
||||
else:
|
||||
await receiver.disconnect()
|
||||
return None
|
||||
|
||||
|
||||
class MarantzRS232ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Marantz RS-232."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
model_key = user_input[CONF_MODEL]
|
||||
|
||||
self._async_abort_entries_match({CONF_DEVICE: user_input[CONF_DEVICE]})
|
||||
error = await _async_attempt_connect(user_input[CONF_DEVICE], model_key)
|
||||
if not error:
|
||||
return self.async_create_entry(
|
||||
title=MODEL_NAMES[model_key],
|
||||
data={
|
||||
CONF_DEVICE: user_input[CONF_DEVICE],
|
||||
CONF_MODEL: model_key,
|
||||
},
|
||||
)
|
||||
errors["base"] = error
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_MODEL, default=MODEL_MODERN): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(MODEL_NAMES),
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="model",
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_DEVICE): SerialPortSelector(),
|
||||
}
|
||||
),
|
||||
user_input or {},
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Constants for the Marantz RS-232 integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from marantz_rs232 import (
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
DOMAIN = "marantz_rs232"
|
||||
|
||||
type MarantzReceiver = (
|
||||
MarantzV2015Receiver | MarantzV2007Receiver | MarantzV2003Receiver
|
||||
)
|
||||
type MarantzRS232ConfigEntry = ConfigEntry[MarantzReceiver]
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"domain": "marantz_rs232",
|
||||
"name": "Marantz RS-232",
|
||||
"codeowners": ["@balloob"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/marantz_rs232",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["marantz_rs232"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["marantz-rs232==2.0.0"]
|
||||
}
|
||||
@@ -1,567 +0,0 @@
|
||||
"""Media player platform for the Marantz RS-232 integration."""
|
||||
|
||||
import math
|
||||
from typing import cast
|
||||
|
||||
from marantz_rs232 import (
|
||||
V2015_MIN_VOLUME_DB,
|
||||
V2015_VOLUME_DB_RANGE,
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
V2003MainPlayer,
|
||||
V2003MultiRoomPlayer,
|
||||
V2003ReceiverState,
|
||||
V2003Source,
|
||||
V2007MainPlayer,
|
||||
V2007MultiRoomPlayer,
|
||||
V2007ReceiverState,
|
||||
V2007Source,
|
||||
V2015InputSource,
|
||||
V2015MainPlayer,
|
||||
V2015ReceiverState,
|
||||
V2015ZonePlayer,
|
||||
)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .config_flow import MODEL_NAMES
|
||||
from .const import DOMAIN, MarantzRS232ConfigEntry
|
||||
|
||||
V2003_MIN_VOLUME_DB = -90.0
|
||||
V2003_VOLUME_DB_RANGE = 189.0 # -90..+99
|
||||
|
||||
INPUT_SOURCE_V2015_TO_HA: dict[V2015InputSource, str] = {
|
||||
V2015InputSource.PHONO: "phono",
|
||||
V2015InputSource.CD: "cd",
|
||||
V2015InputSource.TUNER: "tuner",
|
||||
V2015InputSource.DVD: "dvd",
|
||||
V2015InputSource.BD: "bd",
|
||||
V2015InputSource.TV: "tv",
|
||||
V2015InputSource.SAT_CBL: "sat_cbl",
|
||||
V2015InputSource.SAT: "sat",
|
||||
V2015InputSource.MPLAY: "mplay",
|
||||
V2015InputSource.VCR: "vcr",
|
||||
V2015InputSource.GAME: "game",
|
||||
V2015InputSource.V_AUX: "v_aux",
|
||||
V2015InputSource.HDRADIO: "hdradio",
|
||||
V2015InputSource.SIRIUS: "sirius",
|
||||
V2015InputSource.SPOTIFY: "spotify",
|
||||
V2015InputSource.SIRIUSXM: "siriusxm",
|
||||
V2015InputSource.RHAPSODY: "rhapsody",
|
||||
V2015InputSource.PANDORA: "pandora",
|
||||
V2015InputSource.NAPSTER: "napster",
|
||||
V2015InputSource.LASTFM: "lastfm",
|
||||
V2015InputSource.FLICKR: "flickr",
|
||||
V2015InputSource.IRADIO: "iradio",
|
||||
V2015InputSource.SERVER: "server",
|
||||
V2015InputSource.FAVORITES: "favorites",
|
||||
V2015InputSource.CDR: "cdr",
|
||||
V2015InputSource.AUX1: "aux1",
|
||||
V2015InputSource.AUX2: "aux2",
|
||||
V2015InputSource.AUX3: "aux3",
|
||||
V2015InputSource.AUX4: "aux4",
|
||||
V2015InputSource.AUX5: "aux5",
|
||||
V2015InputSource.AUX6: "aux6",
|
||||
V2015InputSource.AUX7: "aux7",
|
||||
V2015InputSource.NET: "net",
|
||||
V2015InputSource.NET_USB: "net_usb",
|
||||
V2015InputSource.BT: "bt",
|
||||
V2015InputSource.M_XPORT: "m_xport",
|
||||
V2015InputSource.USB_IPOD: "usb_ipod",
|
||||
}
|
||||
|
||||
INPUT_SOURCE_V2007_TO_HA: dict[V2007Source, str] = {
|
||||
V2007Source.TV: "tv",
|
||||
V2007Source.DVD: "dvd",
|
||||
V2007Source.VCR1: "vcr1",
|
||||
V2007Source.DSS_VCR2: "dss_vcr2",
|
||||
V2007Source.AUX1: "aux1",
|
||||
V2007Source.AUX2: "aux2",
|
||||
V2007Source.CD_CDR: "cd_cdr",
|
||||
V2007Source.TAPE: "tape",
|
||||
V2007Source.TUNER1: "tuner",
|
||||
V2007Source.FM1: "fm",
|
||||
V2007Source.AM1: "am",
|
||||
V2007Source.XM1: "xm",
|
||||
}
|
||||
|
||||
INPUT_SOURCE_V2003_TO_HA: dict[V2003Source, str] = {
|
||||
V2003Source.DSS: "dss",
|
||||
V2003Source.TV: "tv",
|
||||
V2003Source.LD: "ld",
|
||||
V2003Source.DVD: "dvd",
|
||||
V2003Source.VCR1: "vcr1",
|
||||
V2003Source.VCR2_DVDR: "vcr2_dvdr",
|
||||
V2003Source.AUX1: "aux1",
|
||||
V2003Source.AUX2: "aux2",
|
||||
V2003Source.DVDR: "dvdr",
|
||||
V2003Source.CD: "cd",
|
||||
V2003Source.TAPE: "tape",
|
||||
V2003Source.CDR: "cdr",
|
||||
V2003Source.FM: "fm",
|
||||
V2003Source.AM: "am",
|
||||
V2003Source.MW: "mw",
|
||||
V2003Source.LW: "lw",
|
||||
V2003Source.TUNER: "tuner",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MarantzRS232ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Marantz RS-232 media player."""
|
||||
receiver = config_entry.runtime_data
|
||||
|
||||
entities: list[MediaPlayerEntity]
|
||||
if isinstance(receiver, MarantzV2015Receiver):
|
||||
entities = [
|
||||
MarantzV2015MediaPlayer(receiver, receiver.main, config_entry, "main")
|
||||
]
|
||||
if receiver.zone_2.power is not None:
|
||||
entities.append(
|
||||
MarantzV2015MediaPlayer(
|
||||
receiver, receiver.zone_2, config_entry, "zone_2"
|
||||
)
|
||||
)
|
||||
if receiver.zone_3.power is not None:
|
||||
entities.append(
|
||||
MarantzV2015MediaPlayer(
|
||||
receiver, receiver.zone_3, config_entry, "zone_3"
|
||||
)
|
||||
)
|
||||
elif isinstance(receiver, MarantzV2003Receiver):
|
||||
entities = [
|
||||
MarantzV2003MediaPlayer(receiver, receiver.main, config_entry, "main")
|
||||
]
|
||||
if receiver.multi_room.power is not None:
|
||||
entities.append(
|
||||
MarantzV2003MediaPlayer(
|
||||
receiver, receiver.multi_room, config_entry, "multi_room"
|
||||
)
|
||||
)
|
||||
else:
|
||||
entities = [
|
||||
MarantzV2007MediaPlayer(receiver, receiver.main, config_entry, "main")
|
||||
]
|
||||
if receiver.multi_room_a.power is not None:
|
||||
entities.append(
|
||||
MarantzV2007MediaPlayer(
|
||||
receiver, receiver.multi_room_a, config_entry, "multi_room_a"
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MarantzV2015MediaPlayer(MediaPlayerEntity):
|
||||
"""Representation of a modern Marantz receiver controlled over RS-232."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "receiver"
|
||||
_attr_should_poll = False
|
||||
|
||||
_volume_min = V2015_MIN_VOLUME_DB
|
||||
_volume_range = V2015_VOLUME_DB_RANGE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: MarantzV2015Receiver,
|
||||
player: V2015MainPlayer | V2015ZonePlayer,
|
||||
config_entry: MarantzRS232ConfigEntry,
|
||||
zone: str,
|
||||
) -> None:
|
||||
"""Initialize the media player."""
|
||||
self._receiver = receiver
|
||||
self._player = player
|
||||
self._is_main = zone == "main"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Marantz",
|
||||
model_id=MODEL_NAMES.get(config_entry.data["model"]),
|
||||
)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{zone}"
|
||||
|
||||
self._attr_source_list = sorted(INPUT_SOURCE_V2015_TO_HA.values())
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
|
||||
if zone == "main":
|
||||
self._attr_name = None
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
elif zone == "zone_2":
|
||||
self._attr_name = "Zone 2"
|
||||
else:
|
||||
self._attr_name = "Zone 3"
|
||||
|
||||
self._async_update_from_player()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to receiver state updates."""
|
||||
self.async_on_remove(self._receiver.subscribe(self._async_on_state_update))
|
||||
|
||||
@callback
|
||||
def _async_on_state_update(self, state: V2015ReceiverState | None) -> None:
|
||||
if state is None:
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._async_update_from_player()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_from_player(self) -> None:
|
||||
if self._player.power is None:
|
||||
self._attr_state = None
|
||||
else:
|
||||
self._attr_state = (
|
||||
MediaPlayerState.ON if self._player.power else MediaPlayerState.OFF
|
||||
)
|
||||
|
||||
source = self._player.input_source
|
||||
self._attr_source = INPUT_SOURCE_V2015_TO_HA.get(source) if source else None
|
||||
|
||||
volume_min = self._player.volume_min
|
||||
volume_max = self._player.volume_max
|
||||
if volume_min is not None:
|
||||
self._volume_min = volume_min
|
||||
if volume_max is not None and volume_max > volume_min:
|
||||
self._volume_range = volume_max - volume_min
|
||||
|
||||
volume = self._player.volume
|
||||
if volume is not None:
|
||||
self._attr_volume_level = (volume - self._volume_min) / self._volume_range
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
|
||||
if self._is_main:
|
||||
self._attr_is_volume_muted = cast(V2015MainPlayer, self._player).mute
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the receiver on."""
|
||||
await self._player.power_on()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the receiver off."""
|
||||
await self._player.power_off()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
db = volume * self._volume_range + self._volume_min
|
||||
await self._player.set_volume(db)
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up."""
|
||||
await self._player.volume_up()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down."""
|
||||
await self._player.volume_down()
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute."""
|
||||
player = cast(V2015MainPlayer, self._player)
|
||||
if mute:
|
||||
await player.mute_on()
|
||||
else:
|
||||
await player.mute_off()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
input_source = next(
|
||||
(
|
||||
input_source
|
||||
for input_source, ha_source in INPUT_SOURCE_V2015_TO_HA.items()
|
||||
if ha_source == source
|
||||
),
|
||||
None,
|
||||
)
|
||||
if input_source is None:
|
||||
raise HomeAssistantError("Invalid source")
|
||||
|
||||
await self._player.select_source(input_source)
|
||||
|
||||
|
||||
class MarantzV2007MediaPlayer(MediaPlayerEntity):
|
||||
"""Representation of a 2007-era Marantz receiver controlled over RS-232."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "receiver"
|
||||
_attr_should_poll = False
|
||||
|
||||
_volume_min = V2015_MIN_VOLUME_DB
|
||||
_volume_range = V2015_VOLUME_DB_RANGE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: MarantzV2007Receiver,
|
||||
player: V2007MainPlayer | V2007MultiRoomPlayer,
|
||||
config_entry: MarantzRS232ConfigEntry,
|
||||
zone: str,
|
||||
) -> None:
|
||||
"""Initialize the v2007 media player."""
|
||||
self._receiver = receiver
|
||||
self._player = player
|
||||
|
||||
if isinstance(player, V2007MainPlayer):
|
||||
self._set_volume = player.set_volume
|
||||
self._volume_up = player.volume_up
|
||||
self._volume_down = player.volume_down
|
||||
else:
|
||||
self._set_volume = player.set_line_volume
|
||||
self._volume_up = player.line_volume_up
|
||||
self._volume_down = player.line_volume_down
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Marantz",
|
||||
model_id=MODEL_NAMES.get(config_entry.data["model"]),
|
||||
)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{zone}"
|
||||
|
||||
self._attr_source_list = sorted(INPUT_SOURCE_V2007_TO_HA.values())
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
|
||||
if zone == "main":
|
||||
self._attr_name = None
|
||||
else:
|
||||
self._attr_name = "Multi Room"
|
||||
|
||||
self._async_update_from_player()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to receiver state updates."""
|
||||
self.async_on_remove(self._receiver.subscribe(self._async_on_state_update))
|
||||
|
||||
@callback
|
||||
def _async_on_state_update(self, state: V2007ReceiverState | None) -> None:
|
||||
if state is None:
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._async_update_from_player()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_from_player(self) -> None:
|
||||
if self._player.power is None:
|
||||
self._attr_state = None
|
||||
else:
|
||||
self._attr_state = (
|
||||
MediaPlayerState.ON if self._player.power else MediaPlayerState.OFF
|
||||
)
|
||||
|
||||
source = self._player.input_source
|
||||
self._attr_source = INPUT_SOURCE_V2007_TO_HA.get(source) if source else None
|
||||
|
||||
if isinstance(self._player, V2007MainPlayer):
|
||||
volume = self._player.volume
|
||||
else:
|
||||
volume = self._player.line_volume
|
||||
|
||||
if volume is not None:
|
||||
self._attr_volume_level = (volume - self._volume_min) / self._volume_range
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
|
||||
self._attr_is_volume_muted = self._player.mute
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the receiver on."""
|
||||
await self._player.power_on()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the receiver off."""
|
||||
await self._player.power_off()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
db = volume * self._volume_range + self._volume_min
|
||||
await self._set_volume(db)
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up."""
|
||||
await self._volume_up()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down."""
|
||||
await self._volume_down()
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute."""
|
||||
if mute:
|
||||
await self._player.mute_on()
|
||||
else:
|
||||
await self._player.mute_off()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
v2007_source = next(
|
||||
(
|
||||
ls
|
||||
for ls, ha_source in INPUT_SOURCE_V2007_TO_HA.items()
|
||||
if ha_source == source
|
||||
),
|
||||
None,
|
||||
)
|
||||
if v2007_source is None:
|
||||
raise HomeAssistantError("Invalid source")
|
||||
|
||||
await self._player.select_source(v2007_source)
|
||||
|
||||
|
||||
class MarantzV2003MediaPlayer(MediaPlayerEntity):
|
||||
"""Representation of a 2003-era Marantz receiver controlled over RS-232."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "receiver"
|
||||
_attr_should_poll = False
|
||||
|
||||
_volume_min = V2003_MIN_VOLUME_DB
|
||||
_volume_range = V2003_VOLUME_DB_RANGE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: MarantzV2003Receiver,
|
||||
player: V2003MainPlayer | V2003MultiRoomPlayer,
|
||||
config_entry: MarantzRS232ConfigEntry,
|
||||
zone: str,
|
||||
) -> None:
|
||||
"""Initialize the v2003 media player."""
|
||||
self._receiver = receiver
|
||||
self._player = player
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Marantz",
|
||||
model_id=MODEL_NAMES.get(config_entry.data["model"]),
|
||||
)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{zone}"
|
||||
|
||||
self._attr_source_list = sorted(INPUT_SOURCE_V2003_TO_HA.values())
|
||||
if zone == "main":
|
||||
self._attr_name = None
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
else:
|
||||
self._attr_name = "Multi Room"
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
|
||||
self._async_update_from_player()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to receiver state updates."""
|
||||
self.async_on_remove(self._receiver.subscribe(self._async_on_state_update))
|
||||
|
||||
@callback
|
||||
def _async_on_state_update(self, state: V2003ReceiverState | None) -> None:
|
||||
if state is None:
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._async_update_from_player()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_from_player(self) -> None:
|
||||
power = self._player.power
|
||||
if power is None:
|
||||
self._attr_state = None
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.ON if power else MediaPlayerState.OFF
|
||||
|
||||
source = self._player.input_source
|
||||
self._attr_source = (
|
||||
INPUT_SOURCE_V2003_TO_HA.get(source) if source is not None else None
|
||||
)
|
||||
|
||||
volume = self._player.volume
|
||||
if volume is not None and volume != -math.inf:
|
||||
self._attr_volume_level = (volume - self._volume_min) / self._volume_range
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
|
||||
if isinstance(self._player, V2003MainPlayer):
|
||||
self._attr_is_volume_muted = self._player.mute
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the receiver on."""
|
||||
await self._player.power_on()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the receiver off."""
|
||||
await self._player.power_off()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1. Main zone only."""
|
||||
db = round(volume * self._volume_range + self._volume_min)
|
||||
await cast(V2003MainPlayer, self._player).set_volume(db)
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up."""
|
||||
await self._player.volume_up()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down."""
|
||||
await self._player.volume_down()
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute. Main zone only."""
|
||||
player = cast(V2003MainPlayer, self._player)
|
||||
if mute:
|
||||
await player.mute_on()
|
||||
else:
|
||||
await player.mute_off()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
v2003_source = next(
|
||||
(
|
||||
vs
|
||||
for vs, ha_source in INPUT_SOURCE_V2003_TO_HA.items()
|
||||
if ha_source == source
|
||||
),
|
||||
None,
|
||||
)
|
||||
if v2003_source is None:
|
||||
raise HomeAssistantError("Invalid source")
|
||||
|
||||
await self._player.select_source(v2003_source)
|
||||
@@ -1,64 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: "The integration does not create dynamic devices."
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: "The integration does not create devices that can become stale."
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -1,96 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"device": "[%key:common::config_flow::data::port%]",
|
||||
"model": "Receiver model"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Serial port path to connect to",
|
||||
"model": "Determines the protocol used to communicate with the receiver"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"media_player": {
|
||||
"receiver": {
|
||||
"state_attributes": {
|
||||
"source": {
|
||||
"state": {
|
||||
"am": "AM",
|
||||
"aux1": "Aux 1",
|
||||
"aux2": "Aux 2",
|
||||
"aux3": "Aux 3",
|
||||
"aux4": "Aux 4",
|
||||
"aux5": "Aux 5",
|
||||
"aux6": "Aux 6",
|
||||
"aux7": "Aux 7",
|
||||
"bd": "BD Player",
|
||||
"bt": "Bluetooth",
|
||||
"cd": "CD",
|
||||
"cd_cdr": "CD/CDR",
|
||||
"cdr": "CDR",
|
||||
"dss": "DSS",
|
||||
"dss_vcr2": "DSS/VCR 2",
|
||||
"dvd": "DVD",
|
||||
"dvdr": "DVDR",
|
||||
"favorites": "Favorites",
|
||||
"flickr": "Flickr",
|
||||
"fm": "FM",
|
||||
"game": "Game",
|
||||
"hdradio": "HD Radio",
|
||||
"iradio": "Internet Radio",
|
||||
"lastfm": "Last.fm",
|
||||
"ld": "LaserDisc",
|
||||
"lw": "LW",
|
||||
"m_xport": "M-XPort",
|
||||
"mplay": "Media Player",
|
||||
"mw": "MW",
|
||||
"napster": "Napster",
|
||||
"net": "Network",
|
||||
"net_usb": "Network/USB",
|
||||
"pandora": "Pandora",
|
||||
"phono": "Phono",
|
||||
"rhapsody": "Rhapsody",
|
||||
"sat": "Sat",
|
||||
"sat_cbl": "Satellite/Cable",
|
||||
"server": "Server",
|
||||
"sirius": "Sirius",
|
||||
"siriusxm": "SiriusXM",
|
||||
"spotify": "Spotify",
|
||||
"tape": "Tape",
|
||||
"tuner": "Tuner",
|
||||
"tv": "TV Audio",
|
||||
"usb_ipod": "USB/iPod",
|
||||
"v_aux": "V. Aux",
|
||||
"vcr": "VCR",
|
||||
"vcr1": "VCR 1",
|
||||
"vcr2_dvdr": "VCR 2/DVDR",
|
||||
"xm": "XM"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"model": {
|
||||
"options": {
|
||||
"modern": "Modern",
|
||||
"sr7002": "SR7002",
|
||||
"sr8002": "SR8002",
|
||||
"sr8300": "SR8300",
|
||||
"sr9300": "SR9300"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["mitsubishi-comfort==0.3.1"]
|
||||
"requirements": ["mitsubishi-comfort==0.3.2"]
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class MonarchMoneyDataUpdateCoordinator(DataUpdateCoordinator[MonarchData]):
|
||||
async def _async_update_data(self) -> MonarchData:
|
||||
"""Fetch data for all accounts."""
|
||||
|
||||
now = datetime.now()
|
||||
now = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
|
||||
account_data, cashflow_summary = await asyncio.gather(
|
||||
self.client.get_accounts_as_dict_with_id_key(),
|
||||
|
||||
@@ -1124,7 +1124,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]:
|
||||
if user_data.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) >= user_data.get(
|
||||
CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN
|
||||
):
|
||||
errors["advanced_settings"] = "max_below_min_kelvin"
|
||||
errors["other_settings"] = "max_below_min_kelvin"
|
||||
return errors
|
||||
|
||||
|
||||
@@ -1217,7 +1217,7 @@ def validate_text_platform_config(
|
||||
and CONF_MAX in config
|
||||
and config[CONF_MIN] > config[CONF_MAX]
|
||||
):
|
||||
errors["text_advanced_settings"] = "max_below_min"
|
||||
errors["text_other_settings"] = "max_below_min"
|
||||
|
||||
return errors
|
||||
|
||||
@@ -1506,7 +1506,7 @@ PLATFORM_ENTITY_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
selector=SUGGESTED_DISPLAY_PRECISION_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_OPTIONS: PlatformField(
|
||||
selector=OPTIONS_SELECTOR,
|
||||
@@ -1678,13 +1678,13 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
selector=TIMEOUT_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_OFF_DELAY: PlatformField(
|
||||
selector=TIMEOUT_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
},
|
||||
Platform.BUTTON: {
|
||||
@@ -3125,7 +3125,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
default=False,
|
||||
validator=cv.boolean,
|
||||
conditions=({CONF_SCHEMA: "json"},),
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_FLASH_TIME_SHORT: PlatformField(
|
||||
selector=FLASH_TIME_SELECTOR,
|
||||
@@ -3133,7 +3133,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
validator=cv.positive_int,
|
||||
default=2,
|
||||
conditions=({CONF_SCHEMA: "json"},),
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_FLASH_TIME_LONG: PlatformField(
|
||||
selector=FLASH_TIME_SELECTOR,
|
||||
@@ -3141,7 +3141,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
validator=cv.positive_int,
|
||||
default=10,
|
||||
conditions=({CONF_SCHEMA: "json"},),
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_TRANSITION: PlatformField(
|
||||
selector=BOOLEAN_SELECTOR,
|
||||
@@ -3149,21 +3149,21 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
default=False,
|
||||
validator=cv.boolean,
|
||||
conditions=({CONF_SCHEMA: "json"},),
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_MAX_KELVIN: PlatformField(
|
||||
selector=KELVIN_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
default=DEFAULT_MAX_KELVIN,
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_MIN_KELVIN: PlatformField(
|
||||
selector=KELVIN_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
default=DEFAULT_MIN_KELVIN,
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
},
|
||||
Platform.LOCK: {
|
||||
@@ -3372,7 +3372,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
selector=TIMEOUT_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
section="advanced_settings",
|
||||
section="other_settings",
|
||||
),
|
||||
},
|
||||
Platform.SIREN: {
|
||||
@@ -3437,7 +3437,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
required=False,
|
||||
validator=validate(cv.template),
|
||||
error="invalid_template",
|
||||
section="siren_advanced_settings",
|
||||
section="siren_other_settings",
|
||||
),
|
||||
},
|
||||
Platform.SWITCH: {
|
||||
@@ -3516,26 +3516,26 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
selector=TEXT_SIZE_SELECTOR,
|
||||
required=True,
|
||||
default=0,
|
||||
section="text_advanced_settings",
|
||||
section="text_other_settings",
|
||||
),
|
||||
CONF_MAX: PlatformField(
|
||||
selector=TEXT_SIZE_SELECTOR,
|
||||
required=True,
|
||||
default=255,
|
||||
section="text_advanced_settings",
|
||||
section="text_other_settings",
|
||||
),
|
||||
CONF_MODE: PlatformField(
|
||||
selector=TEXT_MODE_SELECTOR,
|
||||
required=True,
|
||||
default=TextSelectorType.TEXT.value,
|
||||
section="text_advanced_settings",
|
||||
section="text_other_settings",
|
||||
),
|
||||
CONF_PATTERN: PlatformField(
|
||||
selector=TEXT_SELECTOR,
|
||||
required=False,
|
||||
validator=validate(cv.is_regex),
|
||||
error="invalid_regular_expression",
|
||||
section="text_advanced_settings",
|
||||
section="text_other_settings",
|
||||
),
|
||||
},
|
||||
Platform.TIME: {
|
||||
@@ -3798,10 +3798,10 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
MQTT_DEVICE_PLATFORM_FIELDS = {
|
||||
CONF_NAME: PlatformField(selector=TEXT_SELECTOR, required=True),
|
||||
CONF_SW_VERSION: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, section="advanced_settings"
|
||||
selector=TEXT_SELECTOR, required=False, section="other_settings"
|
||||
),
|
||||
CONF_HW_VERSION: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, section="advanced_settings"
|
||||
selector=TEXT_SELECTOR, required=False, section="other_settings"
|
||||
),
|
||||
CONF_MODEL: PlatformField(selector=TEXT_SELECTOR, required=False),
|
||||
CONF_MODEL_ID: PlatformField(selector=TEXT_SELECTOR, required=False),
|
||||
@@ -4178,7 +4178,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PROTOCOL: DEFAULT_PROTOCOL,
|
||||
CONF_USERNAME: addon_discovery_config.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: addon_discovery_config.get(CONF_PASSWORD),
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
}
|
||||
except AddonError:
|
||||
# We do not have discovery information yet
|
||||
@@ -4419,7 +4418,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PROTOCOL: DEFAULT_PROTOCOL,
|
||||
CONF_USERNAME: data.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: data.get(CONF_PASSWORD),
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4686,8 +4684,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
if user_input is not None:
|
||||
new_device_data: dict[str, Any] = user_input.copy()
|
||||
_, errors = validate_user_input(user_input, MQTT_DEVICE_PLATFORM_FIELDS)
|
||||
if "advanced_settings" in new_device_data:
|
||||
new_device_data |= new_device_data.pop("advanced_settings")
|
||||
if "other_settings" in new_device_data:
|
||||
new_device_data |= new_device_data.pop("other_settings")
|
||||
if not errors:
|
||||
self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, new_device_data)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
|
||||
@@ -184,17 +184,6 @@
|
||||
},
|
||||
"description": "Enter the MQTT device details:",
|
||||
"sections": {
|
||||
"advanced_settings": {
|
||||
"data": {
|
||||
"hw_version": "Hardware version",
|
||||
"sw_version": "Software version"
|
||||
},
|
||||
"data_description": {
|
||||
"hw_version": "The hardware version of the device. E.g. 'v1.0 rev a'.",
|
||||
"sw_version": "The software version of the device. E.g. '2025.1.0'."
|
||||
},
|
||||
"name": "Advanced device settings"
|
||||
},
|
||||
"mqtt_settings": {
|
||||
"data": {
|
||||
"message_expiry_interval": "Message Expiry Interval",
|
||||
@@ -205,6 +194,17 @@
|
||||
"qos": "The Quality of Service value the device's entities should use."
|
||||
},
|
||||
"name": "MQTT settings"
|
||||
},
|
||||
"other_settings": {
|
||||
"data": {
|
||||
"hw_version": "Hardware version",
|
||||
"sw_version": "Software version"
|
||||
},
|
||||
"data_description": {
|
||||
"hw_version": "The hardware version of the device. E.g. 'v1.0 rev a'.",
|
||||
"sw_version": "The software version of the device. E.g. '2025.1.0'."
|
||||
},
|
||||
"name": "Other device settings"
|
||||
}
|
||||
},
|
||||
"title": "Configure MQTT device details"
|
||||
@@ -286,14 +286,14 @@
|
||||
},
|
||||
"description": "Please configure specific details for {platform} entity \"{entity}\":",
|
||||
"sections": {
|
||||
"advanced_settings": {
|
||||
"other_settings": {
|
||||
"data": {
|
||||
"suggested_display_precision": "Suggested display precision"
|
||||
},
|
||||
"data_description": {
|
||||
"suggested_display_precision": "The number of decimals which should be used in the {platform} entity state after rounding. [Learn more.]({url}#suggested_display_precision)"
|
||||
},
|
||||
"name": "Advanced options"
|
||||
"name": "Other settings"
|
||||
}
|
||||
},
|
||||
"title": "Configure MQTT device \"{mqtt_device}\""
|
||||
@@ -438,29 +438,6 @@
|
||||
},
|
||||
"description": "Please configure MQTT specific details for {platform} entity \"{entity}\":",
|
||||
"sections": {
|
||||
"advanced_settings": {
|
||||
"data": {
|
||||
"expire_after": "Expire after",
|
||||
"flash": "Flash support",
|
||||
"flash_time_long": "Flash time long",
|
||||
"flash_time_short": "Flash time short",
|
||||
"max_kelvin": "Max Kelvin",
|
||||
"min_kelvin": "Min Kelvin",
|
||||
"off_delay": "OFF delay",
|
||||
"transition": "Transition support"
|
||||
},
|
||||
"data_description": {
|
||||
"expire_after": "If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. If not set, the sensor's state never expires. [Learn more.]({url}#expire_after)",
|
||||
"flash": "Enable the flash feature for this light",
|
||||
"flash_time_long": "The duration, in seconds, of a \"long\" flash.",
|
||||
"flash_time_short": "The duration, in seconds, of a \"short\" flash.",
|
||||
"max_kelvin": "The maximum color temperature in Kelvin.",
|
||||
"min_kelvin": "The minimum color temperature in Kelvin.",
|
||||
"off_delay": "For sensors that only send \"on\" state updates (like PIRs), this variable sets a delay in seconds after which the sensor’s state will be updated back to \"off\".",
|
||||
"transition": "Enable the transition feature for this light"
|
||||
},
|
||||
"name": "Advanced settings"
|
||||
},
|
||||
"alarm_control_panel_payload_settings": {
|
||||
"data": {
|
||||
"payload_arm_away": "Payload \"arm away\"",
|
||||
@@ -916,14 +893,37 @@
|
||||
},
|
||||
"name": "Lock payload settings"
|
||||
},
|
||||
"siren_advanced_settings": {
|
||||
"other_settings": {
|
||||
"data": {
|
||||
"expire_after": "Expire after",
|
||||
"flash": "Flash support",
|
||||
"flash_time_long": "Flash time long",
|
||||
"flash_time_short": "Flash time short",
|
||||
"max_kelvin": "Max Kelvin",
|
||||
"min_kelvin": "Min Kelvin",
|
||||
"off_delay": "OFF delay",
|
||||
"transition": "Transition support"
|
||||
},
|
||||
"data_description": {
|
||||
"expire_after": "If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. If not set, the sensor's state never expires. [Learn more.]({url}#expire_after)",
|
||||
"flash": "Enable the flash feature for this light",
|
||||
"flash_time_long": "The duration, in seconds, of a \"long\" flash.",
|
||||
"flash_time_short": "The duration, in seconds, of a \"short\" flash.",
|
||||
"max_kelvin": "The maximum color temperature in Kelvin.",
|
||||
"min_kelvin": "The minimum color temperature in Kelvin.",
|
||||
"off_delay": "For sensors that only send \"on\" state updates (like PIRs), this variable sets a delay in seconds after which the sensor’s state will be updated back to \"off\".",
|
||||
"transition": "Enable the transition feature for this light"
|
||||
},
|
||||
"name": "Other settings"
|
||||
},
|
||||
"siren_other_settings": {
|
||||
"data": {
|
||||
"command_off_template": "Command \"off\" template"
|
||||
},
|
||||
"data_description": {
|
||||
"command_off_template": "The [template]({command_templating_url}) for \"off\" state changes. By default the \"[Command template]({url}#command_template)\" will be used. [Learn more.]({url}#command_off_template)"
|
||||
},
|
||||
"name": "Advanced siren settings"
|
||||
"name": "Other siren settings"
|
||||
},
|
||||
"target_humidity_settings": {
|
||||
"data": {
|
||||
@@ -985,7 +985,7 @@
|
||||
},
|
||||
"name": "Target temperature settings"
|
||||
},
|
||||
"text_advanced_settings": {
|
||||
"text_other_settings": {
|
||||
"data": {
|
||||
"max": "Maximum length",
|
||||
"min": "Minimum length",
|
||||
@@ -998,7 +998,7 @@
|
||||
"mode": "Mode of the text input",
|
||||
"pattern": "A valid regex pattern"
|
||||
},
|
||||
"name": "Advanced text entity settings"
|
||||
"name": "Other text entity settings"
|
||||
},
|
||||
"valve_payload_settings": {
|
||||
"data": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "myneomitis",
|
||||
"name": "MyNeomitis",
|
||||
"codeowners": ["@l-pr"],
|
||||
"codeowners": ["@Epyes"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/myneomitis",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -70,5 +70,8 @@ class MyUplinkDataCoordinator(DataUpdateCoordinator[CoordinatorData]):
|
||||
points[device_id] = point_info
|
||||
|
||||
return CoordinatorData(
|
||||
systems=systems, devices=devices, points=points, time=datetime.now()
|
||||
systems=systems,
|
||||
devices=devices,
|
||||
points=points,
|
||||
time=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ class NextBusDataUpdateCoordinator(
|
||||
# But only if we have a reset time to unthrottle
|
||||
and self.client.rate_limit_reset is not None
|
||||
# Unless we are after the reset time
|
||||
and datetime.now() < self.client.rate_limit_reset
|
||||
and datetime.now() < self.client.rate_limit_reset # pylint: disable=home-assistant-enforce-naive-now
|
||||
):
|
||||
self.logger.debug(
|
||||
"Rate limit threshold reached. Skipping updates for. Routes: %s",
|
||||
|
||||
@@ -58,7 +58,7 @@ class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_EMAIL: user_input[CONF_EMAIL],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
CONF_REFRESH_TOKEN: refresh_token,
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
},
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@ class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data={
|
||||
**user_input,
|
||||
CONF_REFRESH_TOKEN: refresh_token,
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
},
|
||||
unique_id=user_input[CONF_EMAIL],
|
||||
)
|
||||
|
||||
@@ -150,7 +150,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
|
||||
+ REFRESH_TOKEN_EXPIRY_TIME.total_seconds()
|
||||
)
|
||||
try:
|
||||
if datetime.now().timestamp() >= expiry_time:
|
||||
if datetime.now().timestamp() >= expiry_time: # pylint: disable=home-assistant-enforce-naive-now
|
||||
await self.update_refresh_token()
|
||||
else:
|
||||
await self.api.authenticate_refresh(
|
||||
@@ -194,7 +194,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
|
||||
data = {
|
||||
**self.config_entry.data,
|
||||
CONF_REFRESH_TOKEN: refresh_token,
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
|
||||
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now
|
||||
}
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class NOAATidesAndCurrentsSensor(SensorEntity):
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the latest data from NOAA Tides and Currents API."""
|
||||
begin = datetime.now()
|
||||
begin = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
end = begin + DEFAULT_PREDICTION_LENGTH
|
||||
try:
|
||||
df_predictions = self._station.get_data(
|
||||
|
||||
@@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OllamaConfigEntry) -> bo
|
||||
# in the UI, instead of ConfigEntryNotReady which would
|
||||
# just keep retrying.
|
||||
raise ConfigEntryError(err) from err
|
||||
except (TimeoutError, httpx.ConnectError) as err:
|
||||
except (TimeoutError, httpx.ConnectError, ConnectionError) as err:
|
||||
raise ConfigEntryNotReady(err) from err
|
||||
|
||||
entry.runtime_data = client
|
||||
|
||||
@@ -385,7 +385,7 @@ class DailyHistory:
|
||||
|
||||
def add_measurement(self, value, timestamp=None):
|
||||
"""Add a new measurement for a certain day."""
|
||||
day = (timestamp or datetime.now()).date()
|
||||
day = (timestamp or datetime.now()).date() # pylint: disable=home-assistant-enforce-naive-now
|
||||
if not isinstance(value, (int, float)):
|
||||
return
|
||||
if self._days is None:
|
||||
|
||||
@@ -26,6 +26,8 @@ from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
|
||||
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
|
||||
from .helpers import is_granted
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
NO_PERM_VM_LXC_POWER = "no_permission_vm_lxc_power"
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import RabbitAirConfigEntry, RabbitAirDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.FAN]
|
||||
PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: RabbitAirConfigEntry) -> bool:
|
||||
|
||||
@@ -25,6 +25,8 @@ MODELS = {
|
||||
class RabbitAirBaseEntity(CoordinatorEntity[RabbitAirDataUpdateCoordinator]):
|
||||
"""Base class for Rabbit Air entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: RabbitAirDataUpdateCoordinator,
|
||||
@@ -32,7 +34,6 @@ class RabbitAirBaseEntity(CoordinatorEntity[RabbitAirDataUpdateCoordinator]):
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_name = entry.title
|
||||
self._attr_unique_id = entry.unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.data[CONF_MAC])},
|
||||
|
||||
@@ -46,6 +46,7 @@ async def async_setup_entry(
|
||||
class RabbitAirFanEntity(RabbitAirBaseEntity, FanEntity):
|
||||
"""Fan control functions of the Rabbit Air air purifier."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_translation_key = "rabbitair"
|
||||
_attr_supported_features = (
|
||||
FanEntityFeature.PRESET_MODE
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"default": "mdi:air-filter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Support for Rabbit Air sensors."""
|
||||
|
||||
from rabbitair import Quality
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import RabbitAirConfigEntry, RabbitAirDataUpdateCoordinator
|
||||
from .entity import RabbitAirBaseEntity
|
||||
|
||||
|
||||
def _quality_value(quality: Quality | None) -> StateType:
|
||||
"""Return the air quality state."""
|
||||
return None if quality is None else quality.name.lower()
|
||||
|
||||
|
||||
AIR_QUALITY_OPTIONS = [quality.name.lower() for quality in Quality]
|
||||
|
||||
AIR_QUALITY_DESCRIPTION = SensorEntityDescription(
|
||||
key="air_quality",
|
||||
translation_key="air_quality",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=AIR_QUALITY_OPTIONS,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: RabbitAirConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Rabbit Air sensors."""
|
||||
if entry.runtime_data.data.quality is not None:
|
||||
async_add_entities([RabbitAirAirQualitySensor(entry.runtime_data, entry)])
|
||||
|
||||
|
||||
class RabbitAirAirQualitySensor(RabbitAirBaseEntity, SensorEntity):
|
||||
"""Rabbit Air air quality sensor."""
|
||||
|
||||
entity_description = AIR_QUALITY_DESCRIPTION
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: RabbitAirDataUpdateCoordinator,
|
||||
entry: RabbitAirConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator, entry)
|
||||
self._attr_unique_id = f"{entry.unique_id}_{self.entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the air quality state."""
|
||||
return _quality_value(self.coordinator.data.quality)
|
||||
@@ -32,6 +32,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"name": "Air quality",
|
||||
"state": {
|
||||
"high": "[%key:common::state::high%]",
|
||||
"highest": "Highest",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"lowest": "Lowest",
|
||||
"medium": "[%key:common::state::medium%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ class CalendarUpdateCoordinator(RadarrDataUpdateCoordinator[None]):
|
||||
self._events = [
|
||||
e
|
||||
for e in self._events
|
||||
if e.start >= datetime.now().date() - timedelta(days=30)
|
||||
if e.start >= datetime.now().date() - timedelta(days=30) # pylint: disable=home-assistant-enforce-naive-now
|
||||
]
|
||||
_days = (end_date - start_date).days
|
||||
await asyncio.gather(
|
||||
|
||||
@@ -11,15 +11,16 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import websocket_api
|
||||
from .const import DATA_COMPONENT, DOMAIN
|
||||
from .entity import (
|
||||
RadioFrequencyTransmitterEntity,
|
||||
RadioFrequencyTransmitterEntityDescription,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DATA_COMPONENT",
|
||||
"DOMAIN",
|
||||
"ModulationType",
|
||||
"RadioFrequencyTransmitterEntity",
|
||||
@@ -30,9 +31,6 @@ __all__ = [
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_COMPONENT: HassKey[EntityComponent[RadioFrequencyTransmitterEntity]] = HassKey(
|
||||
DOMAIN
|
||||
)
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
||||
@@ -46,6 +44,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
](_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
||||
await component.async_setup(config)
|
||||
|
||||
websocket_api.async_setup(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -2,4 +2,13 @@
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .entity import RadioFrequencyTransmitterEntity
|
||||
|
||||
DOMAIN: Final = "radio_frequency"
|
||||
|
||||
DATA_COMPONENT: HassKey[EntityComponent[RadioFrequencyTransmitterEntity]] = HassKey(
|
||||
DOMAIN
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"domain": "radio_frequency",
|
||||
"name": "Radio Frequency",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/radio_frequency",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "Radio frequency transmitter"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"component_not_loaded": {
|
||||
"message": "Radio Frequency component not loaded"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""The Radio Frequency websocket API."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from rf_protocols import ModulationType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .const import DATA_COMPONENT
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the radio frequency websocket API."""
|
||||
websocket_api.async_register_command(hass, ws_list_transmitters)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "radio_frequency/list"})
|
||||
@callback
|
||||
def ws_list_transmitters(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return the available radio frequency transmitters.
|
||||
|
||||
Each transmitter is described by its entity id, the device and config
|
||||
entry it belongs to (when registered), the frequency ranges it can
|
||||
operate on and the modulation types it supports.
|
||||
"""
|
||||
component = hass.data[DATA_COMPONENT]
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
transmitters: list[dict[str, Any]] = []
|
||||
for entity in component.entities:
|
||||
entry = ent_reg.async_get(entity.entity_id)
|
||||
transmitters.append(
|
||||
{
|
||||
"entity_id": entity.entity_id,
|
||||
"device_id": entry.device_id if entry else None,
|
||||
"config_entry_id": entry.config_entry_id if entry else None,
|
||||
"supported_frequency_ranges": [
|
||||
[low, high] for low, high in entity.supported_frequency_ranges
|
||||
],
|
||||
"supported_modulations": [
|
||||
modulation.value
|
||||
for modulation in ModulationType
|
||||
if entity.supports_modulation(modulation)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
connection.send_result(msg["id"], {"transmitters": transmitters})
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
from homeassistant.components.hassio import HassioNotReadyError, get_os_info
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
PLATFORMS = [Platform.UPDATE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Raspberry Pi config entry."""
|
||||
@@ -29,4 +32,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"rpi_power", context={"source": "onboarding"}
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"after_dependencies": ["hassio"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"config_flow": false,
|
||||
"dependencies": ["hardware"],
|
||||
"dependencies": ["hardware", "homeassistant_hardware"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/raspberry_pi",
|
||||
"integration_type": "hardware",
|
||||
"quality_scale": "legacy"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"entity": {
|
||||
"update": {
|
||||
"rpi_firmware": {
|
||||
"name": "Firmware"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"supervisor_not_ready": {
|
||||
"message": "Supervisor is not ready"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Raspberry Pi firmware update entity."""
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
|
||||
from homeassistant.components.hassio import HassioNotReadyError, get_os_info
|
||||
from homeassistant.components.homeassistant_hardware.update import (
|
||||
RaspberryPiFirmwareUpdateEntity,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
BOARDS_WITH_RASPBERRYPI_FIRMWARE,
|
||||
async_get_raspberry_pi_firmware_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .hardware import BOARD_NAMES, MODELS
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Raspberry Pi firmware update entity."""
|
||||
try:
|
||||
os_info = get_os_info(hass)
|
||||
except HassioNotReadyError as err:
|
||||
raise PlatformNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="supervisor_not_ready",
|
||||
) from err
|
||||
|
||||
board = os_info.get("board")
|
||||
|
||||
# Only RPi 4/5 expose the bootloader EEPROM. The Yellow's CM4/CM5 is handled
|
||||
# by the homeassistant_yellow integration on its own device.
|
||||
if board is None or board not in BOARDS_WITH_RASPBERRYPI_FIRMWARE:
|
||||
return
|
||||
|
||||
try:
|
||||
firmware = await async_get_raspberry_pi_firmware_info(hass)
|
||||
except SupervisorError as err:
|
||||
raise PlatformNotReady(
|
||||
f"Error fetching Raspberry Pi firmware info: {err}"
|
||||
) from err
|
||||
|
||||
# Skip when the update is blocked on this boot device (e.g. CM4 without
|
||||
# flashrom updates enabled). The blocked state is surfaced as a repair.
|
||||
if firmware is None or firmware.update_blocked:
|
||||
return
|
||||
|
||||
device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, board)},
|
||||
manufacturer="Raspberry Pi",
|
||||
model=MODELS.get(board),
|
||||
name=BOARD_NAMES.get(board, "Raspberry Pi"),
|
||||
)
|
||||
async_add_entities(
|
||||
[
|
||||
RaspberryPiFirmwareUpdateEntity(
|
||||
firmware, device_info, unique_id=f"{board}_rpi_firmware", board=board
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -19,7 +19,7 @@ async def async_get_config_entry_diagnostics(
|
||||
payload: dict[str, Any] = {
|
||||
"now": dt_util.now().isoformat(),
|
||||
"timezone": str(dt_util.get_default_time_zone()),
|
||||
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo),
|
||||
"system_timezone": str(datetime.datetime.now().astimezone().tzinfo), # pylint: disable=home-assistant-enforce-naive-now
|
||||
}
|
||||
payload["ics"] = "\n".join(redact_ics(coordinator.ics))
|
||||
return payload
|
||||
|
||||
@@ -31,7 +31,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
time_to_send = time
|
||||
if time is None:
|
||||
time_to_send = datetime.now()
|
||||
time_to_send = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
|
||||
await local_data.system.set_time(time_to_send)
|
||||
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
"""The Raspberry Pi Power Supply Checker integration."""
|
||||
|
||||
from rpi_bad_power import UnderVoltage, new_under_voltage
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR]
|
||||
|
||||
type RpiPowerConfigEntry = ConfigEntry[UnderVoltage]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: RpiPowerConfigEntry) -> bool:
|
||||
"""Set up Raspberry Pi Power Supply Checker from a config entry."""
|
||||
|
||||
if (client := await hass.async_add_executor_job(new_under_voltage)) is None:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="under_voltage_not_supported",
|
||||
)
|
||||
|
||||
entry.runtime_data = client
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: RpiPowerConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -5,17 +5,20 @@ Minimal Kernel needed is 4.14+
|
||||
|
||||
import logging
|
||||
|
||||
from rpi_bad_power import UnderVoltage, new_under_voltage
|
||||
from rpi_bad_power import UnderVoltage
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import RpiPowerConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DESCRIPTION_NORMALIZED = "Voltage normalized. Everything is working as intended."
|
||||
@@ -27,11 +30,11 @@ DESCRIPTION_UNDER_VOLTAGE = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: RpiPowerConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up rpi_power binary sensor."""
|
||||
under_voltage = await hass.async_add_executor_job(new_under_voltage)
|
||||
under_voltage = config_entry.runtime_data
|
||||
async_add_entities([RaspberryChargerBinarySensor(under_voltage)], True)
|
||||
|
||||
|
||||
@@ -40,14 +43,20 @@ class RaspberryChargerBinarySensor(BinarySensorEntity):
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.PROBLEM
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_icon = "mdi:raspberry-pi"
|
||||
_attr_name = "RPi Power status"
|
||||
_attr_translation_key = "rpi_power"
|
||||
_attr_has_entity_name = True
|
||||
_attr_unique_id = "rpi_power" # only one sensor possible
|
||||
|
||||
def __init__(self, under_voltage: UnderVoltage) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
self._under_voltage = under_voltage
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
manufacturer="Raspberry Pi",
|
||||
identifiers={(DOMAIN, "rpi_power")},
|
||||
name="Raspberry Pi",
|
||||
)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the state."""
|
||||
value = self._under_voltage.get()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"rpi_power": {
|
||||
"default": "mdi:raspberry-pi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"rpi_power": {
|
||||
"name": "Power status"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"under_voltage_not_supported": {
|
||||
"message": "Under-voltage monitoring is not supported on this device."
|
||||
}
|
||||
},
|
||||
"title": "Raspberry Pi Power Supply Checker"
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class SensoterraConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
api = CustomerApi(user_input[CONF_EMAIL], user_input[CONF_PASSWORD])
|
||||
# We need a unique tag per HA instance
|
||||
uuid = self.hass.data["core.uuid"]
|
||||
expiration = datetime.now() + timedelta(TOKEN_EXPIRATION_DAYS)
|
||||
expiration = datetime.now() + timedelta(TOKEN_EXPIRATION_DAYS) # pylint: disable=home-assistant-enforce-naive-now
|
||||
|
||||
try:
|
||||
token: str = await api.get_token(
|
||||
|
||||
@@ -70,7 +70,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator[bool]):
|
||||
try:
|
||||
if (
|
||||
self.ayla_api.token_expiring_soon
|
||||
or datetime.now()
|
||||
or datetime.now() # pylint: disable=home-assistant-enforce-naive-now
|
||||
> self.ayla_api.auth_expiration - timedelta(seconds=600)
|
||||
):
|
||||
await self.ayla_api.async_refresh_auth()
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util.ssl import create_client_context
|
||||
|
||||
@@ -75,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmtpConfigEntry) -> bool
|
||||
try:
|
||||
await hass.async_add_executor_job(lambda: client.connect().quit())
|
||||
except SMTPAuthenticationError as e:
|
||||
raise ConfigEntryError(
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_error",
|
||||
) from e
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Config flow for the SMTP integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from smtplib import SMTP, SMTP_SSL, SMTPAuthenticationError
|
||||
import socket
|
||||
@@ -95,6 +96,22 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_USERNAME): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.TEXT,
|
||||
autocomplete="username",
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -201,6 +218,39 @@ class MailConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm reauthentication dialog."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
entry = self._get_reauth_entry()
|
||||
|
||||
if user_input is not None:
|
||||
errors = await self.hass.async_add_executor_job(
|
||||
validate_input, {**entry.data, **user_input}
|
||||
)
|
||||
if not errors:
|
||||
return self.async_update_and_abort(
|
||||
entry,
|
||||
data_updates=user_input,
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||
suggested_values=user_input
|
||||
or {CONF_USERNAME: entry.data.get(CONF_USERNAME)},
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import config from yaml."""
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Mail (SMTP) notification service."""
|
||||
|
||||
from contextlib import suppress
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
import email.utils
|
||||
@@ -41,7 +42,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -208,7 +209,7 @@ class MailNotifyEntity(NotifyEntity):
|
||||
try:
|
||||
client = self._client.connect()
|
||||
except SMTPAuthenticationError as e:
|
||||
raise HomeAssistantError(
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_error",
|
||||
) from e
|
||||
@@ -236,7 +237,8 @@ class MailNotifyEntity(NotifyEntity):
|
||||
translation_key="send_mail_connection_error",
|
||||
) from e
|
||||
finally:
|
||||
client.quit()
|
||||
with suppress(SMTPException):
|
||||
client.quit()
|
||||
|
||||
|
||||
class MailNotificationService(SmtpClient, BaseNotificationService):
|
||||
@@ -316,10 +318,13 @@ class MailNotificationService(SmtpClient, BaseNotificationService):
|
||||
_LOGGER.warning(
|
||||
"SMTPServerDisconnected sending mail: retrying connection"
|
||||
)
|
||||
mail.quit()
|
||||
with suppress(SMTPException):
|
||||
mail.quit()
|
||||
mail = self.connect()
|
||||
except SMTPException:
|
||||
_LOGGER.warning("SMTPException sending mail: retrying connection")
|
||||
mail.quit()
|
||||
with suppress(SMTPException):
|
||||
mail.quit()
|
||||
mail = self.connect()
|
||||
mail.quit()
|
||||
with suppress(SMTPException):
|
||||
mail.quit()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
@@ -11,6 +12,17 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::smtp::config::step::user::data_description::password%]",
|
||||
"username": "[%key:component::smtp::config::step::user::data_description::username%]"
|
||||
},
|
||||
"title": "Re-authenticate SMTP"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"encryption": "[%key:component::smtp::config::step::user::data::encryption%]",
|
||||
|
||||
@@ -160,7 +160,7 @@ async def async_setup_entry(
|
||||
model=model,
|
||||
manufacturer=manufacturer,
|
||||
model_id=model_id,
|
||||
hw_version=player.firmware,
|
||||
hw_version=str(player.firmware) if player.firmware is not None else None,
|
||||
sw_version=sw_version,
|
||||
via_device=(DOMAIN, coordinator.server_uuid),
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ class StarlineAccount:
|
||||
|
||||
def _check_slnet_token(self, interval: int) -> None:
|
||||
"""Check SLNet token expiration and update if needed."""
|
||||
now = datetime.now().timestamp()
|
||||
now = datetime.now().timestamp() # pylint: disable=home-assistant-enforce-naive-now
|
||||
slnet_token_expires = self._config_entry.data[DATA_EXPIRES]
|
||||
|
||||
if now + interval > slnet_token_expires:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user