Compare commits

..

1 Commits

Author SHA1 Message Date
ludeeus 374cb0e69d Add host and add-on resource usage to support package download 2026-01-16 07:45:13 +00:00
602 changed files with 18395 additions and 30355 deletions
-1
View File
@@ -91,7 +91,6 @@ 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/**
+9 -9
View File
@@ -30,10 +30,10 @@ jobs:
architectures: ${{ env.ARCHITECTURES }}
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -96,7 +96,7 @@ jobs:
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
@@ -122,7 +122,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -273,7 +273,7 @@ jobs:
- green
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set build additional args
run: |
@@ -311,7 +311,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
@@ -474,10 +474,10 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -519,7 +519,7 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
+14 -11
View File
@@ -97,7 +97,7 @@ jobs:
steps:
- &checkout
name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Generate partial Python venv restore key
id: generate_python_cache_key
run: |
@@ -247,11 +247,17 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- *checkout
- name: Register problem matchers
- name: Register yamllint problem matcher
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
@@ -297,7 +303,7 @@ jobs:
- &setup-python-matrix
name: Set up Python ${{ matrix.python-version }}
id: python
uses: &actions-setup-python actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: &actions-setup-python actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -310,7 +316,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: &actions-cache actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
key: &key-python-venv >-
@@ -374,7 +380,7 @@ jobs:
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: &actions-cache-save actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: &actions-cache-save actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: *path-apt-cache
key: *key-apt-cache
@@ -425,7 +431,7 @@ jobs:
steps:
- &cache-restore-apt
name: Restore apt cache
uses: &actions-cache-restore actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: *path-apt-cache
fail-on-cache-miss: true
@@ -1187,8 +1193,6 @@ jobs:
- pytest-postgres
- pytest-mariadb
timeout-minutes: 10
permissions:
id-token: write
# codecov/test-results-action currently doesn't support tokenless uploads
# therefore we can't run it on forks
if: |
@@ -1200,9 +1204,8 @@ jobs:
with:
pattern: test-results-*
- name: Upload test results to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1
with:
report_type: test_results
fail_ci_if_error: true
verbose: true
use_oidc: true
token: ${{ secrets.CODECOV_TOKEN }}
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
@@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@a6101c89c6feaecc585efdd8d461f18bb7896f20 # v2.0.5
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o
system-prompt: |
@@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@a6101c89c6feaecc585efdd8d461f18bb7896f20 # v2.0.5
uses: actions/ai-inference@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
with:
model: openai/gpt-4o-mini
system-prompt: |
@@ -4,7 +4,7 @@
"owner": "check-executables-have-shebangs",
"pattern": [
{
"regexp": "^(.+):\\s(marked executable but has no \\(or invalid\\) shebang!.*)$",
"regexp": "^(.+):\\s(.+)$",
"file": 1,
"message": 2
}
+2 -2
View File
@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
+2 -2
View File
@@ -31,11 +31,11 @@ jobs:
steps:
- &checkout
name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.13
rev: v0.13.0
hooks:
- id: ruff-check
args:
-1
View File
@@ -455,7 +455,6 @@ 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.*
Generated
+3 -4
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 @zachdeibert
/tests/components/minecraft_server/ @elmurato @zachdeibert
/homeassistant/components/minecraft_server/ @elmurato
/tests/components/minecraft_server/ @elmurato
/homeassistant/components/minio/ @tkislan
/tests/components/minio/ @tkislan
/homeassistant/components/moat/ @bdraco
@@ -1273,8 +1273,7 @@ build.json @home-assistant/supervisor
/tests/components/prosegur/ @dgomes
/homeassistant/components/proximity/ @mib1185
/tests/components/proximity/ @mib1185
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
/tests/components/proxmoxve/ @jhollowe @Corbeno @erwindouna
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
/homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45
/homeassistant/components/pterodactyl/ @elmurato
Generated
+1 -1
View File
@@ -30,7 +30,7 @@ RUN \
# Verify go2rtc can be executed
go2rtc --version \
# Install uv
&& pip3 install uv==0.9.26
&& pip3 install uv==0.9.17
WORKDIR /usr/src
+1 -1
View File
@@ -52,7 +52,7 @@ class AdGuardHomeEntity(Entity):
def device_info(self) -> DeviceInfo:
"""Return device information about this AdGuard Home instance."""
if self._entry.source == SOURCE_HASSIO:
config_url = "homeassistant://app/a0d7b954_adguard"
config_url = "homeassistant://hassio/ingress/a0d7b954_adguard"
elif self.adguard.tls:
config_url = f"https://{self.adguard.host}:{self.adguard.port}"
else:
@@ -8,15 +8,12 @@ from advantage_air import ApiError, advantage_air
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
from .const import ADVANTAGE_AIR_RETRY
from .models import AdvantageAirData
from .services import async_setup_services
type AdvantageAirDataConfigEntry = ConfigEntry[AdvantageAirData]
@@ -35,14 +32,6 @@ PLATFORMS = [
_LOGGER = logging.getLogger(__name__)
REQUEST_REFRESH_DELAY = 0.5
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(
hass: HomeAssistant, entry: AdvantageAirDataConfigEntry
@@ -5,6 +5,8 @@ from __future__ import annotations
from decimal import Decimal
from typing import Any
import voluptuous as vol
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -12,6 +14,7 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
@@ -21,6 +24,7 @@ from .models import AdvantageAirData
ADVANTAGE_AIR_SET_COUNTDOWN_VALUE = "minutes"
ADVANTAGE_AIR_SET_COUNTDOWN_UNIT = "min"
ADVANTAGE_AIR_SERVICE_SET_TIME_TO = "set_time_to"
PARALLEL_UPDATES = 0
@@ -49,6 +53,13 @@ async def async_setup_entry(
entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key))
async_add_entities(entities)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
ADVANTAGE_AIR_SERVICE_SET_TIME_TO,
{vol.Required("minutes"): cv.positive_int},
"set_time_to",
)
class AdvantageAirTimeTo(AdvantageAirAcEntity, SensorEntity):
"""Representation of Advantage Air timer control."""
@@ -1,27 +0,0 @@
"""Services for Advantage Air integration."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN
ADVANTAGE_AIR_SERVICE_SET_TIME_TO = "set_time_to"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
ADVANTAGE_AIR_SERVICE_SET_TIME_TO,
entity_domain=SENSOR_DOMAIN,
schema={vol.Required("minutes"): cv.positive_int},
func="set_time_to",
)
@@ -12,7 +12,6 @@ PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
]
@@ -63,11 +63,6 @@ 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."""
@@ -24,6 +24,8 @@ 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))
@@ -9,14 +9,6 @@
"hysteresis_band": {
"default": "mdi:delta"
}
},
"switch": {
"actuator_exercise_disabled": {
"default": "mdi:valve"
},
"child_lock": {
"default": "mdi:lock"
}
}
}
}
@@ -85,14 +85,6 @@
"heating_uptime": {
"name": "Heating uptime"
}
},
"switch": {
"actuator_exercise_disabled": {
"name": "Actuator exercise disabled"
},
"child_lock": {
"name": "Child lock"
}
}
},
"exceptions": {
@@ -113,12 +105,6 @@
},
"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}."
}
}
}
-118
View File
@@ -1,118 +0,0 @@
"""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()
@@ -14,7 +14,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed"
"name": "If an alarm is armed"
},
"is_armed_away": {
"description": "Tests if one or more alarms are armed in away mode.",
@@ -24,7 +24,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed away"
"name": "If an alarm is armed away"
},
"is_armed_home": {
"description": "Tests if one or more alarms are armed in home mode.",
@@ -34,7 +34,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed home"
"name": "If an alarm is armed home"
},
"is_armed_night": {
"description": "Tests if one or more alarms are armed in night mode.",
@@ -44,7 +44,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed night"
"name": "If an alarm is armed night"
},
"is_armed_vacation": {
"description": "Tests if one or more alarms are armed in vacation mode.",
@@ -54,7 +54,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is armed vacation"
"name": "If an alarm is armed vacation"
},
"is_disarmed": {
"description": "Tests if one or more alarms are disarmed.",
@@ -64,7 +64,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is disarmed"
"name": "If an alarm is disarmed"
},
"is_triggered": {
"description": "Tests if one or more alarms are triggered.",
@@ -74,7 +74,7 @@
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
}
},
"name": "Alarm is triggered"
"name": "If an alarm is triggered"
}
},
"device_automation": {
+47 -27
View File
@@ -5,14 +5,9 @@ from __future__ import annotations
import asyncio
import logging
from random import randrange
import sys
from typing import Any, cast
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -29,7 +24,11 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -43,6 +42,18 @@ from .const import (
SIGNAL_DISCONNECTED,
)
if sys.version_info < (3, 14):
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
else:
class DeviceListener:
"""Dummy class."""
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME_TV = "Apple TV"
@@ -51,27 +62,32 @@ DEFAULT_NAME_HP = "HomePod"
BACKOFF_TIME_LOWER_LIMIT = 15 # seconds
BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes
PLATFORMS = [Platform.BINARY_SENSOR, Platform.MEDIA_PLAYER, Platform.REMOTE]
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
if sys.version_info < (3, 14):
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
else:
AUTH_EXCEPTIONS = ()
CONNECTION_TIMEOUT_EXCEPTIONS = ()
DEVICE_EXCEPTIONS = ()
type AppleTvConfigEntry = ConfigEntry[AppleTVManager]
@@ -79,6 +95,10 @@ type AppleTvConfigEntry = ConfigEntry[AppleTVManager]
async def async_setup_entry(hass: HomeAssistant, entry: AppleTvConfigEntry) -> bool:
"""Set up a config entry for Apple TV."""
if sys.version_info >= (3, 14):
raise HomeAssistantError(
"Apple TV is not supported on Python 3.14. Please use Python 3.13."
)
manager = AppleTVManager(hass, entry)
if manager.is_on:
@@ -1,63 +0,0 @@
"""Binary sensor support for Apple TV."""
from __future__ import annotations
from pyatv.const import KeyboardFocusState
from pyatv.interface import AppleTV, KeyboardListener
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AppleTvConfigEntry
from .entity import AppleTVEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AppleTvConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Load Apple TV binary sensor based on a config entry."""
# apple_tv config entries always have a unique id
assert config_entry.unique_id is not None
name: str = config_entry.data[CONF_NAME]
manager = config_entry.runtime_data
async_add_entities([AppleTVKeyboardFocused(name, config_entry.unique_id, manager)])
class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener):
"""Binary sensor for Text input focused."""
_attr_translation_key = "keyboard_focused"
_attr_available = True
@callback
def async_device_connected(self, atv: AppleTV) -> None:
"""Handle when connection is made to device."""
self._attr_available = True
# Listen to keyboard updates
atv.keyboard.listener = self
# Set initial state based on current focus state
self._update_state(atv.keyboard.text_focus_state == KeyboardFocusState.Focused)
@callback
def async_device_disconnected(self) -> None:
"""Handle when connection was lost to device."""
self._attr_available = False
self._update_state(False)
def focusstate_update(
self, old_state: KeyboardFocusState, new_state: KeyboardFocusState
) -> None:
"""Update keyboard state when it changes.
This is a callback function from pyatv.interface.KeyboardListener.
"""
self._update_state(new_state == KeyboardFocusState.Focused)
def _update_state(self, new_state: bool) -> None:
"""Update and report."""
self._attr_is_on = new_state
self.async_write_ha_state()
@@ -18,6 +18,7 @@ class AppleTVEntity(Entity):
_attr_should_poll = False
_attr_has_entity_name = True
_attr_name = None
atv: AppleTVInterface | None = None
def __init__(self, name: str, identifier: str, manager: AppleTVManager) -> None:
@@ -1,12 +0,0 @@
{
"entity": {
"binary_sensor": {
"keyboard_focused": {
"default": "mdi:keyboard",
"state": {
"off": "mdi:keyboard-off"
}
}
}
}
}
@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.17.0"],
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",
@@ -115,7 +115,6 @@ class AppleTvMediaPlayer(
"""Representation of an Apple TV media player."""
_attr_supported_features = SUPPORT_APPLE_TV
_attr_name = None
def __init__(self, name: str, identifier: str, manager: AppleTVManager) -> None:
"""Initialize the Apple TV media player."""
@@ -240,15 +239,6 @@ class AppleTvMediaPlayer(
"""
self.async_write_ha_state()
@callback
def volume_device_update(
self, output_device: OutputDevice, old_level: float, new_level: float
) -> None:
"""Output device volume was updated.
This is a callback function from pyatv.interface.AudioListener.
"""
@callback
def outputdevices_update(
self, old_devices: list[OutputDevice], new_devices: list[OutputDevice]
@@ -51,8 +51,6 @@ async def async_setup_entry(
class AppleTVRemote(AppleTVEntity, RemoteEntity):
"""Device that sends commands to an Apple TV."""
_attr_name = None
@property
def is_on(self) -> bool:
"""Return true if device is on."""
@@ -62,13 +62,6 @@
}
}
},
"entity": {
"binary_sensor": {
"keyboard_focused": {
"name": "Keyboard focus"
}
}
},
"options": {
"step": {
"init": {
-21
View File
@@ -2,35 +2,14 @@
from __future__ import annotations
import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import ArveConfigEntry, ArveCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_migrate_entry(hass: HomeAssistant, entry: ArveConfigEntry) -> bool:
"""Migrate entry."""
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
if entry.version == 1:
# 1 -> 1.2: Unique ID from integer to string
if entry.minor_version == 1:
minor_version = 2
hass.config_entries.async_update_entry(
entry, unique_id=str(entry.unique_id), minor_version=minor_version
)
_LOGGER.debug("Migration successful")
return True
async def async_setup_entry(hass: HomeAssistant, entry: ArveConfigEntry) -> bool:
"""Set up Arve from a config entry."""
+1 -4
View File
@@ -19,9 +19,6 @@ _LOGGER = logging.getLogger(__name__)
class ArveConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Arve."""
VERSION = 1
MINOR_VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -38,7 +35,7 @@ class ArveConfigFlowHandler(ConfigFlow, domain=DOMAIN):
except ArveConnectionError:
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(str(customer.customerId))
await self.async_set_unique_id(customer.customerId)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="Arve",
@@ -14,7 +14,7 @@
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "Satellite is idle"
"name": "If a satellite is idle"
},
"is_listening": {
"description": "Tests if one or more Assist satellites are listening.",
@@ -24,7 +24,7 @@
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "Satellite is listening"
"name": "If a satellite is listening"
},
"is_processing": {
"description": "Tests if one or more Assist satellites are processing.",
@@ -34,7 +34,7 @@
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "Satellite is processing"
"name": "If a satellite is processing"
},
"is_responding": {
"description": "Tests if one or more Assist satellites are responding.",
@@ -44,7 +44,7 @@
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
}
},
"name": "Satellite is responding"
"name": "If a satellite is responding"
}
},
"entity_component": {
+16 -34
View File
@@ -56,7 +56,7 @@ from homeassistant.core import (
valid_entity_id,
)
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
from homeassistant.helpers import condition as condition_helper, config_validation as cv
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.issue_registry import (
@@ -125,10 +125,8 @@ NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
_EXPERIMENTAL_CONDITION_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"device_tracker",
"fan",
"light",
"siren",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
@@ -555,7 +553,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
automation_id: str | None,
name: str,
trigger_config: list[ConfigType],
condition: IfAction | None,
cond_func: IfAction | None,
action_script: Script,
initial_state: bool | None,
variables: ScriptVariables | None,
@@ -568,7 +566,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
self._attr_name = name
self._trigger_config = trigger_config
self._async_detach_triggers: CALLBACK_TYPE | None = None
self._condition = condition
self._cond_func = cond_func
self.action_script = action_script
self.action_script.change_listener = self.async_write_ha_state
self._initial_state = initial_state
@@ -603,12 +601,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced labels."""
referenced = self.action_script.referenced_labels
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_targets(
conf, ATTR_LABEL_ID
)
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
return referenced
@@ -618,12 +610,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced floors."""
referenced = self.action_script.referenced_floors
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_targets(
conf, ATTR_FLOOR_ID
)
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
return referenced
@@ -633,10 +619,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced areas."""
referenced = self.action_script.referenced_areas
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_targets(conf, ATTR_AREA_ID)
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
return referenced
@@ -653,9 +635,9 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced devices."""
referenced = self.action_script.referenced_devices
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_devices(conf)
if self._cond_func is not None:
for conf in self._cond_func.config:
referenced |= condition.async_extract_devices(conf)
for conf in self._trigger_config:
referenced |= set(_trigger_extract_devices(conf))
@@ -667,9 +649,9 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return a set of referenced entities."""
referenced = self.action_script.referenced_entities
if self._condition is not None:
for conf in self._condition.config:
referenced |= condition_helper.async_extract_entities(conf)
if self._cond_func is not None:
for conf in self._cond_func.config:
referenced |= condition.async_extract_entities(conf)
for conf in self._trigger_config:
for entity_id in _trigger_extract_entities(conf):
@@ -789,8 +771,8 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
if (
not skip_condition
and self._condition is not None
and not self._condition(variables)
and self._cond_func is not None
and not self._cond_func(variables)
):
self._logger.debug(
"Conditions not met, aborting automation. Condition summary: %s",
@@ -1052,12 +1034,12 @@ async def _create_automation_entities(
)
if CONF_CONDITIONS in config_block:
condition = await _async_process_if(hass, name, config_block)
cond_func = await _async_process_if(hass, name, config_block)
if condition is None:
if cond_func is None:
continue
else:
condition = None
cond_func = None
# Add trigger variables to variables
variables = None
@@ -1075,7 +1057,7 @@ async def _create_automation_entities(
automation_id,
name,
config_block[CONF_TRIGGERS],
condition,
cond_func,
action_script,
initial_state,
variables,
@@ -1217,7 +1199,7 @@ async def _async_process_if(
if_configs = config[CONF_CONDITIONS]
try:
if_action = await condition_helper.async_conditions_from_config(
if_action = await condition.async_conditions_from_config(
hass, if_configs, LOGGER, name
)
except HomeAssistantError as ex:
@@ -17,12 +17,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MODEL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers import device_registry as dr
from homeassistant.util.ssl import get_default_context
from .const import DOMAIN
from .services import async_setup_services
from .websocket import BeoWebsocket
@@ -43,14 +41,6 @@ PLATFORMS = [
Platform.SENSOR,
]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the component."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
"""Set up from a config entry."""
@@ -38,6 +38,7 @@ from mozart_api.models import (
VolumeState,
)
from mozart_api.mozart_client import MozartClient, get_highest_resolution_artwork
import voluptuous as vol
from homeassistant.components import media_source
from homeassistant.components.media_player import (
@@ -55,10 +56,17 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
async_get_current_platform,
)
from homeassistant.util.dt import utcnow
from . import BeoConfigEntry
@@ -66,6 +74,7 @@ from .const import (
BEO_REPEAT_FROM_HA,
BEO_REPEAT_TO_HA,
BEO_STATES,
BEOLINK_JOIN_SOURCES,
BEOLINK_JOIN_SOURCES_TO_UPPER,
CONF_BEOLINK_JID,
CONNECTION_STATUS,
@@ -120,6 +129,61 @@ async def async_setup_entry(
update_before_add=True,
)
# Register actions.
platform = async_get_current_platform()
jid_regex = vol.Match(
r"(^\d{4})[.](\d{7})[.](\d{8})(@products\.bang-olufsen\.com)$"
)
platform.async_register_entity_service(
name="beolink_join",
schema={
vol.Optional("beolink_jid"): jid_regex,
vol.Optional("source_id"): vol.In(BEOLINK_JOIN_SOURCES),
},
func="async_beolink_join",
)
platform.async_register_entity_service(
name="beolink_expand",
schema={
vol.Exclusive("all_discovered", "devices", ""): cv.boolean,
vol.Exclusive(
"beolink_jids",
"devices",
"Define either specific Beolink JIDs or all discovered",
): vol.All(
cv.ensure_list,
[jid_regex],
),
},
func="async_beolink_expand",
)
platform.async_register_entity_service(
name="beolink_unexpand",
schema={
vol.Required("beolink_jids"): vol.All(
cv.ensure_list,
[jid_regex],
),
},
func="async_beolink_unexpand",
)
platform.async_register_entity_service(
name="beolink_leave",
schema=None,
func="async_beolink_leave",
)
platform.async_register_entity_service(
name="beolink_allstandby",
schema=None,
func="async_beolink_allstandby",
)
class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
"""Representation of a media player."""
@@ -1,83 +0,0 @@
"""Services for Bang & Olufsen integration."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, service
from .const import BEOLINK_JOIN_SOURCES, DOMAIN
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
jid_regex = vol.Match(
r"(^\d{4})[.](\d{7})[.](\d{8})(@products\.bang-olufsen\.com)$"
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
"beolink_join",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema={
vol.Optional("beolink_jid"): jid_regex,
vol.Optional("source_id"): vol.In(BEOLINK_JOIN_SOURCES),
},
func="async_beolink_join",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
"beolink_expand",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema={
vol.Exclusive("all_discovered", "devices", ""): cv.boolean,
vol.Exclusive(
"beolink_jids",
"devices",
"Define either specific Beolink JIDs or all discovered",
): vol.All(
cv.ensure_list,
[jid_regex],
),
},
func="async_beolink_expand",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
"beolink_unexpand",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema={
vol.Required("beolink_jids"): vol.All(
cv.ensure_list,
[jid_regex],
),
},
func="async_beolink_unexpand",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
"beolink_leave",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema=None,
func="async_beolink_leave",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
"beolink_allstandby",
entity_domain=MEDIA_PLAYER_DOMAIN,
schema=None,
func="async_beolink_allstandby",
)
@@ -85,9 +85,9 @@
}
},
"moving": {
"default": "mdi:octagon",
"default": "mdi:arrow-right",
"state": {
"on": "mdi:arrow-right"
"on": "mdi:octagon"
}
},
"occupancy": {
+105
View File
@@ -19,6 +19,7 @@ import attr
from hass_nabucasa import AlreadyConnectedError, Cloud, auth
from hass_nabucasa.const import STATE_DISCONNECTED
from hass_nabucasa.voice_data import TTS_VOICES
import psutil_home_assistant as ha_psutil
import voluptuous as vol
from homeassistant.components import websocket_api
@@ -27,6 +28,7 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
)
from homeassistant.components.google_assistant import helpers as google_helpers
from homeassistant.components.hassio import get_addons_stats, get_supervisor_info
from homeassistant.components.homeassistant import exposed_entities
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
from homeassistant.components.http.data_validator import RequestDataValidator
@@ -37,6 +39,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.hassio import is_hassio
from homeassistant.loader import (
async_get_custom_components,
async_get_loaded_integration,
@@ -571,6 +574,11 @@ class DownloadSupportPackageView(HomeAssistantView):
"</details>\n\n"
)
markdown += await self._get_host_resources_markdown(hass)
if is_hassio(hass):
markdown += await self._get_addon_resources_markdown(hass)
log_handler = hass.data[DATA_CLOUD_LOG_HANDLER]
logs = "\n".join(await log_handler.get_logs(hass))
markdown += (
@@ -584,6 +592,103 @@ class DownloadSupportPackageView(HomeAssistantView):
return markdown
async def _get_host_resources_markdown(self, hass: HomeAssistant) -> str:
"""Get host resource usage markdown using psutil."""
def _collect_system_stats() -> dict[str, Any]:
"""Collect system stats."""
psutil_wrapper = ha_psutil.PsutilWrapper()
psutil_mod = psutil_wrapper.psutil
cpu_percent = psutil_mod.cpu_percent(interval=0.1)
memory = psutil_mod.virtual_memory()
disk = psutil_mod.disk_usage("/")
return {
"cpu_percent": cpu_percent,
"memory_total": memory.total,
"memory_used": memory.used,
"memory_available": memory.available,
"memory_percent": memory.percent,
"disk_total": disk.total,
"disk_used": disk.used,
"disk_free": disk.free,
"disk_percent": disk.percent,
}
markdown = ""
try:
stats = await hass.async_add_executor_job(_collect_system_stats)
markdown += "## Host resource usage\n\n"
markdown += "Resource | Value\n"
markdown += "--- | ---\n"
markdown += f"CPU usage | {stats['cpu_percent']}%\n"
memory_total_gb = round(stats["memory_total"] / (1024**3), 2)
memory_used_gb = round(stats["memory_used"] / (1024**3), 2)
memory_available_gb = round(stats["memory_available"] / (1024**3), 2)
markdown += f"Memory total | {memory_total_gb} GB\n"
markdown += (
f"Memory used | {memory_used_gb} GB ({stats['memory_percent']}%)\n"
)
markdown += f"Memory available | {memory_available_gb} GB\n"
disk_total_gb = round(stats["disk_total"] / (1024**3), 2)
disk_used_gb = round(stats["disk_used"] / (1024**3), 2)
disk_free_gb = round(stats["disk_free"] / (1024**3), 2)
markdown += f"Disk total | {disk_total_gb} GB\n"
markdown += f"Disk used | {disk_used_gb} GB ({stats['disk_percent']}%)\n"
markdown += f"Disk free | {disk_free_gb} GB\n"
markdown += "\n"
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package generation
markdown += "## Host resource usage\n\n"
markdown += "Unable to collect host resource information\n\n"
return markdown
async def _get_addon_resources_markdown(self, hass: HomeAssistant) -> str:
"""Get add-on resource usage markdown for hassio."""
markdown = ""
try:
supervisor_info = get_supervisor_info(hass) or {}
addons_stats = get_addons_stats(hass)
addons = supervisor_info.get("addons", [])
if addons:
markdown += "## Add-on resource usage\n\n"
markdown += "<details><summary>Add-on resources</summary>\n\n"
markdown += "Add-on | Version | State | CPU | Memory\n"
markdown += "--- | --- | --- | --- | ---\n"
for addon in addons:
slug = addon.get("slug", "unknown")
name = addon.get("name", slug)
version = addon.get("version", "unknown")
state = addon.get("state", "unknown")
addon_stats = addons_stats.get(slug, {})
cpu = addon_stats.get("cpu_percent")
memory = addon_stats.get("memory_percent")
cpu_str = f"{cpu}%" if cpu is not None else "N/A"
memory_str = f"{memory}%" if memory is not None else "N/A"
markdown += (
f"{name} | {version} | {state} | {cpu_str} | {memory_str}\n"
)
markdown += "\n</details>\n\n"
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package generation
markdown += "## Add-on resource usage\n\n"
markdown += "Unable to collect add-on resource information\n\n"
return markdown
async def get(self, request: web.Request) -> web.Response:
"""Download support package file."""
+3 -2
View File
@@ -5,7 +5,8 @@
"alexa",
"assist_pipeline",
"backup",
"google_assistant"
"google_assistant",
"hassio"
],
"codeowners": ["@home-assistant/cloud"],
"dependencies": ["auth", "http", "repairs", "webhook", "web_rtc"],
@@ -13,6 +14,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.11.0"],
"requirements": ["hass-nabucasa==1.9.0", "psutil-home-assistant==0.0.1"],
"single_config_entry": true
}
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["compit"],
"quality_scale": "bronze",
"requirements": ["compit-inext-api==0.4.2"]
"requirements": ["compit-inext-api==0.3.4"]
}
@@ -49,11 +49,11 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Concord232 alarm control panel platform."""
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]
name = config[CONF_NAME]
code = config.get(CONF_CODE)
mode = config[CONF_MODE]
host = config[CONF_HOST]
port = config[CONF_PORT]
url = f"http://{host}:{port}"
@@ -72,7 +72,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY
)
def __init__(self, url: str, name: str, code: str | None, mode: str) -> None:
def __init__(self, url, name, code, mode):
"""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: str | None, state: AlarmControlPanelState) -> bool:
def _validate_code(self, code, state):
"""Validate given code."""
if self._code is None:
return True
@@ -4,7 +4,6 @@ from __future__ import annotations
import datetime
import logging
from typing import Any
from concord232 import client as concord232_client
import requests
@@ -30,7 +29,8 @@ CONF_ZONE_TYPES = "zone_types"
DEFAULT_HOST = "localhost"
DEFAULT_NAME = "Alarm"
DEFAULT_PORT = 5007
DEFAULT_PORT = "5007"
DEFAULT_SSL = False
SCAN_INTERVAL = datetime.timedelta(seconds=10)
@@ -56,10 +56,10 @@ def setup_platform(
) -> None:
"""Set up the Concord232 binary sensor platform."""
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]
host = config[CONF_HOST]
port = config[CONF_PORT]
exclude = config[CONF_EXCLUDE_ZONES]
zone_types = config[CONF_ZONE_TYPES]
sensors = []
try:
@@ -84,6 +84,7 @@ def setup_platform(
if zone["number"] not in exclude:
sensors.append(
Concord232ZoneSensor(
hass,
client,
zone,
zone_types.get(zone["number"], get_opening_type(zone)),
@@ -109,25 +110,26 @@ def get_opening_type(zone):
class Concord232ZoneSensor(BinarySensorEntity):
"""Representation of a Concord232 zone as a sensor."""
def __init__(
self,
client: concord232_client.Client,
zone: dict[str, Any],
zone_type: BinarySensorDeviceClass,
) -> None:
def __init__(self, hass, client, zone, zone_type):
"""Initialize the Concord232 binary sensor."""
self._hass = hass
self._client = client
self._zone = zone
self._number = zone["number"]
self._attr_device_class = zone_type
self._zone_type = zone_type
@property
def name(self) -> str:
def device_class(self) -> BinarySensorDeviceClass:
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
@property
def name(self):
"""Return the name of the binary sensor."""
return self._zone["name"]
@property
def is_on(self) -> bool:
def is_on(self):
"""Return true if the binary sensor is on."""
# True means "faulted" or "open" or "abnormal state"
return bool(self._zone["state"] != "Normal")
@@ -143,5 +145,5 @@ class Concord232ZoneSensor(BinarySensorEntity):
if hasattr(self._client, "zones"):
self._zone = next(
x for x in self._client.zones if x["number"] == self._number
(x for x in self._client.zones if x["number"] == self._number), None
)
+1 -1
View File
@@ -10,7 +10,7 @@ LOGGER = logging.getLogger(__package__)
DOMAIN = "deconz"
HASSIO_CONFIGURATION_URL = "homeassistant://app/core_deconz"
HASSIO_CONFIGURATION_URL = "homeassistant://hassio/ingress/core_deconz"
CONF_BRIDGE_ID = "bridgeid"
CONF_GROUP_ID_BASE = "group_id_base"
@@ -1,17 +0,0 @@
"""Provides conditions for device trackers."""
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from .const import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_home": make_entity_state_condition(DOMAIN, STATE_HOME),
"is_not_home": make_entity_state_condition(DOMAIN, STATE_NOT_HOME),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for device trackers."""
return CONDITIONS
@@ -1,17 +0,0 @@
.condition_common: &condition_common
target:
entity:
domain: device_tracker
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_home: *condition_common
is_not_home: *condition_common
@@ -1,12 +1,4 @@
{
"conditions": {
"is_home": {
"condition": "mdi:account"
},
"is_not_home": {
"condition": "mdi:account-arrow-right"
}
},
"entity_component": {
"_": {
"default": "mdi:account",
@@ -1,32 +1,8 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted device trackers.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted device trackers to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_home": {
"description": "Tests if one or more device trackers are home.",
"fields": {
"behavior": {
"description": "[%key:component::device_tracker::common::condition_behavior_description%]",
"name": "[%key:component::device_tracker::common::condition_behavior_name%]"
}
},
"name": "Device tracker is home"
},
"is_not_home": {
"description": "Tests if one or more device trackers are not home.",
"fields": {
"behavior": {
"description": "[%key:component::device_tracker::common::condition_behavior_description%]",
"name": "[%key:component::device_tracker::common::condition_behavior_name%]"
}
},
"name": "Device tracker is not home"
}
},
"device_automation": {
"condition_type": {
"is_home": "{entity_name} is home",
@@ -73,12 +49,6 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
+1 -2
View File
@@ -18,7 +18,6 @@ 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__)
@@ -36,7 +35,7 @@ DEFAULT_REPORT_SERVER_PORT = 52010
DEFAULT_VERSION = "GATE-01"
DOMAIN = "egardia"
EGARDIA_DEVICE: HassKey[egardiadevice.EgardiaDevice] = HassKey(DOMAIN)
EGARDIA_DEVICE = "egardiadevice"
EGARDIA_NAME = "egardianame"
EGARDIA_REPORT_SERVER_CODES = "egardia_rs_codes"
EGARDIA_REPORT_SERVER_ENABLED = "egardia_rs_enabled"
@@ -4,7 +4,6 @@ from __future__ import annotations
import logging
from pythonegardia.egardiadevice import EgardiaDevice
import requests
from homeassistant.components.alarm_control_panel import (
@@ -12,7 +11,6 @@ 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
@@ -49,10 +47,10 @@ def setup_platform(
if discovery_info is None:
return
device = EgardiaAlarm(
discovery_info[CONF_NAME],
discovery_info["name"],
hass.data[EGARDIA_DEVICE],
discovery_info[CONF_REPORT_SERVER_ENABLED],
discovery_info[CONF_REPORT_SERVER_CODES],
discovery_info.get(CONF_REPORT_SERVER_CODES),
discovery_info[CONF_REPORT_SERVER_PORT],
)
@@ -69,13 +67,8 @@ class EgardiaAlarm(AlarmControlPanelEntity):
)
def __init__(
self,
name: str,
egardiasystem: EgardiaDevice,
rs_enabled: bool,
rs_codes: dict[str, list[str]],
rs_port: int,
) -> None:
self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010
):
"""Initialize the Egardia alarm."""
self._attr_name = name
self._egardiasystem = egardiasystem
@@ -92,7 +85,9 @@ class EgardiaAlarm(AlarmControlPanelEntity):
@property
def should_poll(self) -> bool:
"""Poll if no report server is enabled."""
return not self._rs_enabled
if not self._rs_enabled:
return True
return False
def handle_status_event(self, event):
"""Handle the Egardia system status event."""
@@ -2,12 +2,11 @@
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
@@ -52,20 +51,30 @@ async def async_setup_platform(
class EgardiaBinarySensor(BinarySensorEntity):
"""Represents a sensor based on an Egardia sensor (IR, Door Contact)."""
def __init__(
self,
sensor_id: str,
name: str,
egardia_system: EgardiaDevice,
device_class: BinarySensorDeviceClass | None,
) -> None:
def __init__(self, sensor_id, name, egardia_system, device_class):
"""Initialize the sensor device."""
self._id = sensor_id
self._attr_name = name
self._attr_device_class = device_class
self._name = name
self._state = None
self._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._attr_is_on = bool(egardia_input)
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
+1 -1
View File
@@ -1034,7 +1034,7 @@ def _async_setup_device_registry(
and dashboard.data
and dashboard.data.get(device_info.name)
):
configuration_url = f"homeassistant://app/{dashboard.addon_slug}"
configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}"
manufacturer = "espressif"
if device_info.manufacturer:
+2 -2
View File
@@ -14,7 +14,7 @@
"name": "[%key:component::fan::common::condition_behavior_name%]"
}
},
"name": "Fan is off"
"name": "If a fan is off"
},
"is_on": {
"description": "Tests if one or more fans are on.",
@@ -24,7 +24,7 @@
"name": "[%key:component::fan::common::condition_behavior_name%]"
}
},
"name": "Fan is on"
"name": "If a fan is on"
}
},
"device_automation": {
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyfirefly==0.1.12"]
"requirements": ["pyfirefly==0.1.11"]
}
@@ -23,5 +23,5 @@
"winter_mode": {}
},
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260107.2"]
"requirements": ["home-assistant-frontend==20260107.1"]
}
@@ -7,8 +7,20 @@
"benzene": {
"default": "mdi:molecule"
},
"nitrogen_dioxide": {
"default": "mdi:molecule"
},
"nitrogen_monoxide": {
"default": "mdi:molecule"
},
"non_methane_hydrocarbons": {
"default": "mdi:molecule"
},
"ozone": {
"default": "mdi:molecule"
},
"sulphur_dioxide": {
"default": "mdi:molecule"
}
}
}
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_air_quality_api"],
"quality_scale": "bronze",
"requirements": ["google_air_quality_api==3.0.0"]
"requirements": ["google_air_quality_api==2.1.2"]
}
@@ -13,11 +13,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
CONF_LATITUDE,
CONF_LONGITUDE,
)
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -118,7 +114,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
native_unit_of_measurement_fn=lambda x: x.pollutants.co.concentration.units,
exists_fn=lambda x: "co" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.co.concentration.value,
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
AirQualitySensorEntityDescription(
key="nh3",
@@ -138,24 +133,24 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
),
AirQualitySensorEntityDescription(
key="no",
translation_key="nitrogen_monoxide",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
native_unit_of_measurement_fn=lambda x: x.pollutants.no.concentration.units,
value_fn=lambda x: x.pollutants.no.concentration.value,
exists_fn=lambda x: "no" in {p.code for p in x.pollutants},
),
AirQualitySensorEntityDescription(
key="no2",
translation_key="nitrogen_dioxide",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement_fn=lambda x: x.pollutants.no2.concentration.units,
exists_fn=lambda x: "no2" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.no2.concentration.value,
),
AirQualitySensorEntityDescription(
key="o3",
translation_key="ozone",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement_fn=lambda x: x.pollutants.o3.concentration.units,
exists_fn=lambda x: "o3" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.o3.concentration.value,
@@ -178,8 +173,8 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
),
AirQualitySensorEntityDescription(
key="so2",
translation_key="sulphur_dioxide",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement_fn=lambda x: x.pollutants.so2.concentration.units,
exists_fn=lambda x: "so2" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.so2.concentration.value,
@@ -205,9 +205,21 @@
"so2": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
}
},
"nitrogen_dioxide": {
"name": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]"
},
"nitrogen_monoxide": {
"name": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]"
},
"non_methane_hydrocarbons": {
"name": "Non-methane hydrocarbons"
},
"ozone": {
"name": "[%key:component::sensor::entity_component::ozone::name%]"
},
"sulphur_dioxide": {
"name": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
},
"uaqi": {
"name": "Universal Air Quality Index"
},
@@ -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.59.0"]
"requirements": ["google-genai==1.56.0"]
}
@@ -83,9 +83,6 @@
"invalid_credentials": "Input is incomplete. You must provide either your login details or an API token",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"advanced": {
"data": {
+1 -2
View File
@@ -16,7 +16,7 @@ from aiohasupervisor.models import GreenOptions, YellowOptions # noqa: F401
import voluptuous as vol
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.components import frontend, panel_custom
from homeassistant.components import panel_custom
from homeassistant.components.homeassistant import async_set_stop_handler
from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
@@ -292,7 +292,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
return False
async_load_websocket_api(hass)
frontend.async_register_built_in_panel(hass, "app")
host = os.environ["SUPERVISOR"]
websession = async_get_clientsession(hass)
+11 -7
View File
@@ -6,7 +6,7 @@ from typing import Any
from aiohttp import web
from homeassistant.components import frontend
from homeassistant.components import frontend, panel_custom
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import ATTR_ICON
from homeassistant.core import HomeAssistant
@@ -33,7 +33,7 @@ async def async_setup_addon_panel(hass: HomeAssistant, hassio: HassIO) -> None:
# _register_panel never suspends and is only
# a coroutine because it would be a breaking change
# to make it a normal function
_register_panel(hass, addon, data)
await _register_panel(hass, addon, data)
class HassIOAddonPanel(HomeAssistantView):
@@ -58,7 +58,7 @@ class HassIOAddonPanel(HomeAssistantView):
data = panels[addon]
# Register panel
_register_panel(self.hass, addon, data)
await _register_panel(self.hass, addon, data)
return web.Response()
async def delete(self, request: web.Request, addon: str) -> web.Response:
@@ -76,14 +76,18 @@ class HassIOAddonPanel(HomeAssistantView):
return {}
def _register_panel(hass: HomeAssistant, addon: str, data: dict[str, Any]):
async def _register_panel(
hass: HomeAssistant, addon: str, data: dict[str, Any]
) -> None:
"""Init coroutine to register the panel."""
frontend.async_register_built_in_panel(
await panel_custom.async_register_panel(
hass,
"app",
frontend_url_path=addon,
webcomponent_name="hassio-main",
sidebar_title=data[ATTR_TITLE],
sidebar_icon=data[ATTR_ICON],
js_url="/api/hassio/app/entrypoint.js",
embed_iframe=True,
require_admin=data[ATTR_ADMIN],
config={"addon": addon},
config={"ingress": addon},
)
@@ -19,8 +19,6 @@ from .const import DOMAIN
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
PARALLEL_UPDATES = 1
@dataclass(kw_only=True, frozen=True)
class HDFuryButtonEntityDescription(ButtonEntityDescription):
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/hdfury",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["hdfury==1.4.2"]
"quality_scale": "bronze",
"requirements": ["hdfury==1.3.1"]
}
@@ -35,11 +35,11 @@ rules:
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
parallel-updates: todo
reauthentication-flow:
status: exempt
comment: Integration has no authentication flow.
test-coverage: done
test-coverage: todo
# Gold
devices: done
+7 -7
View File
@@ -20,8 +20,6 @@ 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):
@@ -79,11 +77,13 @@ async def async_setup_entry(
coordinator = entry.runtime_data
entities: list[HDFuryEntity] = [
HDFurySelect(coordinator, description)
for description in SELECT_PORTS
if description.key in coordinator.data.info
]
entities: list[HDFuryEntity] = []
for description in SELECT_PORTS:
if description.key not in coordinator.data.info:
continue
entities.append(HDFurySelect(coordinator, description))
# Add OPMODE select if present
if "opmode" in coordinator.data.info:
@@ -8,8 +8,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
PARALLEL_UPDATES = 0
SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="RX0",
@@ -16,8 +16,6 @@ from .const import DOMAIN
from .coordinator import HDFuryConfigEntry
from .entity import HDFuryEntity
PARALLEL_UPDATES = 1
@dataclass(kw_only=True, frozen=True)
class HDFurySwitchEntityDescription(SwitchEntityDescription):
+1 -13
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
from dataclasses import dataclass
import logging
from xml.etree.ElementTree import ParseError
from pyhik.constants import SENSOR_MAP
from pyhik.hikvision import HikCamera
@@ -89,12 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
def fetch_and_inject_nvr_events() -> None:
"""Fetch and inject NVR events in a single executor job."""
try:
nvr_events = camera.get_event_triggers(nvr_notification_methods)
except (requests.exceptions.RequestException, ParseError) as err:
_LOGGER.warning("Unable to fetch event triggers from %s: %s", host, err)
return
nvr_events = camera.get_event_triggers(nvr_notification_methods)
_LOGGER.debug("NVR events fetched with extended methods: %s", nvr_events)
if nvr_events:
# Map raw event type names to friendly names using SENSOR_MAP
@@ -107,12 +101,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
mapped_events[friendly_name] = list(channels)
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
camera.inject_events(mapped_events)
else:
_LOGGER.debug(
"No event triggers returned from %s. "
"Ensure events are configured on the device",
host,
)
await hass.async_add_executor_job(fetch_and_inject_nvr_events)
@@ -27,6 +27,7 @@ from homeassistant.const import (
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
@@ -35,7 +36,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import HikvisionConfigEntry
from .const import DEFAULT_PORT, DOMAIN
from .entity import HikvisionEntity
CONF_IGNORED = "ignored"
@@ -150,12 +150,7 @@ async def async_setup_entry(
sensors = camera.current_event_states
if sensors is None or not sensors:
_LOGGER.warning(
"Hikvision %s %s has no sensors available. "
"Ensure event detection is enabled and configured on the device",
data.device_type,
data.device_name,
)
_LOGGER.warning("Hikvision device has no sensors available")
return
async_add_entities(
@@ -169,9 +164,10 @@ async def async_setup_entry(
)
class HikvisionBinarySensor(HikvisionEntity, BinarySensorEntity):
class HikvisionBinarySensor(BinarySensorEntity):
"""Representation of a Hikvision binary sensor."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
@@ -181,14 +177,38 @@ class HikvisionBinarySensor(HikvisionEntity, BinarySensorEntity):
channel: int,
) -> None:
"""Initialize the binary sensor."""
super().__init__(entry, channel)
self._data = entry.runtime_data
self._camera = self._data.camera
self._sensor_type = sensor_type
self._channel = channel
# Build unique ID (includes sensor_type for uniqueness per sensor)
# Build unique ID
self._attr_unique_id = f"{self._data.device_id}_{sensor_type}_{channel}"
# Set entity name
self._attr_name = sensor_type
# Device info for device registry
if self._data.device_type == "NVR":
# NVR channels get their own device linked to the NVR via via_device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._data.device_id}_{channel}")},
via_device=(DOMAIN, self._data.device_id),
translation_key="nvr_channel",
translation_placeholders={
"device_name": self._data.device_name,
"channel_number": str(channel),
},
manufacturer="Hikvision",
model="NVR Channel",
)
self._attr_name = sensor_type
else:
# Single camera device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._data.device_id)},
name=self._data.device_name,
manufacturer="Hikvision",
model=self._data.device_type,
)
self._attr_name = sensor_type
# Set device class
self._attr_device_class = DEVICE_CLASS_MAP.get(sensor_type)
+31 -3
View File
@@ -5,10 +5,11 @@ from __future__ import annotations
from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HikvisionConfigEntry
from .entity import HikvisionEntity
from .const import DOMAIN
PARALLEL_UPDATES = 0
@@ -34,9 +35,10 @@ async def async_setup_entry(
async_add_entities(entities)
class HikvisionCamera(HikvisionEntity, Camera):
class HikvisionCamera(Camera):
"""Representation of a Hikvision camera."""
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = CameraEntityFeature.STREAM
@@ -46,11 +48,37 @@ class HikvisionCamera(HikvisionEntity, Camera):
channel: int,
) -> None:
"""Initialize the camera."""
super().__init__(entry, channel)
super().__init__()
self._data = entry.runtime_data
self._channel = channel
self._camera = self._data.camera
# Build unique ID (unique per platform per integration)
self._attr_unique_id = f"{self._data.device_id}_{channel}"
# Device info for device registry
if self._data.device_type == "NVR":
# NVR channels get their own device linked to the NVR via via_device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._data.device_id}_{channel}")},
via_device=(DOMAIN, self._data.device_id),
translation_key="nvr_channel",
translation_placeholders={
"device_name": self._data.device_name,
"channel_number": str(channel),
},
manufacturer="Hikvision",
model="NVR Channel",
)
else:
# Single camera device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._data.device_id)},
name=self._data.device_name,
manufacturer="Hikvision",
model=self._data.device_type,
)
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
@@ -1,49 +0,0 @@
"""Base entity for Hikvision integration."""
from __future__ import annotations
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from . import HikvisionConfigEntry, HikvisionData
from .const import DOMAIN
class HikvisionEntity(Entity):
"""Base class for Hikvision entities."""
_attr_has_entity_name = True
def __init__(
self,
entry: HikvisionConfigEntry,
channel: int,
) -> None:
"""Initialize the entity."""
super().__init__()
self._data: HikvisionData = entry.runtime_data
self._camera = self._data.camera
self._channel = channel
# Device info for device registry
if self._data.device_type == "NVR":
# NVR channels get their own device linked to the NVR via via_device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._data.device_id}_{channel}")},
via_device=(DOMAIN, self._data.device_id),
translation_key="nvr_channel",
translation_placeholders={
"device_name": self._data.device_name,
"channel_number": str(channel),
},
manufacturer="Hikvision",
model="NVR Channel",
)
else:
# Single camera device
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._data.device_id)},
name=self._data.device_name,
manufacturer="Hikvision",
model=self._data.device_type,
)
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.89", "babel==2.15.0"]
"requirements": ["holidays==0.84", "babel==2.15.0"]
}
+73 -7
View File
@@ -6,7 +6,10 @@ from dataclasses import dataclass
from pyHomee.const import AttributeType, NodeState
from pyHomee.model import HomeeAttribute, HomeeNode
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
@@ -14,10 +17,17 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from . import HomeeConfigEntry
from .const import (
DOMAIN,
HOMEE_UNIT_TO_HA_UNIT,
OPEN_CLOSE_MAP,
OPEN_CLOSE_MAP_REVERSED,
@@ -99,6 +109,11 @@ SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.CURRENT_VALVE_POSITION: HomeeSensorEntityDescription(
key="valve_position",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.DAWN: HomeeSensorEntityDescription(
key="dawn",
device_class=SensorDeviceClass.ILLUMINANCE,
@@ -279,12 +294,57 @@ NODE_SENSOR_DESCRIPTIONS: tuple[HomeeNodeSensorEntityDescription, ...] = (
)
def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Get list of related automations and scripts."""
used_in = automations_with_entity(hass, entity_id)
used_in += scripts_with_entity(hass, entity_id)
return used_in
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the homee platform for the sensor components."""
ent_reg = er.async_get(hass)
def add_deprecated_entity(
attribute: HomeeAttribute, description: HomeeSensorEntityDescription
) -> list[HomeeSensor]:
"""Add deprecated entities."""
deprecated_entities: list[HomeeSensor] = []
entity_uid = f"{config_entry.runtime_data.settings.uid}-{attribute.node_id}-{attribute.id}"
if entity_id := ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, entity_uid):
entity_entry = ent_reg.async_get(entity_id)
if entity_entry and entity_entry.disabled:
ent_reg.async_remove(entity_id)
async_delete_issue(
hass,
DOMAIN,
f"deprecated_entity_{entity_uid}",
)
elif entity_entry:
deprecated_entities.append(
HomeeSensor(attribute, config_entry, description)
)
if entity_used_in(hass, entity_id):
async_create_issue(
hass,
DOMAIN,
f"deprecated_entity_{entity_uid}",
breaks_in_ha_version="2025.12.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_entity",
translation_placeholders={
"name": str(
entity_entry.name or entity_entry.original_name
),
"entity": entity_id,
},
)
return deprecated_entities
async def add_sensor_entities(
config_entry: HomeeConfigEntry,
@@ -302,13 +362,19 @@ async def async_setup_entry(
)
# Node attributes that are sensors.
entities.extend(
HomeeSensor(
attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type]
)
for attribute in node.attributes
if attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable
)
for attribute in node.attributes:
if attribute.type == AttributeType.CURRENT_VALVE_POSITION:
entities.extend(
add_deprecated_entity(
attribute, SENSOR_DESCRIPTIONS[attribute.type]
)
)
elif attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable:
entities.append(
HomeeSensor(
attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type]
)
)
if entities:
async_add_entities(entities)
@@ -495,5 +495,11 @@
"invalid_preset_mode": {
"message": "Invalid preset mode: {preset_mode}. Turning on is only supported with preset mode 'Manual'."
}
},
"issues": {
"deprecated_entity": {
"description": "The Homee entity `{entity}` is deprecated and will be removed in release 2025.12.\nThe valve is available directly in the respective climate entity.\nPlease update your automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue.",
"title": "The Homee {name} entity is deprecated"
}
}
}
+9 -3
View File
@@ -108,6 +108,7 @@ _DEFAULT_BIND = ["0.0.0.0", "::"] if _HAS_IPV6 else ["0.0.0.0"]
HTTP_SCHEMA: Final = vol.All(
cv.deprecated(CONF_BASE_URL),
cv.deprecated(CONF_SERVER_HOST), # Deprecated in HA Core 2025.12
vol.Schema(
{
vol.Optional(CONF_SERVER_HOST): vol.All(
@@ -208,15 +209,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if conf is None:
conf = cast(ConfData, HTTP_SCHEMA({}))
if CONF_SERVER_HOST in conf and is_hassio(hass):
issue_id = "server_host_deprecated_hassio"
if CONF_SERVER_HOST in conf:
if is_hassio(hass):
issue_id = "server_host_deprecated_hassio"
severity = ir.IssueSeverity.ERROR
else:
issue_id = "server_host_deprecated"
severity = ir.IssueSeverity.WARNING
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
breaks_in_ha_version="2026.6.0",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
severity=severity,
translation_key=issue_id,
)
@@ -1,5 +1,9 @@
{
"issues": {
"server_host_deprecated": {
"description": "The `server_host` configuration option in the HTTP integration is deprecated and will be removed.\n\nIf you are using this option to bind Home Assistant to specific network interfaces, please remove it from your configuration. Home Assistant will automatically bind to all available interfaces by default.\n\nIf you have specific networking requirements, consider using firewall rules or other network configuration to control access to Home Assistant.",
"title": "The `server_host` HTTP configuration option is deprecated"
},
"server_host_deprecated_hassio": {
"description": "The deprecated `server_host` configuration option in the HTTP integration is prone to break the communication between Home Assistant Core and Supervisor, and will be removed.\n\nIf you are using this option to bind Home Assistant to specific network interfaces, please remove it from your configuration. Home Assistant will automatically bind to all available interfaces by default.\n\nIf you have specific networking requirements, consider using firewall rules or other network configuration to control access to Home Assistant.",
"title": "The `server_host` HTTP configuration may break Home Assistant Core - Supervisor communication"
@@ -28,7 +28,6 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2
from .const import DOMAIN, UPDATE_INTERVAL
from .entity import AqualinkEntity
@@ -67,11 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AqualinkConfigEntry) ->
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
aqualink = AqualinkClient(
username,
password,
httpx_client=get_async_client(hass, alpn_protocols=SSL_ALPN_HTTP11_HTTP2),
)
aqualink = AqualinkClient(username, password, httpx_client=get_async_client(hass))
try:
await aqualink.login()
except AqualinkServiceException as login_exception:
@@ -15,7 +15,6 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2
from .const import DOMAIN
@@ -37,11 +36,7 @@ class AqualinkFlowHandler(ConfigFlow, domain=DOMAIN):
try:
async with AqualinkClient(
username,
password,
httpx_client=get_async_client(
self.hass, alpn_protocols=SSL_ALPN_HTTP11_HTTP2
),
username, password, httpx_client=get_async_client(self.hass)
):
pass
except AqualinkServiceUnauthorizedException:
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["keyrings.alt", "pyicloud"],
"requirements": ["pyicloud==2.3.0"]
"requirements": ["pyicloud==2.2.0"]
}
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["imgw_pib==2.0.1"]
"requirements": ["imgw_pib==1.6.0"]
}
@@ -19,7 +19,7 @@
"loggers": ["pyinsteon", "pypubsub"],
"requirements": [
"pyinsteon==1.6.4",
"insteon-frontend-home-assistant==0.6.1"
"insteon-frontend-home-assistant==0.6.0"
],
"single_config_entry": true,
"usb": [
+1 -1
View File
@@ -73,7 +73,7 @@ SENSOR_TYPES: list[IOmeterEntityDescription] = [
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: int(round(data.status.device.core.battery_level)),
value_fn=lambda data: data.status.device.core.battery_level,
),
IOmeterEntityDescription(
key="pin_status",
@@ -2,7 +2,7 @@
from __future__ import annotations
from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorTimeoutError
from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError
from homeassistant.const import (
CONF_HOST,
@@ -11,9 +11,8 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator
@@ -29,8 +28,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: JVCConfigEntry) -> bool:
)
try:
await device.connect()
except JvcProjectorTimeoutError as err:
await device.connect(True)
except JvcProjectorConnectError as err:
await device.disconnect()
raise ConfigEntryNotReady(
f"Unable to connect to {entry.data[CONF_HOST]}"
@@ -51,8 +50,6 @@ 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
@@ -63,21 +60,3 @@ 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)
@@ -2,17 +2,16 @@
from __future__ import annotations
from jvcprojector import command as cmd
from jvcprojector import const
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 = (cmd.Power.ON, cmd.Power.WARMING)
ON_STATUS = (const.ON, const.WARMING)
async def async_setup_entry(
@@ -22,13 +21,14 @@ 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 = "power"
_attr_translation_key = "jvc_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.unique_id}_power"
self._attr_unique_id = f"{coordinator.device.mac}_power"
@property
def is_on(self) -> bool:
"""Return true if the JVC Projector is on."""
return self.coordinator.data[POWER] in ON_STATUS
"""Return true if the JVC is on."""
return self.coordinator.data["power"] in ON_STATUS
@@ -5,12 +5,7 @@ from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from jvcprojector import (
JvcProjector,
JvcProjectorAuthError,
JvcProjectorTimeoutError,
command as cmd,
)
from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnectError
from jvcprojector.projector import DEFAULT_PORT
import voluptuous as vol
@@ -45,7 +40,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN):
mac = await get_mac_address(host, port, password)
except InvalidHost:
errors["base"] = "invalid_host"
except JvcProjectorTimeoutError:
except JvcProjectorConnectError:
errors["base"] = "cannot_connect"
except JvcProjectorAuthError:
errors["base"] = "invalid_auth"
@@ -96,7 +91,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN):
try:
await get_mac_address(host, port, password)
except JvcProjectorTimeoutError:
except JvcProjectorConnectError:
errors["base"] = "cannot_connect"
except JvcProjectorAuthError:
errors["base"] = "invalid_auth"
@@ -120,7 +115,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()
return await device.get(cmd.MacAddress)
await device.connect(True)
finally:
await device.disconnect()
return device.mac
@@ -3,7 +3,3 @@
NAME = "JVC Projector"
DOMAIN = "jvc_projector"
MANUFACTURER = "JVC"
POWER = "power"
INPUT = "input"
SOURCE = "source"
@@ -4,21 +4,22 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import TYPE_CHECKING, Any
from typing import Any
from jvcprojector import (
JvcProjector,
JvcProjectorAuthError,
JvcProjectorTimeoutError,
command as cmd,
JvcProjectorConnectError,
const,
)
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 INPUT, NAME, POWER
from .const import NAME
_LOGGER = logging.getLogger(__name__)
@@ -45,33 +46,26 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
update_interval=INTERVAL_SLOW,
)
self.device: JvcProjector = device
if TYPE_CHECKING:
assert config_entry.unique_id is not None
self.unique_id = config_entry.unique_id
self.device = device
self.unique_id = format_mac(device.mac)
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[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:
state = await self.device.get_state()
except JvcProjectorConnectError 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
if state[POWER] != cmd.Power.STANDBY:
old_interval = self.update_interval
if state[const.POWER] != const.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
@@ -26,7 +26,7 @@ class JvcProjectorEntity(CoordinatorEntity[JvcProjectorDataUpdateCoordinator]):
self._attr_unique_id = coordinator.unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
identifiers={(DOMAIN, coordinator.unique_id)},
name=NAME,
model=self.device.model,
manufacturer=MANUFACTURER,
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==2.0.0"]
"requirements": ["pyjvcprojector==1.1.3"]
}
@@ -7,62 +7,54 @@ from collections.abc import Iterable
import logging
from typing import Any
from jvcprojector import command as cmd
from jvcprojector import const
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: 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,
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,
}
_LOGGER = logging.getLogger(__name__)
@@ -85,34 +77,25 @@ class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity):
@property
def is_on(self) -> bool:
"""Return True if the entity is on."""
return self.coordinator.data[POWER] in (cmd.Power.ON, cmd.Power.WARMING)
"""Return True if entity is on."""
return self.coordinator.data["power"] in [const.ON, const.WARMING]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
await self.device.set(cmd.Power, cmd.Power.ON)
await self.device.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.set(cmd.Power, cmd.Power.OFF)
await self.device.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 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)
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])
@@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Final
from jvcprojector import JvcProjector, command as cmd
from jvcprojector import JvcProjector, const
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
@@ -23,12 +23,16 @@ 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=[cmd.Input.HDMI1, cmd.Input.HDMI2],
command=lambda device, option: device.set(cmd.Input, option),
options=list(OPTIONS["input"]),
command=lambda device, option: device.remote(OPTIONS["input"][option]),
)
]
@@ -2,7 +2,7 @@
from __future__ import annotations
from jvcprojector import command as cmd
from jvcprojector import const
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -23,11 +23,11 @@ JVC_SENSORS = (
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=[
cmd.Power.STANDBY,
cmd.Power.ON,
cmd.Power.WARMING,
cmd.Power.COOLING,
cmd.Power.ERROR,
const.STANDBY,
const.ON,
const.WARMING,
const.COOLING,
const.ERROR,
],
),
)
@@ -35,7 +35,7 @@
},
"entity": {
"binary_sensor": {
"power": {
"jvc_power": {
"name": "[%key:component::binary_sensor::entity_component::power::name%]"
}
},
@@ -50,7 +50,7 @@
},
"sensor": {
"jvc_power_status": {
"name": "Status",
"name": "Power status",
"state": {
"cooling": "Cooling",
"error": "[%key:common::state::error%]",
+1 -1
View File
@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.14.0",
"xknxproject==3.8.2",
"knx-frontend==2026.1.15.112308"
"knx-frontend==2025.12.30.151231"
],
"single_config_entry": true
}
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["kostal"],
"requirements": ["pykoplenti==1.5.0"]
"requirements": ["pykoplenti==1.3.0"]
}
+1 -6
View File
@@ -18,11 +18,7 @@ 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,
async_update_preview_feature,
)
from .helpers import async_is_preview_feature_enabled, async_listen
from .models import (
EventLabsUpdatedData,
LabPreviewFeature,
@@ -41,7 +37,6 @@ __all__ = [
"EventLabsUpdatedData",
"async_is_preview_feature_enabled",
"async_listen",
"async_update_preview_feature",
]

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