Compare commits

...

53 Commits

Author SHA1 Message Date
tronikos
65123609ea Bump opower to 0.16.4 (#161153) 2026-01-18 10:12:43 +01:00
Manu
847adcf977 Add tests for media player actions in Xbox integration (#161156) 2026-01-18 10:09:05 +01:00
Daniel Hjelseth Høyer
f0dc66cb53 Update Tibber library 0.35.0 (#161139) 2026-01-18 06:43:56 +01:00
Ivan Lopez Hernandez
54275a0ee4 Update Gemini SDK Version (#161137) 2026-01-17 15:21:22 -05:00
Manu
964f36bc50 Assume muted state in Xbox integration (#161118) 2026-01-17 21:05:53 +01:00
Erwin Douna
e83cbc3fc5 Proxmox set integration type (#161141) 2026-01-17 20:59:40 +01:00
Tero Paloheimo
e26d90d82b Bump xiaomi-ble to 1.4.3 (#161132) 2026-01-17 18:33:29 +01:00
Artur Pragacz
da52482365 Add labs to core files (#161126) 2026-01-17 17:01:59 +01:00
Maciej Bieniek
6ba16ee9e9 Create cable unplugged entity only for Shelly Flood Gen4 (#161053) 2026-01-17 16:58:23 +01:00
Glenn de Haan
fa29d8180f Improve quality scale to silver HDFury integration (#161077) 2026-01-17 16:57:25 +01:00
Zach Deibert
5d43efb22d Add support for Minecraft Server Java Edition 1.4 - 1.6 (#161035)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-17 16:14:17 +01:00
mettolen
3539c4bcec Update Saunum integration to platinum quality (#160824)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-17 15:44:45 +01:00
Allan Lewis
3e3ec4616c Update list of supported locations for London Air (#160884) 2026-01-17 15:44:34 +01:00
Steve Easley
907861effd Bump pyjvcprojector to 2.0.0 (#160739)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-17 15:44:29 +01:00
Zach Deibert
862a2bc95c Update mcstatus to 12.1.0 (#161124) 2026-01-17 15:41:47 +01:00
DeerMaximum
60f498c1fa Use configuration constants in NINA tests (#161119) 2026-01-17 14:22:32 +01:00
mettolen
bb3617ac08 Add switch entitles to Airobot integration (#161090) 2026-01-17 13:17:22 +01:00
Niracler
48d1bd13fa Add sensor platform support to sunricher_dali integration (#159579) 2026-01-17 13:16:43 +01:00
Josef Zweck
8555bc9da0 Add reauthentication to openai_conversation (#161044)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-17 13:13:58 +01:00
epenet
9260394883 Mark preset_mode type hints as compulsory in climate/fan platforms (#161043) 2026-01-17 13:09:18 +01:00
DeerMaximum
8503637a80 Patch the NINA library instead of the HTTP requests (#161074) 2026-01-17 13:00:40 +01:00
Erwin Douna
c993cd9bee Add Config Flow for ProxmoxVE (#142432)
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-17 12:59:58 +01:00
epenet
171013c0d0 Improve type hints in nx584 (#161065) 2026-01-17 12:56:31 +01:00
Evan Graham
c8a7aa359e Add gpt-4.1-mini and gpt-4.1-nano to unsupported extended cache retention list (#161097) 2026-01-17 12:16:25 +01:00
Ludovic BOUÉ
88d8951657 Adjust battery voltage sensor display precision for Matter devices (#161088)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-17 11:55:44 +01:00
epenet
b66ab3cf92 Improve type hints in ness_alarm (#161064) 2026-01-17 11:43:14 +01:00
epenet
253b32abd6 Use HassKey in qwikswitch (#161066) 2026-01-17 00:33:30 +01:00
Ludovic BOUÉ
cc20072c86 Fix Matter Window covering config status entity name (#160960) 2026-01-17 00:25:48 +01:00
Bram Kragten
f86db56d48 Update frontend to 20260107.2 (#161061) 2026-01-16 16:44:08 +01:00
Manu
3e2ebb8ebb Fix entity description in Mastodon (#161068) 2026-01-16 16:38:00 +01:00
Artur Pragacz
6e7b206788 Add update preview feature to labs (#160989) 2026-01-16 15:18:06 +01:00
Manu
cee007b0b0 Add binary sensor platform to Mastodon (#161056) 2026-01-16 14:31:42 +01:00
Erwin Douna
bd24c27bc9 SMA add selector strings/translation (#161060) 2026-01-16 13:56:15 +01:00
Andrew Jackson
49bd26da86 Bump aiomealie to 1.2.0 (#161058) 2026-01-16 13:37:22 +01:00
AlCalzone
49c42b9ad0 Clean up unnecessary Z-Wave "device config changed" repairs (#161000) 2026-01-16 12:51:42 +01:00
Josef Zweck
411491dc45 Type OpenAI config entry consistently (#161052) 2026-01-16 11:19:51 +01:00
Erik Montnemery
47383a499e Remove useless @pytest.mark.asyncio decorators from tests (#161050) 2026-01-16 10:19:23 +01:00
Erwin Douna
f9aa307cb2 SMA add reconfigure flow (#160743)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 10:16:34 +01:00
epenet
7c6a31861e Improve type hints in egardia (#161048) 2026-01-16 10:08:24 +01:00
Robert Resch
b2b25ca28c Revert "Add SmartThings media-player audio notifications" (#161049) 2026-01-16 10:06:30 +01:00
epenet
ad9efab16a Improve type hints in concord232 (#161045) 2026-01-16 09:46:53 +01:00
Matthias Alphart
e967d33911 Update knx-frontend to 2026.1.15.112308 (#161004) 2026-01-16 09:37:09 +01:00
epenet
86bacdbdde Use shorthand attributes in oasa_telematics (#160990) 2026-01-16 09:34:51 +01:00
Robert Resch
644a40674d Make shebang matcher stricter (#160986) 2026-01-16 09:21:19 +01:00
Raphael Hehl
2cf813758e Add per-camera ring volume control for UniFi Protect chimes (#161031)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-01-16 08:29:35 +01:00
DeerMaximum
ad47eccf5f Bump pynina to 1.0.2 (#161013) 2026-01-16 08:24:58 +01:00
epenet
581b554a66 Improve type hints in digital_ocean (#161006) 2026-01-16 08:23:13 +01:00
epenet
e4def9eb03 Improve type hints in envisalink (#161005) 2026-01-16 08:22:15 +01:00
epenet
5f2d17faf6 Improve type hints in homematic (#161002) 2026-01-16 08:21:30 +01:00
TheJulianJES
e17565c069 Add Resideo X2S Smart Thermostat diagnostics to Matter fixture (#161037) 2026-01-16 08:20:42 +01:00
Erik Montnemery
b856e04825 Add assist_satellite conditions (#161019) 2026-01-16 07:39:59 +01:00
epenet
67e676df4f Fix duplicate HVACMode in Tuya climate (#160918) 2026-01-15 22:12:24 +01:00
Erik Montnemery
e2e7485e30 Remove unused test fixture from light condition tests (#160925) 2026-01-15 22:03:18 +01:00
220 changed files with 17455 additions and 14458 deletions

View File

@@ -91,6 +91,7 @@ components: &components
- homeassistant/components/input_number/**
- homeassistant/components/input_select/**
- homeassistant/components/input_text/**
- homeassistant/components/labs/**
- homeassistant/components/logbook/**
- homeassistant/components/logger/**
- homeassistant/components/lovelace/**

View File

@@ -247,17 +247,11 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- *checkout
- name: Register yamllint problem matcher
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/yamllint.json"
- name: Register check-json problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/check-json.json"
- name: Register check executables problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
- name: Register codespell problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@9d6a3097e0c1865ecce00cfb89fe80f2ee91b547 # v1.0.12

View File

@@ -4,7 +4,7 @@
"owner": "check-executables-have-shebangs",
"pattern": [
{
"regexp": "^(.+):\\s(.+)$",
"regexp": "^(.+):\\s(marked executable but has no \\(or invalid\\) shebang!.*)$",
"file": 1,
"message": 2
}

View File

@@ -455,6 +455,7 @@ homeassistant.components.russound_rio.*
homeassistant.components.ruuvi_gateway.*
homeassistant.components.ruuvitag_ble.*
homeassistant.components.samsungtv.*
homeassistant.components.saunum.*
homeassistant.components.scene.*
homeassistant.components.schedule.*
homeassistant.components.schlage.*

7
CODEOWNERS generated
View File

@@ -1017,8 +1017,8 @@ build.json @home-assistant/supervisor
/tests/components/mill/ @danielhiversen
/homeassistant/components/min_max/ @gjohansson-ST
/tests/components/min_max/ @gjohansson-ST
/homeassistant/components/minecraft_server/ @elmurato
/tests/components/minecraft_server/ @elmurato
/homeassistant/components/minecraft_server/ @elmurato @zachdeibert
/tests/components/minecraft_server/ @elmurato @zachdeibert
/homeassistant/components/minio/ @tkislan
/tests/components/minio/ @tkislan
/homeassistant/components/moat/ @bdraco
@@ -1273,7 +1273,8 @@ build.json @home-assistant/supervisor
/tests/components/prosegur/ @dgomes
/homeassistant/components/proximity/ @mib1185
/tests/components/proximity/ @mib1185
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
/tests/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
/homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45
/homeassistant/components/pterodactyl/ @elmurato

View File

@@ -12,6 +12,7 @@ PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@@ -63,6 +63,11 @@ class AirobotClimate(AirobotEntity, ClimateEntity):
_attr_min_temp = SETPOINT_TEMP_MIN
_attr_max_temp = SETPOINT_TEMP_MAX
def __init__(self, coordinator) -> None:
"""Initialize the climate entity."""
super().__init__(coordinator)
self._attr_unique_id = coordinator.data.status.device_id
@property
def _status(self) -> ThermostatStatus:
"""Get status from coordinator data."""

View File

@@ -24,8 +24,6 @@ class AirobotEntity(CoordinatorEntity[AirobotDataUpdateCoordinator]):
status = coordinator.data.status
settings = coordinator.data.settings
self._attr_unique_id = status.device_id
connections = set()
if (mac := coordinator.config_entry.data.get(CONF_MAC)) is not None:
connections.add((CONNECTION_NETWORK_MAC, mac))

View File

@@ -9,6 +9,14 @@
"hysteresis_band": {
"default": "mdi:delta"
}
},
"switch": {
"actuator_exercise_disabled": {
"default": "mdi:valve"
},
"child_lock": {
"default": "mdi:lock"
}
}
}
}

View File

@@ -85,6 +85,14 @@
"heating_uptime": {
"name": "Heating uptime"
}
},
"switch": {
"actuator_exercise_disabled": {
"name": "Actuator exercise disabled"
},
"child_lock": {
"name": "Child lock"
}
}
},
"exceptions": {
@@ -105,6 +113,12 @@
},
"set_value_failed": {
"message": "Failed to set value: {error}"
},
"switch_turn_off_failed": {
"message": "Failed to turn off {switch}."
},
"switch_turn_on_failed": {
"message": "Failed to turn on {switch}."
}
}
}

View File

@@ -0,0 +1,118 @@
"""Switch platform for Airobot thermostat."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from pyairobotrest.exceptions import AirobotError
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AirobotConfigEntry
from .const import DOMAIN
from .coordinator import AirobotDataUpdateCoordinator
from .entity import AirobotEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirobotSwitchEntityDescription(SwitchEntityDescription):
"""Describes Airobot switch entity."""
is_on_fn: Callable[[AirobotDataUpdateCoordinator], bool]
turn_on_fn: Callable[[AirobotDataUpdateCoordinator], Coroutine[Any, Any, None]]
turn_off_fn: Callable[[AirobotDataUpdateCoordinator], Coroutine[Any, Any, None]]
SWITCH_TYPES: tuple[AirobotSwitchEntityDescription, ...] = (
AirobotSwitchEntityDescription(
key="child_lock",
translation_key="child_lock",
entity_category=EntityCategory.CONFIG,
is_on_fn=lambda coordinator: (
coordinator.data.settings.setting_flags.childlock_enabled
),
turn_on_fn=lambda coordinator: coordinator.client.set_child_lock(True),
turn_off_fn=lambda coordinator: coordinator.client.set_child_lock(False),
),
AirobotSwitchEntityDescription(
key="actuator_exercise_disabled",
translation_key="actuator_exercise_disabled",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
is_on_fn=lambda coordinator: (
coordinator.data.settings.setting_flags.actuator_exercise_disabled
),
turn_on_fn=lambda coordinator: coordinator.client.toggle_actuator_exercise(
True
),
turn_off_fn=lambda coordinator: coordinator.client.toggle_actuator_exercise(
False
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot switch entities."""
coordinator = entry.runtime_data
async_add_entities(
AirobotSwitch(coordinator, description) for description in SWITCH_TYPES
)
class AirobotSwitch(AirobotEntity, SwitchEntity):
"""Representation of an Airobot switch."""
entity_description: AirobotSwitchEntityDescription
def __init__(
self,
coordinator: AirobotDataUpdateCoordinator,
description: AirobotSwitchEntityDescription,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
@property
def is_on(self) -> bool:
"""Return true if the switch is on."""
return self.entity_description.is_on_fn(self.coordinator)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
try:
await self.entity_description.turn_on_fn(self.coordinator)
except AirobotError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="switch_turn_on_failed",
translation_placeholders={"switch": self.entity_description.key},
) from err
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
try:
await self.entity_description.turn_off_fn(self.coordinator)
except AirobotError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="switch_turn_off_failed",
translation_placeholders={"switch": self.entity_description.key},
) from err
await self.coordinator.async_request_refresh()

View File

@@ -0,0 +1,23 @@
"""Provides conditions for assist satellites."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from .const import DOMAIN
from .entity import AssistSatelliteState
CONDITIONS: dict[str, type[Condition]] = {
"is_idle": make_entity_state_condition(DOMAIN, AssistSatelliteState.IDLE),
"is_listening": make_entity_state_condition(DOMAIN, AssistSatelliteState.LISTENING),
"is_processing": make_entity_state_condition(
DOMAIN, AssistSatelliteState.PROCESSING
),
"is_responding": make_entity_state_condition(
DOMAIN, AssistSatelliteState.RESPONDING
),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the assist satellite conditions."""
return CONDITIONS

View File

@@ -0,0 +1,19 @@
.condition_common: &condition_common
target:
entity:
domain: assist_satellite
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_idle: *condition_common
is_listening: *condition_common
is_processing: *condition_common
is_responding: *condition_common

View File

@@ -1,4 +1,18 @@
{
"conditions": {
"is_idle": {
"condition": "mdi:chat-sleep"
},
"is_listening": {
"condition": "mdi:chat-question"
},
"is_processing": {
"condition": "mdi:chat-processing"
},
"is_responding": {
"condition": "mdi:chat-alert"
}
},
"entity_component": {
"_": {
"default": "mdi:account-voice"

View File

@@ -1,8 +1,52 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted Assist satellites.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted Assist satellites to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_idle": {
"description": "Tests if one or more Assist satellites are idle.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "If a satellite is idle"
},
"is_listening": {
"description": "Tests if one or more Assist satellites are listening.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "If a satellite is listening"
},
"is_processing": {
"description": "Tests if one or more Assist satellites are processing.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "If a satellite is processing"
},
"is_responding": {
"description": "Tests if one or more Assist satellites are responding.",
"fields": {
"behavior": {
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "If a satellite is responding"
}
},
"entity_component": {
"_": {
"name": "Assist satellite",
@@ -21,6 +65,12 @@
"sentences": "Sentences"
}
},
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -124,6 +124,7 @@ NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
_EXPERIMENTAL_CONDITION_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"fan",
"light",
}

View File

@@ -49,11 +49,11 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Concord232 alarm control panel platform."""
name = config[CONF_NAME]
code = config.get(CONF_CODE)
mode = config[CONF_MODE]
host = config[CONF_HOST]
port = config[CONF_PORT]
name: str = config[CONF_NAME]
code: str | None = config.get(CONF_CODE)
mode: str = config[CONF_MODE]
host: str = config[CONF_HOST]
port: int = config[CONF_PORT]
url = f"http://{host}:{port}"
@@ -72,7 +72,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY
)
def __init__(self, url, name, code, mode):
def __init__(self, url: str, name: str, code: str | None, mode: str) -> None:
"""Initialize the Concord232 alarm panel."""
self._attr_name = name
@@ -125,7 +125,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
return
self._alarm.arm("away")
def _validate_code(self, code, state):
def _validate_code(self, code: str | None, state: AlarmControlPanelState) -> bool:
"""Validate given code."""
if self._code is None:
return True

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import datetime
import logging
from typing import Any
from concord232 import client as concord232_client
import requests
@@ -29,8 +30,7 @@ CONF_ZONE_TYPES = "zone_types"
DEFAULT_HOST = "localhost"
DEFAULT_NAME = "Alarm"
DEFAULT_PORT = "5007"
DEFAULT_SSL = False
DEFAULT_PORT = 5007
SCAN_INTERVAL = datetime.timedelta(seconds=10)
@@ -56,10 +56,10 @@ def setup_platform(
) -> None:
"""Set up the Concord232 binary sensor platform."""
host = config[CONF_HOST]
port = config[CONF_PORT]
exclude = config[CONF_EXCLUDE_ZONES]
zone_types = config[CONF_ZONE_TYPES]
host: str = config[CONF_HOST]
port: int = config[CONF_PORT]
exclude: list[int] = config[CONF_EXCLUDE_ZONES]
zone_types: dict[int, BinarySensorDeviceClass] = config[CONF_ZONE_TYPES]
sensors = []
try:
@@ -84,7 +84,6 @@ def setup_platform(
if zone["number"] not in exclude:
sensors.append(
Concord232ZoneSensor(
hass,
client,
zone,
zone_types.get(zone["number"], get_opening_type(zone)),
@@ -110,26 +109,25 @@ def get_opening_type(zone):
class Concord232ZoneSensor(BinarySensorEntity):
"""Representation of a Concord232 zone as a sensor."""
def __init__(self, hass, client, zone, zone_type):
def __init__(
self,
client: concord232_client.Client,
zone: dict[str, Any],
zone_type: BinarySensorDeviceClass,
) -> None:
"""Initialize the Concord232 binary sensor."""
self._hass = hass
self._client = client
self._zone = zone
self._number = zone["number"]
self._zone_type = zone_type
self._attr_device_class = zone_type
@property
def device_class(self) -> BinarySensorDeviceClass:
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
@property
def name(self):
def name(self) -> str:
"""Return the name of the binary sensor."""
return self._zone["name"]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
# True means "faulted" or "open" or "abnormal state"
return bool(self._zone["state"] != "Normal")
@@ -145,5 +143,5 @@ class Concord232ZoneSensor(BinarySensorEntity):
if hasattr(self._client, "zones"):
self._zone = next(
(x for x in self._client.zones if x["number"] == self._number), None
x for x in self._client.zones if x["number"] == self._number
)

View File

@@ -1,6 +1,7 @@
"""Support for Digital Ocean."""
from datetime import timedelta
from __future__ import annotations
import logging
import digitalocean
@@ -12,27 +13,12 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import Throttle
from .const import DATA_DIGITAL_OCEAN, DOMAIN, MIN_TIME_BETWEEN_UPDATES
_LOGGER = logging.getLogger(__name__)
ATTR_CREATED_AT = "created_at"
ATTR_DROPLET_ID = "droplet_id"
ATTR_DROPLET_NAME = "droplet_name"
ATTR_FEATURES = "features"
ATTR_IPV4_ADDRESS = "ipv4_address"
ATTR_IPV6_ADDRESS = "ipv6_address"
ATTR_MEMORY = "memory"
ATTR_REGION = "region"
ATTR_VCPUS = "vcpus"
ATTRIBUTION = "Data provided by Digital Ocean"
CONF_DROPLETS = "droplets"
DATA_DIGITAL_OCEAN = "data_do"
DIGITAL_OCEAN_PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR]
DOMAIN = "digital_ocean"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
@@ -16,7 +17,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
from .const import (
ATTR_CREATED_AT,
ATTR_DROPLET_ID,
ATTR_DROPLET_NAME,
@@ -65,6 +66,7 @@ class DigitalOceanBinarySensor(BinarySensorEntity):
"""Representation of a Digital Ocean droplet sensor."""
_attr_attribution = ATTRIBUTION
_attr_device_class = BinarySensorDeviceClass.MOVING
def __init__(self, do, droplet_id):
"""Initialize a new Digital Ocean sensor."""
@@ -79,17 +81,12 @@ class DigitalOceanBinarySensor(BinarySensorEntity):
return self.data.name
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self.data.status == "active"
@property
def device_class(self) -> BinarySensorDeviceClass:
"""Return the class of this sensor."""
return BinarySensorDeviceClass.MOVING
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the Digital Ocean droplet."""
return {
ATTR_CREATED_AT: self.data.created_at,

View File

@@ -0,0 +1,30 @@
"""Support for Digital Ocean."""
from __future__ import annotations
from datetime import timedelta
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import DigitalOcean
ATTR_CREATED_AT = "created_at"
ATTR_DROPLET_ID = "droplet_id"
ATTR_DROPLET_NAME = "droplet_name"
ATTR_FEATURES = "features"
ATTR_IPV4_ADDRESS = "ipv4_address"
ATTR_IPV6_ADDRESS = "ipv6_address"
ATTR_MEMORY = "memory"
ATTR_REGION = "region"
ATTR_VCPUS = "vcpus"
ATTRIBUTION = "Data provided by Digital Ocean"
CONF_DROPLETS = "droplets"
DOMAIN = "digital_ocean"
DATA_DIGITAL_OCEAN: HassKey[DigitalOcean] = HassKey(DOMAIN)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
from .const import (
ATTR_CREATED_AT,
ATTR_DROPLET_ID,
ATTR_DROPLET_NAME,
@@ -80,12 +80,12 @@ class DigitalOceanSwitch(SwitchEntity):
return self.data.name
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if switch is on."""
return self.data.status == "active"
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the Digital Ocean droplet."""
return {
ATTR_CREATED_AT: self.data.created_at,

View File

@@ -18,6 +18,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
@@ -35,7 +36,7 @@ DEFAULT_REPORT_SERVER_PORT = 52010
DEFAULT_VERSION = "GATE-01"
DOMAIN = "egardia"
EGARDIA_DEVICE = "egardiadevice"
EGARDIA_DEVICE: HassKey[egardiadevice.EgardiaDevice] = HassKey(DOMAIN)
EGARDIA_NAME = "egardianame"
EGARDIA_REPORT_SERVER_CODES = "egardia_rs_codes"
EGARDIA_REPORT_SERVER_ENABLED = "egardia_rs_enabled"

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import logging
from pythonegardia.egardiadevice import EgardiaDevice
import requests
from homeassistant.components.alarm_control_panel import (
@@ -11,6 +12,7 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -47,10 +49,10 @@ def setup_platform(
if discovery_info is None:
return
device = EgardiaAlarm(
discovery_info["name"],
discovery_info[CONF_NAME],
hass.data[EGARDIA_DEVICE],
discovery_info[CONF_REPORT_SERVER_ENABLED],
discovery_info.get(CONF_REPORT_SERVER_CODES),
discovery_info[CONF_REPORT_SERVER_CODES],
discovery_info[CONF_REPORT_SERVER_PORT],
)
@@ -67,8 +69,13 @@ class EgardiaAlarm(AlarmControlPanelEntity):
)
def __init__(
self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010
):
self,
name: str,
egardiasystem: EgardiaDevice,
rs_enabled: bool,
rs_codes: dict[str, list[str]],
rs_port: int,
) -> None:
"""Initialize the Egardia alarm."""
self._attr_name = name
self._egardiasystem = egardiasystem
@@ -85,9 +92,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
@property
def should_poll(self) -> bool:
"""Poll if no report server is enabled."""
if not self._rs_enabled:
return True
return False
return not self._rs_enabled
def handle_status_event(self, event):
"""Handle the Egardia system status event."""

View File

@@ -2,11 +2,12 @@
from __future__ import annotations
from pythonegardia.egardiadevice import EgardiaDevice
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -51,30 +52,20 @@ async def async_setup_platform(
class EgardiaBinarySensor(BinarySensorEntity):
"""Represents a sensor based on an Egardia sensor (IR, Door Contact)."""
def __init__(self, sensor_id, name, egardia_system, device_class):
def __init__(
self,
sensor_id: str,
name: str,
egardia_system: EgardiaDevice,
device_class: BinarySensorDeviceClass | None,
) -> None:
"""Initialize the sensor device."""
self._id = sensor_id
self._name = name
self._state = None
self._device_class = device_class
self._attr_name = name
self._attr_device_class = device_class
self._egardia_system = egardia_system
def update(self) -> None:
"""Update the status."""
egardia_input = self._egardia_system.getsensorstate(self._id)
self._state = STATE_ON if egardia_input else STATE_OFF
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def is_on(self):
"""Whether the device is switched on."""
return self._state == STATE_ON
@property
def device_class(self) -> BinarySensorDeviceClass | None:
"""Return the device class."""
return self._device_class
self._attr_is_on = bool(egardia_input)

View File

@@ -18,12 +18,13 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
DOMAIN = "envisalink"
DATA_EVL = "envisalink"
DATA_EVL: HassKey[EnvisalinkAlarmPanel] = HassKey(DOMAIN)
CONF_EVL_KEEPALIVE = "keepalive_interval"
CONF_EVL_PORT = "port"

View File

@@ -3,7 +3,9 @@
from __future__ import annotations
import logging
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
@@ -22,6 +24,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
CONF_PANIC,
CONF_PARTITIONNAME,
CONF_PARTITIONS,
DATA_EVL,
DOMAIN,
PARTITION_SCHEMA,
@@ -51,15 +54,14 @@ async def async_setup_platform(
"""Perform the setup for Envisalink alarm panels."""
if not discovery_info:
return
configured_partitions = discovery_info["partitions"]
code = discovery_info[CONF_CODE]
panic_type = discovery_info[CONF_PANIC]
configured_partitions: dict[int, dict[str, Any]] = discovery_info[CONF_PARTITIONS]
code: str | None = discovery_info[CONF_CODE]
panic_type: str = discovery_info[CONF_PANIC]
entities = []
for part_num in configured_partitions:
entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
for part_num, part_config in configured_partitions.items():
entity_config_data = PARTITION_SCHEMA(part_config)
entity = EnvisalinkAlarm(
hass,
part_num,
entity_config_data[CONF_PARTITIONNAME],
code,
@@ -103,8 +105,14 @@ class EnvisalinkAlarm(EnvisalinkEntity, AlarmControlPanelEntity):
)
def __init__(
self, hass, partition_number, alarm_name, code, panic_type, info, controller
):
self,
partition_number: int,
alarm_name: str,
code: str | None,
panic_type: str,
info: dict[str, Any],
controller: EnvisalinkAlarmPanel,
) -> None:
"""Initialize the alarm panel."""
self._partition_number = partition_number
self._panic_type = panic_type

View File

@@ -4,6 +4,9 @@ from __future__ import annotations
import datetime
import logging
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -16,7 +19,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
from . import CONF_ZONENAME, CONF_ZONETYPE, DATA_EVL, SIGNAL_ZONE_UPDATE, ZONE_SCHEMA
from . import (
CONF_ZONENAME,
CONF_ZONES,
CONF_ZONETYPE,
DATA_EVL,
SIGNAL_ZONE_UPDATE,
ZONE_SCHEMA,
)
from .entity import EnvisalinkEntity
_LOGGER = logging.getLogger(__name__)
@@ -31,13 +41,12 @@ async def async_setup_platform(
"""Set up the Envisalink binary sensor entities."""
if not discovery_info:
return
configured_zones = discovery_info["zones"]
configured_zones: dict[int, dict[str, Any]] = discovery_info[CONF_ZONES]
entities = []
for zone_num in configured_zones:
entity_config_data = ZONE_SCHEMA(configured_zones[zone_num])
for zone_num, zone_data in configured_zones.items():
entity_config_data = ZONE_SCHEMA(zone_data)
entity = EnvisalinkBinarySensor(
hass,
zone_num,
entity_config_data[CONF_ZONENAME],
entity_config_data[CONF_ZONETYPE],
@@ -52,9 +61,16 @@ async def async_setup_platform(
class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
"""Representation of an Envisalink binary sensor."""
def __init__(self, hass, zone_number, zone_name, zone_type, info, controller):
def __init__(
self,
zone_number: int,
zone_name: str,
zone_type: BinarySensorDeviceClass,
info: dict[str, Any],
controller: EnvisalinkAlarmPanel,
) -> None:
"""Initialize the binary_sensor."""
self._zone_type = zone_type
self._attr_device_class = zone_type
self._zone_number = zone_number
_LOGGER.debug("Setting up zone: %s", zone_name)
@@ -69,9 +85,9 @@ class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
)
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
attr = {}
attr: dict[str, Any] = {}
# The Envisalink library returns a "last_fault" value that's the
# number of seconds since the last fault, up to a maximum of 327680
@@ -104,11 +120,6 @@ class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
"""Return true if sensor is on."""
return self._info["status"]["open"]
@property
def device_class(self) -> BinarySensorDeviceClass:
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
@callback
def async_update_callback(self, zone):
"""Update the zone's state, if needed."""

View File

@@ -1,5 +1,9 @@
"""Support for Envisalink devices."""
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
from homeassistant.helpers.entity import Entity
@@ -8,13 +12,10 @@ class EnvisalinkEntity(Entity):
_attr_should_poll = False
def __init__(self, name, info, controller):
def __init__(
self, name: str, info: dict[str, Any], controller: EnvisalinkAlarmPanel
) -> None:
"""Initialize the device."""
self._controller = controller
self._info = info
self._name = name
@property
def name(self):
"""Return the name of the device."""
return self._name
self._attr_name = name

View File

@@ -3,6 +3,9 @@
from __future__ import annotations
import logging
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
from homeassistant.components.sensor import SensorEntity
from homeassistant.core import HomeAssistant, callback
@@ -12,6 +15,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import (
CONF_PARTITIONNAME,
CONF_PARTITIONS,
DATA_EVL,
PARTITION_SCHEMA,
SIGNAL_KEYPAD_UPDATE,
@@ -31,13 +35,12 @@ async def async_setup_platform(
"""Perform the setup for Envisalink sensor entities."""
if not discovery_info:
return
configured_partitions = discovery_info["partitions"]
configured_partitions: dict[int, dict[str, Any]] = discovery_info[CONF_PARTITIONS]
entities = []
for part_num in configured_partitions:
entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
for part_num, part_config in configured_partitions.items():
entity_config_data = PARTITION_SCHEMA(part_config)
entity = EnvisalinkSensor(
hass,
entity_config_data[CONF_PARTITIONNAME],
part_num,
hass.data[DATA_EVL].alarm_state["partition"][part_num],
@@ -52,9 +55,16 @@ async def async_setup_platform(
class EnvisalinkSensor(EnvisalinkEntity, SensorEntity):
"""Representation of an Envisalink keypad."""
def __init__(self, hass, partition_name, partition_number, info, controller):
_attr_icon = "mdi:alarm"
def __init__(
self,
partition_name: str,
partition_number: int,
info: dict[str, Any],
controller: EnvisalinkAlarmPanel,
) -> None:
"""Initialize the sensor."""
self._icon = "mdi:alarm"
self._partition_number = partition_number
_LOGGER.debug("Setting up sensor for partition: %s", partition_name)
@@ -73,11 +83,6 @@ class EnvisalinkSensor(EnvisalinkEntity, SensorEntity):
)
)
@property
def icon(self):
"""Return the icon if any."""
return self._icon
@property
def native_value(self):
"""Return the overall state."""

View File

@@ -5,13 +5,21 @@ from __future__ import annotations
import logging
from typing import Any
from pyenvisalink import EnvisalinkAlarmPanel
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import CONF_ZONENAME, DATA_EVL, SIGNAL_ZONE_BYPASS_UPDATE, ZONE_SCHEMA
from . import (
CONF_ZONENAME,
CONF_ZONES,
DATA_EVL,
SIGNAL_ZONE_BYPASS_UPDATE,
ZONE_SCHEMA,
)
from .entity import EnvisalinkEntity
_LOGGER = logging.getLogger(__name__)
@@ -26,16 +34,15 @@ async def async_setup_platform(
"""Set up the Envisalink switch entities."""
if not discovery_info:
return
configured_zones = discovery_info["zones"]
configured_zones: dict[int, dict[str, Any]] = discovery_info[CONF_ZONES]
entities = []
for zone_num in configured_zones:
entity_config_data = ZONE_SCHEMA(configured_zones[zone_num])
for zone_num, zone_data in configured_zones.items():
entity_config_data = ZONE_SCHEMA(zone_data)
zone_name = f"{entity_config_data[CONF_ZONENAME]}_bypass"
_LOGGER.debug("Setting up zone_bypass switch: %s", zone_name)
entity = EnvisalinkSwitch(
hass,
zone_num,
zone_name,
hass.data[DATA_EVL].alarm_state["zone"][zone_num],
@@ -49,7 +56,13 @@ async def async_setup_platform(
class EnvisalinkSwitch(EnvisalinkEntity, SwitchEntity):
"""Representation of an Envisalink switch."""
def __init__(self, hass, zone_number, zone_name, info, controller):
def __init__(
self,
zone_number: int,
zone_name: str,
info: dict[str, Any],
controller: EnvisalinkAlarmPanel,
) -> None:
"""Initialize the switch."""
self._zone_number = zone_number

View File

@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260107.1"]
"requirements": ["home-assistant-frontend==20260107.2"]
}

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["google-genai==1.56.0"]
"requirements": ["google-genai==1.59.0"]
}

View File

@@ -19,6 +19,8 @@ from .const import DOMAIN
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
PARALLEL_UPDATES = 1
@dataclass(kw_only=True, frozen=True)
class HDFuryButtonEntityDescription(ButtonEntityDescription):

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/hdfury",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["hdfury==1.3.1"]
}

View File

@@ -35,11 +35,11 @@ rules:
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
parallel-updates: done
reauthentication-flow:
status: exempt
comment: Integration has no authentication flow.
test-coverage: todo
test-coverage: done
# Gold
devices: done

View File

@@ -20,6 +20,8 @@ from .const import DOMAIN
from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
from .entity import HDFuryEntity
PARALLEL_UPDATES = 1
@dataclass(kw_only=True, frozen=True)
class HDFurySelectEntityDescription(SelectEntityDescription):
@@ -77,13 +79,11 @@ async def async_setup_entry(
coordinator = entry.runtime_data
entities: list[HDFuryEntity] = []
for description in SELECT_PORTS:
if description.key not in coordinator.data.info:
continue
entities.append(HDFurySelect(coordinator, description))
entities: list[HDFuryEntity] = [
HDFurySelect(coordinator, description)
for description in SELECT_PORTS
if description.key in coordinator.data.info
]
# Add OPMODE select if present
if "opmode" in coordinator.data.info:

View File

@@ -8,6 +8,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
PARALLEL_UPDATES = 0
SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="RX0",

View File

@@ -16,6 +16,8 @@ from .const import DOMAIN
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
PARALLEL_UPDATES = 1
@dataclass(kw_only=True, frozen=True)
class HDFurySwitchEntityDescription(SwitchEntityDescription):

View File

@@ -59,7 +59,7 @@ class HMBinarySensor(HMDevice, BinarySensorEntity):
"""Representation of a binary HomeMatic device."""
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if switch is on."""
if not self.available:
return False
@@ -73,7 +73,7 @@ class HMBinarySensor(HMDevice, BinarySensorEntity):
return BinarySensorDeviceClass.MOTION
return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__)
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate the data dictionary (self._data) from metadata."""
# Add state to data struct
if self._state:
@@ -86,11 +86,11 @@ class HMBatterySensor(HMDevice, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.BATTERY
@property
def is_on(self):
def is_on(self) -> bool:
"""Return True if battery is low."""
return bool(self._hm_get_state())
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate the data dictionary (self._data) from metadata."""
# Add state to data struct
if self._state:

View File

@@ -178,7 +178,7 @@ class HMThermostat(HMDevice, ClimateEntity):
# Homematic
return self._data.get("CONTROL_MODE")
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate a data dict (self._data) from the Homematic metadata."""
self._state = next(iter(self._hmdevice.WRITENODE.keys()))
self._data[self._state] = None

View File

@@ -78,7 +78,7 @@ class HMCover(HMDevice, CoverEntity):
"""Stop the device if in motion."""
self._hmdevice.stop(self._channel)
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate a data dictionary (self._data) from metadata."""
self._state = "LEVEL"
self._data.update({self._state: None})
@@ -138,7 +138,7 @@ class HMGarage(HMCover):
"""Return whether the cover is closed."""
return self._hmdevice.is_closed(self._hm_get_state())
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate a data dictionary (self._data) from metadata."""
self._state = "DOOR_STATE"
self._data.update({self._state: None})

View File

@@ -204,7 +204,7 @@ class HMDevice(Entity):
self._init_data_struct()
@abstractmethod
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate a data dictionary from the HomeMatic device metadata."""

View File

@@ -51,7 +51,7 @@ class HMLight(HMDevice, LightEntity):
_attr_max_color_temp_kelvin = 6500 # 153 Mireds
@property
def brightness(self):
def brightness(self) -> int | None:
"""Return the brightness of this light between 0..255."""
# Is dimmer?
if self._state == "LEVEL":
@@ -59,7 +59,7 @@ class HMLight(HMDevice, LightEntity):
return None
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if light is on."""
try:
return self._hm_get_state() > 0
@@ -98,7 +98,7 @@ class HMLight(HMDevice, LightEntity):
return features
@property
def hs_color(self):
def hs_color(self) -> tuple[float, float] | None:
"""Return the hue and saturation color value [float, float]."""
if ColorMode.HS not in self.supported_color_modes:
return None
@@ -116,14 +116,14 @@ class HMLight(HMDevice, LightEntity):
)
@property
def effect_list(self):
def effect_list(self) -> list[str] | None:
"""Return the list of supported effects."""
if not self.supported_features & LightEntityFeature.EFFECT:
return None
return self._hmdevice.get_effect_list()
@property
def effect(self):
def effect(self) -> str | None:
"""Return the current color change program of the light."""
if not self.supported_features & LightEntityFeature.EFFECT:
return None
@@ -166,7 +166,7 @@ class HMLight(HMDevice, LightEntity):
self._hmdevice.off(self._channel)
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate a data dict (self._data) from the Homematic metadata."""
# Use LEVEL
self._state = "LEVEL"

View File

@@ -48,7 +48,7 @@ class HMLock(HMDevice, LockEntity):
"""Open the door latch."""
self._hmdevice.open()
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate the data dictionary (self._data) from metadata."""
self._state = "STATE"
self._data.update({self._state: None})

View File

@@ -339,7 +339,7 @@ class HMSensor(HMDevice, SensorEntity):
# No cast, return original value
return self._hm_get_state()
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate a data dictionary (self._data) from metadata."""
if self._state:
self._data.update({self._state: None})

View File

@@ -35,7 +35,7 @@ class HMSwitch(HMDevice, SwitchEntity):
"""Representation of a HomeMatic switch."""
@property
def is_on(self):
def is_on(self) -> bool:
"""Return True if switch is on."""
try:
return self._hm_get_state() > 0
@@ -43,7 +43,7 @@ class HMSwitch(HMDevice, SwitchEntity):
return False
@property
def today_energy_kwh(self):
def today_energy_kwh(self) -> float | None:
"""Return the current power usage in kWh."""
if "ENERGY_COUNTER" in self._data:
try:
@@ -61,7 +61,7 @@ class HMSwitch(HMDevice, SwitchEntity):
"""Turn the switch off."""
self._hmdevice.off(self._channel)
def _init_data_struct(self):
def _init_data_struct(self) -> None:
"""Generate the data dictionary (self._data) from metadata."""
self._state = "STATE"
self._data.update({self._state: None})

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError
from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorTimeoutError
from homeassistant.const import (
CONF_HOST,
@@ -11,8 +11,9 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator
@@ -28,8 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool:
)
try:
await device.connect(True)
except JvcProjectorConnectError as err:
await device.connect()
except JvcProjectorTimeoutError as err:
await device.disconnect()
raise ConfigEntryNotReady(
f"Unable to connect to {entry.data[CONF_HOST]}"
@@ -50,6 +51,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect)
)
await async_migrate_entities(hass, entry, coordinator)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -60,3 +63,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await entry.runtime_data.device.disconnect()
return unload_ok
async def async_migrate_entities(
hass: HomeAssistant,
config_entry: JVCConfigEntry,
coordinator: JvcProjectorDataUpdateCoordinator,
) -> None:
"""Migrate old entities as needed."""
@callback
def _update_entry(entry: RegistryEntry) -> dict[str, str] | None:
"""Fix unique_id of power binary_sensor entry."""
if entry.domain == Platform.BINARY_SENSOR and ":" not in entry.unique_id:
if "_power" in entry.unique_id:
return {"new_unique_id": f"{coordinator.unique_id}_power"}
return None
await async_migrate_entries(hass, config_entry.entry_id, _update_entry)

View File

@@ -2,16 +2,17 @@
from __future__ import annotations
from jvcprojector import const
from jvcprojector import command as cmd
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import POWER
from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator
from .entity import JvcProjectorEntity
ON_STATUS = (const.ON, const.WARMING)
ON_STATUS = (cmd.Power.ON, cmd.Power.WARMING)
async def async_setup_entry(
@@ -21,14 +22,13 @@ async def async_setup_entry(
) -> None:
"""Set up the JVC Projector platform from a config entry."""
coordinator = entry.runtime_data
async_add_entities([JvcBinarySensor(coordinator)])
class JvcBinarySensor(JvcProjectorEntity, BinarySensorEntity):
"""The entity class for JVC Projector Binary Sensor."""
_attr_translation_key = "jvc_power"
_attr_translation_key = "power"
def __init__(
self,
@@ -36,9 +36,9 @@ class JvcBinarySensor(JvcProjectorEntity, BinarySensorEntity):
) -> None:
"""Initialize the JVC Projector sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.device.mac}_power"
self._attr_unique_id = f"{coordinator.unique_id}_power"
@property
def is_on(self) -> bool:
"""Return true if the JVC is on."""
return self.coordinator.data["power"] in ON_STATUS
"""Return true if the JVC Projector is on."""
return self.coordinator.data[POWER] in ON_STATUS

View File

@@ -5,7 +5,12 @@ from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError
from jvcprojector import (
JvcProjector,
JvcProjectorAuthError,
JvcProjectorTimeoutError,
command as cmd,
)
from jvcprojector.projector import DEFAULT_PORT
import voluptuous as vol
@@ -40,7 +45,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN):
mac = await get_mac_address(host, port, password)
except InvalidHost:
errors["base"] = "invalid_host"
except JvcProjectorConnectError:
except JvcProjectorTimeoutError:
errors["base"] = "cannot_connect"
except JvcProjectorAuthError:
errors["base"] = "invalid_auth"
@@ -91,7 +96,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN):
try:
await get_mac_address(host, port, password)
except JvcProjectorConnectError:
except JvcProjectorTimeoutError:
errors["base"] = "cannot_connect"
except JvcProjectorAuthError:
errors["base"] = "invalid_auth"
@@ -115,7 +120,7 @@ async def get_mac_address(host: str, port: int, password: str | None) -> str:
"""Get device mac address for config flow."""
device = JvcProjector(host, port=port, password=password)
try:
await device.connect(True)
await device.connect()
return await device.get(cmd.MacAddress)
finally:
await device.disconnect()
return device.mac

View File

@@ -3,3 +3,7 @@
NAME = "JVC Projector"
DOMAIN = "jvc_projector"
MANUFACTURER = "JVC"
POWER = "power"
INPUT = "input"
SOURCE = "source"

View File

@@ -4,22 +4,21 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from typing import TYPE_CHECKING, Any
from jvcprojector import (
JvcProjector,
JvcProjectorAuthError,
JvcProjectorConnectError,
const,
JvcProjectorTimeoutError,
command as cmd,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import NAME
from .const import INPUT, NAME, POWER
_LOGGER = logging.getLogger(__name__)
@@ -46,26 +45,33 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
update_interval=INTERVAL_SLOW,
)
self.device = device
self.unique_id = format_mac(device.mac)
self.device: JvcProjector = device
if TYPE_CHECKING:
assert config_entry.unique_id is not None
self.unique_id = config_entry.unique_id
async def _async_update_data(self) -> dict[str, Any]:
"""Get the latest state data."""
state: dict[str, str | None] = {
POWER: None,
INPUT: None,
}
try:
state = await self.device.get_state()
except JvcProjectorConnectError as err:
state[POWER] = await self.device.get(cmd.Power)
if state[POWER] == cmd.Power.ON:
state[INPUT] = await self.device.get(cmd.Input)
except JvcProjectorTimeoutError as err:
raise UpdateFailed(f"Unable to connect to {self.device.host}") from err
except JvcProjectorAuthError as err:
raise ConfigEntryAuthFailed("Password authentication failed") from err
old_interval = self.update_interval
if state[const.POWER] != const.STANDBY:
if state[POWER] != cmd.Power.STANDBY:
self.update_interval = INTERVAL_FAST
else:
self.update_interval = INTERVAL_SLOW
if self.update_interval != old_interval:
_LOGGER.debug("Changed update interval to %s", self.update_interval)
return state

View File

@@ -26,7 +26,7 @@ class JvcProjectorEntity(CoordinatorEntity[JvcProjectorDataUpdateCoordinator]):
self._attr_unique_id = coordinator.unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.unique_id)},
identifiers={(DOMAIN, self._attr_unique_id)},
name=NAME,
model=self.device.model,
manufacturer=MANUFACTURER,

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==1.1.3"]
"requirements": ["pyjvcprojector==2.0.0"]
}

View File

@@ -7,54 +7,62 @@ from collections.abc import Iterable
import logging
from typing import Any
from jvcprojector import const
from jvcprojector import command as cmd
from homeassistant.components.remote import RemoteEntity
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import POWER
from .coordinator import JVCConfigEntry
from .entity import JvcProjectorEntity
COMMANDS = {
"menu": const.REMOTE_MENU,
"up": const.REMOTE_UP,
"down": const.REMOTE_DOWN,
"left": const.REMOTE_LEFT,
"right": const.REMOTE_RIGHT,
"ok": const.REMOTE_OK,
"back": const.REMOTE_BACK,
"mpc": const.REMOTE_MPC,
"hide": const.REMOTE_HIDE,
"info": const.REMOTE_INFO,
"input": const.REMOTE_INPUT,
"cmd": const.REMOTE_CMD,
"advanced_menu": const.REMOTE_ADVANCED_MENU,
"picture_mode": const.REMOTE_PICTURE_MODE,
"color_profile": const.REMOTE_COLOR_PROFILE,
"lens_control": const.REMOTE_LENS_CONTROL,
"setting_memory": const.REMOTE_SETTING_MEMORY,
"gamma_settings": const.REMOTE_GAMMA_SETTINGS,
"hdmi_1": const.REMOTE_HDMI_1,
"hdmi_2": const.REMOTE_HDMI_2,
"mode_1": const.REMOTE_MODE_1,
"mode_2": const.REMOTE_MODE_2,
"mode_3": const.REMOTE_MODE_3,
"mode_4": const.REMOTE_MODE_4,
"mode_5": const.REMOTE_MODE_5,
"mode_6": const.REMOTE_MODE_6,
"mode_7": const.REMOTE_MODE_7,
"mode_8": const.REMOTE_MODE_8,
"mode_9": const.REMOTE_MODE_9,
"mode_10": const.REMOTE_MODE_10,
"lens_ap": const.REMOTE_LENS_AP,
"gamma": const.REMOTE_GAMMA,
"color_temp": const.REMOTE_COLOR_TEMP,
"natural": const.REMOTE_NATURAL,
"cinema": const.REMOTE_CINEMA,
"anamo": const.REMOTE_ANAMO,
"3d_format": const.REMOTE_3D_FORMAT,
COMMANDS: list[str] = [
cmd.Remote.MENU,
cmd.Remote.UP,
cmd.Remote.DOWN,
cmd.Remote.LEFT,
cmd.Remote.RIGHT,
cmd.Remote.OK,
cmd.Remote.BACK,
cmd.Remote.MPC,
cmd.Remote.HIDE,
cmd.Remote.INFO,
cmd.Remote.INPUT,
cmd.Remote.CMD,
cmd.Remote.ADVANCED_MENU,
cmd.Remote.PICTURE_MODE,
cmd.Remote.COLOR_PROFILE,
cmd.Remote.LENS_CONTROL,
cmd.Remote.SETTING_MEMORY,
cmd.Remote.GAMMA_SETTINGS,
cmd.Remote.HDMI1,
cmd.Remote.HDMI2,
cmd.Remote.MODE_1,
cmd.Remote.MODE_2,
cmd.Remote.MODE_3,
cmd.Remote.MODE_4,
cmd.Remote.MODE_5,
cmd.Remote.MODE_6,
cmd.Remote.MODE_7,
cmd.Remote.MODE_8,
cmd.Remote.MODE_9,
cmd.Remote.MODE_10,
cmd.Remote.GAMMA,
cmd.Remote.NATURAL,
cmd.Remote.CINEMA,
cmd.Remote.COLOR_TEMP,
cmd.Remote.ANAMORPHIC,
cmd.Remote.LENS_APERTURE,
cmd.Remote.V3D_FORMAT,
]
RENAMED_COMMANDS: dict[str, str] = {
"anamo": cmd.Remote.ANAMORPHIC,
"lens_ap": cmd.Remote.LENS_APERTURE,
"hdmi1": cmd.Remote.HDMI1,
"hdmi2": cmd.Remote.HDMI2,
}
_LOGGER = logging.getLogger(__name__)
@@ -77,25 +85,34 @@ class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity):
@property
def is_on(self) -> bool:
"""Return True if entity is on."""
return self.coordinator.data["power"] in [const.ON, const.WARMING]
"""Return True if the entity is on."""
return self.coordinator.data[POWER] in (cmd.Power.ON, cmd.Power.WARMING)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self.device.power_on()
await self.device.set(cmd.Power, cmd.Power.ON)
await asyncio.sleep(1)
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
await self.device.power_off()
await self.device.set(cmd.Power, cmd.Power.OFF)
await asyncio.sleep(1)
await self.coordinator.async_refresh()
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a remote command to the device."""
for cmd in command:
if cmd not in COMMANDS:
raise HomeAssistantError(f"{cmd} is not a known command")
_LOGGER.debug("Sending command '%s'", cmd)
await self.device.remote(COMMANDS[cmd])
for send_command in command:
# Legacy name replace
if send_command in RENAMED_COMMANDS:
send_command = RENAMED_COMMANDS[send_command]
# Legacy name fixup
if "_" in send_command:
send_command = send_command.replace("_", "-")
if send_command not in COMMANDS:
raise HomeAssistantError(f"{send_command} is not a known command")
_LOGGER.debug("Sending command '%s'", send_command)
await self.device.remote(send_command)

View File

@@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Final
from jvcprojector import JvcProjector, const
from jvcprojector import JvcProjector, command as cmd
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
@@ -23,16 +23,12 @@ class JvcProjectorSelectDescription(SelectEntityDescription):
command: Callable[[JvcProjector, str], Awaitable[None]]
OPTIONS: Final[dict[str, dict[str, str]]] = {
"input": {const.HDMI1: const.REMOTE_HDMI_1, const.HDMI2: const.REMOTE_HDMI_2}
}
SELECTS: Final[list[JvcProjectorSelectDescription]] = [
JvcProjectorSelectDescription(
key="input",
translation_key="input",
options=list(OPTIONS["input"]),
command=lambda device, option: device.remote(OPTIONS["input"][option]),
options=[cmd.Input.HDMI1, cmd.Input.HDMI2],
command=lambda device, option: device.set(cmd.Input, option),
)
]

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from jvcprojector import const
from jvcprojector import command as cmd
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -23,11 +23,11 @@ JVC_SENSORS = (
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=[
const.STANDBY,
const.ON,
const.WARMING,
const.COOLING,
const.ERROR,
cmd.Power.STANDBY,
cmd.Power.ON,
cmd.Power.WARMING,
cmd.Power.COOLING,
cmd.Power.ERROR,
],
),
)

View File

@@ -35,7 +35,7 @@
},
"entity": {
"binary_sensor": {
"jvc_power": {
"power": {
"name": "[%key:component::binary_sensor::entity_component::power::name%]"
}
},
@@ -50,7 +50,7 @@
},
"sensor": {
"jvc_power_status": {
"name": "Power status",
"name": "Status",
"state": {
"cooling": "Cooling",
"error": "[%key:common::state::error%]",

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.14.0",
"xknxproject==3.8.2",
"knx-frontend==2025.12.30.151231"
"knx-frontend==2026.1.15.112308"
],
"single_config_entry": true
}

View File

@@ -18,7 +18,11 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_custom_components
from .const import DOMAIN, LABS_DATA, STORAGE_KEY, STORAGE_VERSION
from .helpers import async_is_preview_feature_enabled, async_listen
from .helpers import (
async_is_preview_feature_enabled,
async_listen,
async_update_preview_feature,
)
from .models import (
EventLabsUpdatedData,
LabPreviewFeature,
@@ -37,6 +41,7 @@ __all__ = [
"EventLabsUpdatedData",
"async_is_preview_feature_enabled",
"async_listen",
"async_update_preview_feature",
]

View File

@@ -61,3 +61,32 @@ def async_listen(
listener()
return hass.bus.async_listen(EVENT_LABS_UPDATED, _async_feature_updated)
async def async_update_preview_feature(
hass: HomeAssistant,
domain: str,
preview_feature: str,
enabled: bool,
) -> None:
"""Update a lab preview feature state."""
labs_data = hass.data[LABS_DATA]
preview_feature_id = f"{domain}.{preview_feature}"
if preview_feature_id not in labs_data.preview_features:
raise ValueError(f"Preview feature {preview_feature_id} not found")
if enabled:
labs_data.data.preview_feature_status.add((domain, preview_feature))
else:
labs_data.data.preview_feature_status.discard((domain, preview_feature))
await labs_data.store.async_save(labs_data.data.to_store_format())
event_data: EventLabsUpdatedData = {
"domain": domain,
"preview_feature": preview_feature,
"enabled": enabled,
}
hass.bus.async_fire(EVENT_LABS_UPDATED, event_data)

View File

@@ -8,12 +8,14 @@ import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.backup import async_get_manager
from homeassistant.const import EVENT_LABS_UPDATED
from homeassistant.core import HomeAssistant, callback
from .const import LABS_DATA
from .helpers import async_is_preview_feature_enabled, async_listen
from .models import EventLabsUpdatedData
from .helpers import (
async_is_preview_feature_enabled,
async_listen,
async_update_preview_feature,
)
@callback
@@ -95,19 +97,7 @@ async def websocket_update_preview_feature(
)
return
if enabled:
labs_data.data.preview_feature_status.add((domain, preview_feature))
else:
labs_data.data.preview_feature_status.discard((domain, preview_feature))
await labs_data.store.async_save(labs_data.data.to_store_format())
event_data: EventLabsUpdatedData = {
"domain": domain,
"preview_feature": preview_feature,
"enabled": enabled,
}
hass.bus.async_fire(EVENT_LABS_UPDATED, event_data)
await async_update_preview_feature(hass, domain, preview_feature, enabled)
connection.send_result(msg["id"])

View File

@@ -27,6 +27,7 @@ SCAN_INTERVAL = timedelta(minutes=30)
AUTHORITIES = [
"Barking and Dagenham",
"Barnet",
"Bexley",
"Brent",
"Bromley",
@@ -49,11 +50,13 @@ AUTHORITIES = [
"Lambeth",
"Lewisham",
"Merton",
"Newham",
"Redbridge",
"Richmond",
"Southwark",
"Sutton",
"Tower Hamlets",
"Waltham Forest",
"Wandsworth",
"Westminster",
]

View File

@@ -28,7 +28,7 @@ from .coordinator import MastodonConfigEntry, MastodonCoordinator, MastodonData
from .services import async_setup_services
from .utils import construct_mastodon_username, create_mastodon_client
PLATFORMS: list[Platform] = [Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)

View File

@@ -0,0 +1,128 @@
"""Binary sensor platform for the Mastodon integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from enum import StrEnum
from mastodon.Mastodon import Account
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import MastodonConfigEntry
from .entity import MastodonEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
class MastodonBinarySensor(StrEnum):
"""Mastodon binary sensors."""
BOT = "bot"
SUSPENDED = "suspended"
DISCOVERABLE = "discoverable"
LOCKED = "locked"
INDEXABLE = "indexable"
LIMITED = "limited"
MEMORIAL = "memorial"
MOVED = "moved"
@dataclass(frozen=True, kw_only=True)
class MastodonBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Mastodon binary sensor description."""
is_on_fn: Callable[[Account], bool | None]
ENTITY_DESCRIPTIONS: tuple[MastodonBinarySensorEntityDescription, ...] = (
MastodonBinarySensorEntityDescription(
key=MastodonBinarySensor.BOT,
translation_key=MastodonBinarySensor.BOT,
is_on_fn=lambda account: account.bot,
entity_category=EntityCategory.DIAGNOSTIC,
),
MastodonBinarySensorEntityDescription(
key=MastodonBinarySensor.DISCOVERABLE,
translation_key=MastodonBinarySensor.DISCOVERABLE,
is_on_fn=lambda account: account.discoverable,
entity_category=EntityCategory.DIAGNOSTIC,
),
MastodonBinarySensorEntityDescription(
key=MastodonBinarySensor.LOCKED,
translation_key=MastodonBinarySensor.LOCKED,
is_on_fn=lambda account: account.locked,
entity_category=EntityCategory.DIAGNOSTIC,
),
MastodonBinarySensorEntityDescription(
key=MastodonBinarySensor.MOVED,
translation_key=MastodonBinarySensor.MOVED,
is_on_fn=lambda account: account.moved is not None,
entity_category=EntityCategory.DIAGNOSTIC,
),
MastodonBinarySensorEntityDescription(
key=MastodonBinarySensor.INDEXABLE,
translation_key=MastodonBinarySensor.INDEXABLE,
is_on_fn=lambda account: account.indexable,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
MastodonBinarySensorEntityDescription(
key=MastodonBinarySensor.LIMITED,
translation_key=MastodonBinarySensor.LIMITED,
is_on_fn=lambda account: account.limited is True,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
MastodonBinarySensorEntityDescription(
key=MastodonBinarySensor.MEMORIAL,
translation_key=MastodonBinarySensor.MEMORIAL,
is_on_fn=lambda account: account.memorial is True,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
MastodonBinarySensorEntityDescription(
key=MastodonBinarySensor.SUSPENDED,
translation_key=MastodonBinarySensor.SUSPENDED,
is_on_fn=lambda account: account.suspended is True,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: MastodonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the binary sensor platform."""
coordinator = entry.runtime_data.coordinator
async_add_entities(
MastodonBinarySensorEntity(
coordinator=coordinator,
entity_description=entity_description,
data=entry,
)
for entity_description in ENTITY_DESCRIPTIONS
)
class MastodonBinarySensorEntity(MastodonEntity, BinarySensorEntity):
"""Mastodon binary sensor entity."""
entity_description: MastodonBinarySensorEntityDescription
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self.entity_description.is_on_fn(self.coordinator.data)

View File

@@ -1,5 +1,18 @@
{
"entity": {
"binary_sensor": {
"bot": { "default": "mdi:robot" },
"discoverable": { "default": "mdi:magnify-scan" },
"indexable": { "default": "mdi:search-web" },
"limited": { "default": "mdi:account-cancel" },
"locked": {
"default": "mdi:account-lock",
"state": { "off": "mdi:account-lock-open" }
},
"memorial": { "default": "mdi:candle" },
"moved": { "default": "mdi:truck-delivery" },
"suspended": { "default": "mdi:account-off" }
},
"sensor": {
"followers": {
"default": "mdi:account-multiple"

View File

@@ -26,6 +26,16 @@
}
},
"entity": {
"binary_sensor": {
"bot": { "name": "Bot" },
"discoverable": { "name": "Discoverable" },
"indexable": { "name": "Indexable" },
"limited": { "name": "Limited" },
"locked": { "name": "Locked" },
"memorial": { "name": "Memorial" },
"moved": { "name": "Moved" },
"suspended": { "name": "Suspended" }
},
"sensor": {
"followers": {
"name": "Followers",

View File

@@ -489,6 +489,7 @@ DISCOVERY_SCHEMAS = [
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="WindowCoveringConfigStatusOperational",
translation_key="config_status_operational",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
# unset Operational bit from ConfigStatus bitmap means problem

View File

@@ -442,6 +442,9 @@ DISCOVERY_SCHEMAS = [
key="PowerSourceBatVoltage",
translation_key="battery_voltage",
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
# Battery voltages are low-voltage diagnostics; use 2 decimals in volts
# to provide finer granularity than mains-level voltage sensors.
suggested_display_precision=2,
suggested_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,

View File

@@ -56,6 +56,9 @@
"boost_state": {
"name": "Boost state"
},
"config_status_operational": {
"name": "Configuration status"
},
"dishwasher_alarm_inflow": {
"name": "Inflow alarm"
},

View File

@@ -192,7 +192,7 @@ class MaxCubeClimate(ClimateEntity):
self._set_target(None, temp)
@property
def preset_mode(self):
def preset_mode(self) -> str:
"""Return the current preset mode."""
if self._device.mode == MAX_DEVICE_MODE_MANUAL:
if self._device.target_temperature == self._device.comfort_temperature:

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["aiomealie==1.1.1"]
"requirements": ["aiomealie==1.2.0"]
}

View File

@@ -5,8 +5,12 @@ from enum import StrEnum
import logging
from dns.resolver import LifetimeTimeout
from mcstatus import BedrockServer, JavaServer
from mcstatus.responses import BedrockStatusResponse, JavaStatusResponse
from mcstatus import BedrockServer, JavaServer, LegacyServer
from mcstatus.responses import (
BedrockStatusResponse,
JavaStatusResponse,
LegacyStatusResponse,
)
from homeassistant.core import HomeAssistant
@@ -43,6 +47,7 @@ class MinecraftServerType(StrEnum):
BEDROCK_EDITION = "Bedrock Edition"
JAVA_EDITION = "Java Edition"
LEGACY_JAVA_EDITION = "Legacy Java Edition"
class MinecraftServerAddressError(Exception):
@@ -60,7 +65,7 @@ class MinecraftServerNotInitializedError(Exception):
class MinecraftServer:
"""Minecraft Server wrapper class for 3rd party library mcstatus."""
_server: BedrockServer | JavaServer | None
_server: BedrockServer | JavaServer | LegacyServer | None
def __init__(
self, hass: HomeAssistant, server_type: MinecraftServerType, address: str
@@ -76,10 +81,12 @@ class MinecraftServer:
try:
if self._server_type == MinecraftServerType.JAVA_EDITION:
self._server = await JavaServer.async_lookup(self._address)
else:
elif self._server_type == MinecraftServerType.BEDROCK_EDITION:
self._server = await self._hass.async_add_executor_job(
BedrockServer.lookup, self._address
)
else:
self._server = await LegacyServer.async_lookup(self._address)
except (ValueError, LifetimeTimeout) as error:
raise MinecraftServerAddressError(
f"Lookup of '{self._address}' failed: {self._get_error_message(error)}"
@@ -112,7 +119,9 @@ class MinecraftServer:
async def async_get_data(self) -> MinecraftServerData:
"""Get updated data from the server, supporting both Java and Bedrock Edition servers."""
status_response: BedrockStatusResponse | JavaStatusResponse
status_response: (
BedrockStatusResponse | JavaStatusResponse | LegacyStatusResponse
)
if self._server is None:
raise MinecraftServerNotInitializedError(
@@ -128,8 +137,10 @@ class MinecraftServer:
if isinstance(status_response, JavaStatusResponse):
data = self._extract_java_data(status_response)
else:
elif isinstance(status_response, BedrockStatusResponse):
data = self._extract_bedrock_data(status_response)
else:
data = self._extract_legacy_data(status_response)
return data
@@ -169,6 +180,19 @@ class MinecraftServer:
map_name=status_response.map_name,
)
def _extract_legacy_data(
self, status_response: LegacyStatusResponse
) -> MinecraftServerData:
"""Extract legacy Java Edition server data out of status response."""
return MinecraftServerData(
latency=status_response.latency,
motd=status_response.motd.to_plain(),
players_max=status_response.players.max,
players_online=status_response.players.online,
protocol_version=status_response.version.protocol,
version=status_response.version.name,
)
def _get_error_message(self, error: BaseException) -> str:
"""Get error message of an exception."""
if not str(error):

View File

@@ -84,4 +84,5 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN):
}
),
errors=errors,
description_placeholders={"minimum_minecraft_version": "1.4"},
)

View File

@@ -1,12 +1,12 @@
{
"domain": "minecraft_server",
"name": "Minecraft Server",
"codeowners": ["@elmurato"],
"codeowners": ["@elmurato", "@zachdeibert"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/minecraft_server",
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["dnspython", "mcstatus"],
"quality_scale": "silver",
"requirements": ["mcstatus==12.0.6"]
"requirements": ["mcstatus==12.1.0"]
}

View File

@@ -65,6 +65,7 @@ SENSOR_DESCRIPTIONS = [
supported_server_types={
MinecraftServerType.JAVA_EDITION,
MinecraftServerType.BEDROCK_EDITION,
MinecraftServerType.LEGACY_JAVA_EDITION,
},
entity_category=EntityCategory.DIAGNOSTIC,
),
@@ -76,6 +77,7 @@ SENSOR_DESCRIPTIONS = [
supported_server_types={
MinecraftServerType.JAVA_EDITION,
MinecraftServerType.BEDROCK_EDITION,
MinecraftServerType.LEGACY_JAVA_EDITION,
},
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
@@ -89,6 +91,7 @@ SENSOR_DESCRIPTIONS = [
supported_server_types={
MinecraftServerType.JAVA_EDITION,
MinecraftServerType.BEDROCK_EDITION,
MinecraftServerType.LEGACY_JAVA_EDITION,
},
entity_registry_enabled_default=False,
),
@@ -102,6 +105,7 @@ SENSOR_DESCRIPTIONS = [
supported_server_types={
MinecraftServerType.JAVA_EDITION,
MinecraftServerType.BEDROCK_EDITION,
MinecraftServerType.LEGACY_JAVA_EDITION,
},
entity_category=EntityCategory.DIAGNOSTIC,
),
@@ -113,6 +117,7 @@ SENSOR_DESCRIPTIONS = [
supported_server_types={
MinecraftServerType.JAVA_EDITION,
MinecraftServerType.BEDROCK_EDITION,
MinecraftServerType.LEGACY_JAVA_EDITION,
},
),
MinecraftServerSensorEntityDescription(
@@ -124,6 +129,7 @@ SENSOR_DESCRIPTIONS = [
supported_server_types={
MinecraftServerType.JAVA_EDITION,
MinecraftServerType.BEDROCK_EDITION,
MinecraftServerType.LEGACY_JAVA_EDITION,
},
),
MinecraftServerSensorEntityDescription(

View File

@@ -4,7 +4,7 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"cannot_connect": "Failed to connect to server. Please check the address and try again. If a port was provided, it must be within a valid range. If you are running a Minecraft Java Edition server, ensure that it is at least version 1.7."
"cannot_connect": "Failed to connect to server. Please check the address and try again. If a port was provided, it must be within a valid range. If you are running a Minecraft Java Edition server, ensure that it is at least version {minimum_minecraft_version}."
},
"step": {
"user": {

View File

@@ -1,8 +1,8 @@
"""Support for Ness D8X/D16X devices."""
from collections import namedtuple
import datetime
import logging
from typing import NamedTuple
from nessclient import ArmingMode, ArmingState, Client
import voluptuous as vol
@@ -25,11 +25,12 @@ from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.start import async_at_started
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
DOMAIN = "ness_alarm"
DATA_NESS = "ness_alarm"
DATA_NESS: HassKey[Client] = HassKey(DOMAIN)
CONF_DEVICE_PORT = "port"
CONF_INFER_ARMING_STATE = "infer_arming_state"
@@ -44,7 +45,13 @@ DEFAULT_INFER_ARMING_STATE = False
SIGNAL_ZONE_CHANGED = "ness_alarm.zone_changed"
SIGNAL_ARMING_STATE_CHANGED = "ness_alarm.arming_state_changed"
ZoneChangedData = namedtuple("ZoneChangedData", ["zone_id", "state"]) # noqa: PYI024
class ZoneChangedData(NamedTuple):
"""Data for a zone state change."""
zone_id: int
state: bool
DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION
ZONE_SCHEMA = vol.Schema(

View File

@@ -33,18 +33,14 @@ async def async_setup_platform(
configured_zones = discovery_info[CONF_ZONES]
devices = []
for zone_config in configured_zones:
zone_type = zone_config[CONF_ZONE_TYPE]
zone_name = zone_config[CONF_ZONE_NAME]
zone_id = zone_config[CONF_ZONE_ID]
device = NessZoneBinarySensor(
zone_id=zone_id, name=zone_name, zone_type=zone_type
async_add_entities(
NessZoneBinarySensor(
zone_id=zone_config[CONF_ZONE_ID],
name=zone_config[CONF_ZONE_NAME],
zone_type=zone_config[CONF_ZONE_TYPE],
)
devices.append(device)
async_add_entities(devices)
for zone_config in configured_zones
)
class NessZoneBinarySensor(BinarySensorEntity):
@@ -52,12 +48,14 @@ class NessZoneBinarySensor(BinarySensorEntity):
_attr_should_poll = False
def __init__(self, zone_id, name, zone_type):
def __init__(
self, zone_id: int, name: str, zone_type: BinarySensorDeviceClass
) -> None:
"""Initialize the binary_sensor."""
self._zone_id = zone_id
self._name = name
self._type = zone_type
self._state = 0
self._attr_name = name
self._attr_device_class = zone_type
self._attr_is_on = False
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@@ -67,24 +65,9 @@ class NessZoneBinarySensor(BinarySensorEntity):
)
)
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def is_on(self):
"""Return true if sensor is on."""
return self._state == 1
@property
def device_class(self) -> BinarySensorDeviceClass:
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._type
@callback
def _handle_zone_change(self, data: ZoneChangedData):
def _handle_zone_change(self, data: ZoneChangedData) -> None:
"""Handle zone state update."""
if self._zone_id == data.zone_id:
self._state = data.state
self._attr_is_on = data.state
self.async_write_ha_state()

View File

@@ -225,7 +225,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
self._signal_thermostat_update()
@property
def preset_mode(self):
def preset_mode(self) -> str | None:
"""Preset that is active."""
return self._zone.get_preset()

View File

@@ -141,7 +141,7 @@ class NinaConfigFlow(ConfigFlow, domain=DOMAIN):
try:
self._all_region_codes_sorted = swap_key_value(
await nina.getAllRegionalCodes()
await nina.get_all_regional_codes()
)
except ApiError:
return self.async_abort(reason="no_fetch")
@@ -221,7 +221,7 @@ class OptionsFlowHandler(OptionsFlowWithReload):
try:
self._all_region_codes_sorted = swap_key_value(
await nina.getAllRegionalCodes()
await nina.get_all_regional_codes()
)
except ApiError:
return self.async_abort(reason="no_fetch")

View File

@@ -66,7 +66,7 @@ class NINADataUpdateCoordinator(
regions: dict[str, str] = config_entry.data[CONF_REGIONS]
for region in regions:
self._nina.addRegion(region)
self._nina.add_region(region)
super().__init__(
hass,
@@ -151,7 +151,7 @@ class NINADataUpdateCoordinator(
raw_warn.sent or "",
raw_warn.start or "",
raw_warn.expires or "",
raw_warn.isValid(),
raw_warn.is_valid,
)
warnings_for_regions.append(warning_data)

View File

@@ -8,6 +8,6 @@
"iot_class": "cloud_polling",
"loggers": ["pynina"],
"quality_scale": "bronze",
"requirements": ["pynina==0.3.6"],
"requirements": ["pynina==1.0.2"],
"single_config_entry": true
}

View File

@@ -47,10 +47,8 @@ rules:
test-coverage:
status: todo
comment: |
Patch the library instead of the HTTP requests
Create a shared fixture for the mock config entry
Use init_integration in tests
Evaluate the need of test_config_entry_not_ready
# Gold
devices: done

View File

@@ -154,7 +154,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity):
return nuheat_to_fahrenheit(self._target_temperature)
@property
def preset_mode(self):
def preset_mode(self) -> str:
"""Return current preset mode."""
return SCHEDULE_MODE_TO_PRESET_MODE_MAP.get(self._schedule_mode, PRESET_RUN)

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import logging
import threading
import time
from typing import Any
from nx584 import client as nx584_client
import requests
@@ -28,8 +29,7 @@ CONF_EXCLUDE_ZONES = "exclude_zones"
CONF_ZONE_TYPES = "zone_types"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = "5007"
DEFAULT_SSL = False
DEFAULT_PORT = 5007
ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: BINARY_SENSOR_DEVICE_CLASSES_SCHEMA})
@@ -53,10 +53,10 @@ def setup_platform(
) -> None:
"""Set up the NX584 binary sensor platform."""
host = config[CONF_HOST]
port = config[CONF_PORT]
exclude = config[CONF_EXCLUDE_ZONES]
zone_types = config[CONF_ZONE_TYPES]
host: str = config[CONF_HOST]
port: int = config[CONF_PORT]
exclude: list[int] = config[CONF_EXCLUDE_ZONES]
zone_types: dict[int, BinarySensorDeviceClass] = config[CONF_ZONE_TYPES]
try:
client = nx584_client.Client(f"http://{host}:{port}")
@@ -90,15 +90,12 @@ class NX584ZoneSensor(BinarySensorEntity):
_attr_should_poll = False
def __init__(self, zone, zone_type):
def __init__(
self, zone: dict[str, Any], zone_type: BinarySensorDeviceClass
) -> None:
"""Initialize the nx594 binary sensor."""
self._zone = zone
self._zone_type = zone_type
@property
def device_class(self) -> BinarySensorDeviceClass:
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
self._attr_device_class = zone_type
@property
def name(self):
@@ -112,7 +109,7 @@ class NX584ZoneSensor(BinarySensorEntity):
return self._zone["state"]
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return {
"zone_number": self._zone["number"],

View File

@@ -2,9 +2,10 @@
from __future__ import annotations
from datetime import timedelta
from datetime import datetime, timedelta
import logging
from operator import itemgetter
from typing import Any
import oasatelematics
import voluptuous as vol
@@ -55,9 +56,9 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the OASA Telematics sensor."""
name = config[CONF_NAME]
stop_id = config[CONF_STOP_ID]
route_id = config.get(CONF_ROUTE_ID)
name: str = config[CONF_NAME]
stop_id: str = config[CONF_STOP_ID]
route_id: str = config[CONF_ROUTE_ID]
data = OASATelematicsData(stop_id, route_id)
@@ -68,42 +69,31 @@ class OASATelematicsSensor(SensorEntity):
"""Implementation of the OASA Telematics sensor."""
_attr_attribution = "Data retrieved from telematics.oasa.gr"
_attr_device_class = SensorDeviceClass.TIMESTAMP
_attr_icon = "mdi:bus"
def __init__(self, data, stop_id, route_id, name):
def __init__(
self, data: OASATelematicsData, stop_id: str, route_id: str, name: str
) -> None:
"""Initialize the sensor."""
self.data = data
self._name = name
self._attr_name = name
self._stop_id = stop_id
self._route_id = route_id
self._name_data = self._times = self._state = None
self._name_data: dict[str, Any] | None = None
self._times: list[dict[str, Any]] | None = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def device_class(self) -> SensorDeviceClass:
"""Return the class of this sensor."""
return SensorDeviceClass.TIMESTAMP
@property
def native_value(self):
"""Return the state of the sensor."""
return self._state
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
params = {}
if self._times is not None:
next_arrival_data = self._times[0]
if ATTR_NEXT_ARRIVAL in next_arrival_data:
next_arrival = next_arrival_data[ATTR_NEXT_ARRIVAL]
next_arrival: datetime = next_arrival_data[ATTR_NEXT_ARRIVAL]
params.update({ATTR_NEXT_ARRIVAL: next_arrival.isoformat()})
if len(self._times) > 1:
second_next_arrival_time = self._times[1][ATTR_NEXT_ARRIVAL]
second_next_arrival_time: datetime = self._times[1][ATTR_NEXT_ARRIVAL]
if second_next_arrival_time is not None:
second_arrival = second_next_arrival_time
params.update(
@@ -115,12 +105,13 @@ class OASATelematicsSensor(SensorEntity):
ATTR_STOP_ID: self._stop_id,
}
)
params.update(
{
ATTR_ROUTE_NAME: self._name_data[ATTR_ROUTE_NAME],
ATTR_STOP_NAME: self._name_data[ATTR_STOP_NAME],
}
)
if self._name_data is not None:
params.update(
{
ATTR_ROUTE_NAME: self._name_data[ATTR_ROUTE_NAME],
ATTR_STOP_NAME: self._name_data[ATTR_STOP_NAME],
}
)
return {k: v for k, v in params.items() if v}
def update(self) -> None:
@@ -130,7 +121,7 @@ class OASATelematicsSensor(SensorEntity):
self._name_data = self.data.name_data
next_arrival_data = self._times[0]
if ATTR_NEXT_ARRIVAL in next_arrival_data:
self._state = next_arrival_data[ATTR_NEXT_ARRIVAL]
self._attr_native_value = next_arrival_data[ATTR_NEXT_ARRIVAL]
class OASATelematicsData:

View File

@@ -25,6 +25,7 @@ from homeassistant.core import (
SupportsResponse,
)
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
ServiceValidationError,
@@ -96,6 +97,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
response_format="url",
n=1,
)
except openai.AuthenticationError as err:
entry.async_start_reauth(hass)
raise HomeAssistantError("Authentication error") from err
except openai.OpenAIError as err:
raise HomeAssistantError(f"Error generating image: {err}") from err
@@ -179,7 +183,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
try:
response: Response = await client.responses.create(**model_args)
except openai.AuthenticationError as err:
entry.async_start_reauth(hass)
raise HomeAssistantError("Authentication error") from err
except openai.OpenAIError as err:
raise HomeAssistantError(f"Error generating content: {err}") from err
except FileNotFoundError as err:
@@ -245,8 +251,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> bo
try:
await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list)
except openai.AuthenticationError as err:
LOGGER.error("Invalid API key: %s", err)
return False
raise ConfigEntryAuthFailed(err) from err
except openai.OpenAIError as err:
raise ConfigEntryNotReady(err) from err
@@ -259,7 +264,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> bo
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> bool:
"""Unload OpenAI."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -280,7 +285,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
if not any(entry.version == 1 for entry in entries):
return
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
api_keys_entries: dict[str, tuple[OpenAIConfigEntry, bool]] = {}
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)

View File

@@ -10,7 +10,6 @@ from typing import TYPE_CHECKING
from openai.types.responses.response_output_item import ImageGenerationCall
from homeassistant.components import ai_task, conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -35,7 +34,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: OpenAIConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up AI Task entities."""

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Mapping
import json
import logging
from typing import Any
@@ -12,6 +13,7 @@ from voluptuous_openapi import convert
from homeassistant.components.zone import ENTITY_ID_HOME
from homeassistant.config_entries import (
SOURCE_REAUTH,
ConfigEntry,
ConfigEntryState,
ConfigFlow,
@@ -127,6 +129,10 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
return self.async_create_entry(
title="ChatGPT",
data=user_input,
@@ -157,6 +163,23 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
if not user_input:
return self.async_show_form(
step_id="reauth_confirm", data_schema=STEP_USER_DATA_SCHEMA
)
return await self.async_step_user(user_input)
@classmethod
@callback
def async_get_supported_subentry_types(

View File

@@ -89,6 +89,8 @@ UNSUPPORTED_EXTENDED_CACHE_RETENTION_MODELS: list[str] = [
"gpt-3.5",
"gpt-4-turbo",
"gpt-4o",
"gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-5-mini",
"gpt-5-nano",
]

View File

@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -9,6 +10,15 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::openai_conversation::config::step::user::data_description::api_key%]"
},
"description": "Reauthentication required. Please enter your updated API key."
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.16.3"]
"requirements": ["opower==0.16.4"]
}

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from proxmoxer import AuthenticationError, ProxmoxAPI
@@ -10,6 +11,7 @@ import requests.exceptions
from requests.exceptions import ConnectTimeout, SSLError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -18,26 +20,29 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .common import ProxmoxClient, call_api_container_vm, parse_api_container_vm
from .common import (
ProxmoxClient,
ResourceException,
call_api_container_vm,
parse_api_container_vm,
)
from .const import (
_LOGGER,
CONF_CONTAINERS,
CONF_NODE,
CONF_NODES,
CONF_REALM,
CONF_VMS,
COORDINATORS,
DEFAULT_PORT,
DEFAULT_REALM,
DEFAULT_VERIFY_SSL,
DOMAIN,
PROXMOX_CLIENTS,
TYPE_CONTAINER,
TYPE_VM,
UPDATE_INTERVAL,
@@ -45,6 +50,10 @@ from .const import (
PLATFORMS = [Platform.BINARY_SENSOR]
type ProxmoxConfigEntry = ConfigEntry[
dict[str, dict[str, dict[int, DataUpdateCoordinator[dict[str, Any] | None]]]]
]
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
@@ -84,109 +93,154 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the platform."""
hass.data.setdefault(DOMAIN, {})
"""Import the Proxmox configuration from YAML."""
if DOMAIN not in config:
return True
def build_client() -> ProxmoxAPI:
"""Build the Proxmox client connection."""
hass.data[PROXMOX_CLIENTS] = {}
for entry in config[DOMAIN]:
host = entry[CONF_HOST]
port = entry[CONF_PORT]
user = entry[CONF_USERNAME]
realm = entry[CONF_REALM]
password = entry[CONF_PASSWORD]
verify_ssl = entry[CONF_VERIFY_SSL]
hass.data[PROXMOX_CLIENTS][host] = None
try:
# Construct an API client with the given data for the given host
proxmox_client = ProxmoxClient(
host, port, user, realm, password, verify_ssl
)
proxmox_client.build_client()
except AuthenticationError:
_LOGGER.warning(
"Invalid credentials for proxmox instance %s:%d", host, port
)
continue
except SSLError:
_LOGGER.error(
(
"Unable to verify proxmox server SSL. "
'Try using "verify_ssl: false" for proxmox instance %s:%d'
),
host,
port,
)
continue
except ConnectTimeout:
_LOGGER.warning("Connection to host %s timed out during setup", host)
continue
except requests.exceptions.ConnectionError:
_LOGGER.warning("Host %s is not reachable", host)
continue
hass.data[PROXMOX_CLIENTS][host] = proxmox_client
await hass.async_add_executor_job(build_client)
coordinators: dict[
str, dict[str, dict[int, DataUpdateCoordinator[dict[str, Any] | None]]]
] = {}
hass.data[DOMAIN][COORDINATORS] = coordinators
# Create a coordinator for each vm/container
for host_config in config[DOMAIN]:
host_name = host_config["host"]
coordinators[host_name] = {}
proxmox_client = hass.data[PROXMOX_CLIENTS][host_name]
# Skip invalid hosts
if proxmox_client is None:
continue
proxmox = proxmox_client.get_api_client()
for node_config in host_config["nodes"]:
node_name = node_config["node"]
node_coordinators = coordinators[host_name][node_name] = {}
for vm_id in node_config["vms"]:
coordinator = create_coordinator_container_vm(
hass, proxmox, host_name, node_name, vm_id, TYPE_VM
)
# Fetch initial data
await coordinator.async_refresh()
node_coordinators[vm_id] = coordinator
for container_id in node_config["containers"]:
coordinator = create_coordinator_container_vm(
hass, proxmox, host_name, node_name, container_id, TYPE_CONTAINER
)
# Fetch initial data
await coordinator.async_refresh()
node_coordinators[container_id] = coordinator
for component in PLATFORMS:
await hass.async_create_task(
async_load_platform(hass, component, DOMAIN, {"config": config}, config)
)
hass.async_create_task(_async_setup(hass, config))
return True
def create_coordinator_container_vm(
async def _async_setup(hass: HomeAssistant, config: ConfigType) -> None:
for entry_config in config[DOMAIN]:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=entry_config,
)
if (
result.get("type") is FlowResultType.ABORT
and result.get("reason") != "already_configured"
):
ir.async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_{result.get('reason')}",
breaks_in_ha_version="2026.8.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key=f"deprecated_yaml_import_issue_{result.get('reason')}",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Proxmox VE",
},
)
return
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2026.8.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Proxmox VE",
},
)
async def async_setup_entry(hass: HomeAssistant, entry: ProxmoxConfigEntry) -> bool:
"""Set up a ProxmoxVE instance from a config entry."""
def build_client() -> ProxmoxClient:
"""Build and return the Proxmox client connection."""
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
user = entry.data[CONF_USERNAME]
realm = entry.data[CONF_REALM]
password = entry.data[CONF_PASSWORD]
verify_ssl = entry.data[CONF_VERIFY_SSL]
try:
client = ProxmoxClient(host, port, user, realm, password, verify_ssl)
client.build_client()
except AuthenticationError as ex:
raise ConfigEntryAuthFailed("Invalid credentials") from ex
except SSLError as ex:
raise ConfigEntryAuthFailed(
f"Unable to verify proxmox server SSL. Try using 'verify_ssl: false' for proxmox instance {host}:{port}"
) from ex
except ConnectTimeout as ex:
raise ConfigEntryNotReady("Connection timed out") from ex
except requests.exceptions.ConnectionError as ex:
raise ConfigEntryNotReady(f"Host {host} is not reachable: {ex}") from ex
else:
return client
proxmox_client = await hass.async_add_executor_job(build_client)
coordinators: dict[
str, dict[str, dict[int, DataUpdateCoordinator[dict[str, Any] | None]]]
] = {}
entry.runtime_data = coordinators
host_name = entry.data[CONF_HOST]
coordinators[host_name] = {}
proxmox: ProxmoxAPI = proxmox_client.get_api_client()
for node_config in entry.data[CONF_NODES]:
node_name = node_config[CONF_NODE]
node_coordinators = coordinators[host_name][node_name] = {}
try:
vms, containers = await hass.async_add_executor_job(
_get_vms_containers, proxmox, node_config
)
except (ResourceException, requests.exceptions.ConnectionError) as err:
LOGGER.error("Unable to get vms/containers for node %s: %s", node_name, err)
continue
for vm in vms:
coordinator = _create_coordinator_container_vm(
hass, entry, proxmox, host_name, node_name, vm["vmid"], TYPE_VM
)
await coordinator.async_config_entry_first_refresh()
node_coordinators[vm["vmid"]] = coordinator
for container in containers:
coordinator = _create_coordinator_container_vm(
hass,
entry,
proxmox,
host_name,
node_name,
container["vmid"],
TYPE_CONTAINER,
)
await coordinator.async_config_entry_first_refresh()
node_coordinators[container["vmid"]] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
def _get_vms_containers(
proxmox: ProxmoxAPI,
node_config: dict[str, Any],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""Get vms and containers for a node."""
vms = proxmox.nodes(node_config[CONF_NODE]).qemu.get()
containers = proxmox.nodes(node_config[CONF_NODE]).lxc.get()
assert vms is not None and containers is not None
return vms, containers
def _create_coordinator_container_vm(
hass: HomeAssistant,
entry: ProxmoxConfigEntry,
proxmox: ProxmoxAPI,
host_name: str,
node_name: str,
@@ -205,7 +259,7 @@ def create_coordinator_container_vm(
vm_status = await hass.async_add_executor_job(poll_api)
if vm_status is None:
_LOGGER.warning(
LOGGER.warning(
"Vm/Container %s unable to be found in node %s", vm_id, node_name
)
return None
@@ -214,9 +268,14 @@ def create_coordinator_container_vm(
return DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=None,
LOGGER,
config_entry=entry,
name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}",
update_method=async_update_data,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)
async def async_unload_entry(hass: HomeAssistant, entry: ProxmoxConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -2,55 +2,48 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import COORDINATORS, DOMAIN, PROXMOX_CLIENTS
from . import ProxmoxConfigEntry
from .const import CONF_CONTAINERS, CONF_NODE, CONF_NODES, CONF_VMS
from .entity import ProxmoxEntity
async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: ProxmoxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up binary sensors."""
if discovery_info is None:
return
sensors = []
for host_config in discovery_info["config"][DOMAIN]:
host_name = host_config["host"]
host_name_coordinators = hass.data[DOMAIN][COORDINATORS][host_name]
host_name = entry.data[CONF_HOST]
host_name_coordinators = entry.runtime_data[host_name]
if hass.data[PROXMOX_CLIENTS][host_name] is None:
continue
for node_config in entry.data[CONF_NODES]:
node_name = node_config[CONF_NODE]
for node_config in host_config["nodes"]:
node_name = node_config["node"]
for dev_id in node_config[CONF_VMS] + node_config[CONF_CONTAINERS]:
coordinator = host_name_coordinators[node_name][dev_id]
for dev_id in node_config["vms"] + node_config["containers"]:
coordinator = host_name_coordinators[node_name][dev_id]
if TYPE_CHECKING:
assert coordinator.data is not None
name = coordinator.data["name"]
sensor = create_binary_sensor(
coordinator, host_name, node_name, dev_id, name
)
sensors.append(sensor)
# unfound case
if (coordinator_data := coordinator.data) is None:
continue
name = coordinator_data["name"]
sensor = create_binary_sensor(
coordinator, host_name, node_name, dev_id, name
)
sensors.append(sensor)
add_entities(sensors)
async_add_entities(sensors)
def create_binary_sensor(

View File

@@ -0,0 +1,175 @@
"""Config flow for Proxmox VE integration."""
from __future__ import annotations
import logging
from typing import Any
from proxmoxer import AuthenticationError, ProxmoxAPI
import requests
from requests.exceptions import ConnectTimeout, SSLError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from .common import ResourceException
from .const import (
CONF_CONTAINERS,
CONF_NODE,
CONF_NODES,
CONF_REALM,
CONF_VMS,
DEFAULT_PORT,
DEFAULT_REALM,
DEFAULT_VERIFY_SSL,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_REALM, default=DEFAULT_REALM): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
}
)
def _sanitize_userid(data: dict[str, Any]) -> str:
"""Sanitize the user ID."""
return (
data[CONF_USERNAME]
if "@" in data[CONF_USERNAME]
else f"{data[CONF_USERNAME]}@{data[CONF_REALM]}"
)
def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
"""Validate the user input and fetch data (sync, for executor)."""
try:
client = ProxmoxAPI(
data[CONF_HOST],
port=data[CONF_PORT],
user=_sanitize_userid(data),
password=data[CONF_PASSWORD],
verify_ssl=data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
)
nodes = client.nodes.get()
except AuthenticationError as err:
raise ProxmoxAuthenticationError from err
except SSLError as err:
raise ProxmoxSSLError from err
except ConnectTimeout as err:
raise ProxmoxConnectTimeout from err
except (ResourceException, requests.exceptions.ConnectionError) as err:
raise ProxmoxNoNodesFound from err
_LOGGER.debug("Proxmox nodes: %s", nodes)
nodes_data: list[dict[str, Any]] = []
for node in nodes:
try:
vms = client.nodes(node["node"]).qemu.get()
containers = client.nodes(node["node"]).lxc.get()
except (ResourceException, requests.exceptions.ConnectionError) as err:
raise ProxmoxNoNodesFound from err
nodes_data.append(
{
CONF_NODE: node["node"],
CONF_VMS: [vm["vmid"] for vm in vms],
CONF_CONTAINERS: [container["vmid"] for container in containers],
}
)
_LOGGER.debug("Nodes with data: %s", nodes_data)
return nodes_data
class ProxmoxveConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Proxmox VE."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
proxmox_nodes: list[dict[str, Any]] = []
if user_input is not None:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
try:
proxmox_nodes = await self.hass.async_add_executor_job(
_get_nodes_data, user_input
)
except ProxmoxConnectTimeout:
errors["base"] = "connect_timeout"
except ProxmoxAuthenticationError:
errors["base"] = "invalid_auth"
except ProxmoxSSLError:
errors["base"] = "ssl_error"
except ProxmoxNoNodesFound:
errors["base"] = "no_nodes_found"
if not errors:
return self.async_create_entry(
title=user_input[CONF_HOST],
data={**user_input, CONF_NODES: proxmox_nodes},
)
return self.async_show_form(
step_id="user",
data_schema=CONFIG_SCHEMA,
errors=errors,
)
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Handle a flow initiated by configuration file."""
self._async_abort_entries_match({CONF_HOST: import_data[CONF_HOST]})
try:
proxmox_nodes = await self.hass.async_add_executor_job(
_get_nodes_data, import_data
)
except ProxmoxConnectTimeout:
return self.async_abort(reason="connect_timeout")
except ProxmoxAuthenticationError:
return self.async_abort(reason="invalid_auth")
except ProxmoxSSLError:
return self.async_abort(reason="ssl_error")
except ProxmoxNoNodesFound:
return self.async_abort(reason="no_nodes_found")
return self.async_create_entry(
title=import_data[CONF_HOST],
data={**import_data, CONF_NODES: proxmox_nodes},
)
class ProxmoxNoNodesFound(HomeAssistantError):
"""Error to indicate no nodes found."""
class ProxmoxConnectTimeout(HomeAssistantError):
"""Error to indicate a connection timeout."""
class ProxmoxSSLError(HomeAssistantError):
"""Error to indicate an SSL error."""
class ProxmoxAuthenticationError(HomeAssistantError):
"""Error to indicate an authentication error."""

View File

@@ -1,16 +1,12 @@
"""Constants for ProxmoxVE."""
import logging
DOMAIN = "proxmoxve"
PROXMOX_CLIENTS = "proxmox_clients"
CONF_REALM = "realm"
CONF_NODE = "node"
CONF_NODES = "nodes"
CONF_VMS = "vms"
CONF_CONTAINERS = "containers"
COORDINATORS = "coordinators"
DEFAULT_PORT = 8006
DEFAULT_REALM = "pam"
@@ -18,5 +14,3 @@ DEFAULT_VERIFY_SSL = True
TYPE_VM = 0
TYPE_CONTAINER = 1
UPDATE_INTERVAL = 60
_LOGGER = logging.getLogger(__package__)

View File

@@ -1,8 +1,10 @@
{
"domain": "proxmoxve",
"name": "Proxmox VE",
"codeowners": ["@jhollowe", "@Corbeno"],
"codeowners": ["@jhollowe", "@Corbeno", "@erwindouna"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/proxmoxve",
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["proxmoxer"],
"quality_scale": "legacy",

View File

@@ -0,0 +1,46 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "Cannot connect to Proxmox VE server",
"connect_timeout": "[%key:common::config_flow::error::timeout_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"no_nodes_found": "No active nodes found",
"ssl_error": "SSL check failed. Check the SSL settings"
},
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]",
"realm": "Realm",
"username": "[%key:common::config_flow::data::username%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"description": "Enter your Proxmox VE server details to set up the integration.",
"title": "Connect to Proxmox VE"
}
}
},
"issues": {
"deprecated_yaml_import_issue_connect_timeout": {
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, a connection timeout occurred. Please correct your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.",
"title": "The {integration_title} YAML configuration is being removed"
},
"deprecated_yaml_import_issue_invalid_auth": {
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, invalid authentication details were found. Please correct your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.",
"title": "[%key:component::proxmoxve::issues::deprecated_yaml_import_issue_connect_timeout::title%]"
},
"deprecated_yaml_import_issue_no_nodes_found": {
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, no active nodes were found on the Proxmox VE server. Please correct your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.",
"title": "[%key:component::proxmoxve::issues::deprecated_yaml_import_issue_connect_timeout::title%]"
},
"deprecated_yaml_import_issue_ssl_error": {
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, an SSL error occurred. Please correct your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.",
"title": "[%key:component::proxmoxve::issues::deprecated_yaml_import_issue_connect_timeout::title%]"
}
}
}

View File

@@ -24,9 +24,9 @@ from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
from .const import DATA_QUIKSWITCH, DOMAIN
DOMAIN = "qwikswitch"
_LOGGER = logging.getLogger(__name__)
CONF_DIMMER_ADJUST = "dimmer_adjust"
CONF_BUTTON_EVENTS = "button_events"
@@ -96,7 +96,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if not await qsusb.update_from_devices():
return False
hass.data[DOMAIN] = qsusb
hass.data[DATA_QUIKSWITCH] = qsusb
comps: dict[Platform, list] = {
Platform.SWITCH: [],
@@ -168,7 +168,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@callback
def async_stop(_):
"""Stop the listener."""
hass.data[DOMAIN].stop()
hass.data[DATA_QUIKSWITCH].stop()
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop)

Some files were not shown because too many files have changed in this diff Show More