Compare commits

..

2 Commits

Author SHA1 Message Date
Paulus Schoutsen
554c6e0cca Fix service helper tests 2025-05-13 20:17:07 +00:00
Paulus Schoutsen
7bac640267 Add area motion entity ID 2025-05-13 17:29:25 +00:00
1172 changed files with 7110 additions and 31442 deletions

View File

@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile

View File

@@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Dependency review
uses: actions/dependency-review-action@v4.7.1
uses: actions/dependency-review-action@v4.7.0
with:
license-check: false # We use our own license audit checks
@@ -944,8 +944,7 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev \
libxml2-utils
libgammu-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
@@ -1021,12 +1020,6 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1077,8 +1070,7 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libmariadb-dev-compat \
libxml2-utils
libmariadb-dev-compat
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
@@ -1162,12 +1154,6 @@ jobs:
steps.pytest-partial.outputs.mariadb }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1216,8 +1202,7 @@ jobs:
sudo apt-get -y install \
bluez \
ffmpeg \
libturbojpeg \
libxml2-utils
libturbojpeg
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt-get -y install \
postgresql-server-dev-14
@@ -1305,12 +1290,6 @@ jobs:
steps.pytest-partial.outputs.postgresql }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1341,7 +1320,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@v5.4.2
with:
fail_ci_if_error: true
flags: full-suite
@@ -1378,8 +1357,7 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev \
libxml2-utils
libgammu-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
@@ -1458,12 +1436,6 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1491,7 +1463,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.4.3
uses: codecov/codecov-action@v5.4.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.18
uses: github/codeql-action/init@v3.28.17
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.18
uses: github/codeql-action/analyze@v3.28.17
with:
category: "/language:python"

View File

@@ -270,7 +270,6 @@ homeassistant.components.image_processing.*
homeassistant.components.image_upload.*
homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*

6
CODEOWNERS generated
View File

@@ -710,8 +710,6 @@ build.json @home-assistant/supervisor
/tests/components/imeon_inverter/ @Imeon-Energy
/homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/immich/ @mib1185
/tests/components/immich/ @mib1185
/homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
@@ -1486,8 +1484,8 @@ build.json @home-assistant/supervisor
/tests/components/subaru/ @G-Two
/homeassistant/components/suez_water/ @ooii @jb101010-2
/tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @home-assistant/core
/tests/components/sun/ @home-assistant/core
/homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen

View File

@@ -6,7 +6,6 @@ from typing import Any, Concatenate
from airgradient import AirGradientConnectionError, AirGradientError, get_model_name
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -30,7 +29,6 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
model_id=measures.model,
serial_number=coordinator.serial_number,
sw_version=measures.firmware_version,
connections={(dr.CONNECTION_NETWORK_MAC, coordinator.serial_number)},
)

View File

@@ -47,7 +47,7 @@ from .const import (
CONF_VIDEO_SOURCE,
DEFAULT_STREAM_PROFILE,
DEFAULT_VIDEO_SOURCE,
DOMAIN,
DOMAIN as AXIS_DOMAIN,
)
from .errors import AuthenticationRequired, CannotConnect
from .hub import AxisHub, get_axis_api
@@ -58,7 +58,7 @@ DEFAULT_PROTOCOL = "https"
PROTOCOL_CHOICES = ["https", "http"]
class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
"""Handle a Axis config flow."""
VERSION = 3
@@ -146,7 +146,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
model = self.config[CONF_MODEL]
same_model = [
entry.data[CONF_NAME]
for entry in self.hass.config_entries.async_entries(DOMAIN)
for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN)
if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
]

View File

@@ -24,7 +24,7 @@ from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE
type BlueCurrentConfigEntry = ConfigEntry[Connector]
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
PLATFORMS = [Platform.SENSOR]
CHARGE_POINTS = "CHARGE_POINTS"
DATA = "data"
DELAY = 5

View File

@@ -1,89 +0,0 @@
"""Support for Blue Current buttons."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from bluecurrent_api.client import Client
from homeassistant.components.button import (
ButtonDeviceClass,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BlueCurrentConfigEntry, Connector
from .entity import ChargepointEntity
@dataclass(kw_only=True, frozen=True)
class ChargePointButtonEntityDescription(ButtonEntityDescription):
"""Describes a Blue Current button entity."""
function: Callable[[Client, str], Coroutine[Any, Any, None]]
CHARGE_POINT_BUTTONS = (
ChargePointButtonEntityDescription(
key="reset",
translation_key="reset",
function=lambda client, evse_id: client.reset(evse_id),
device_class=ButtonDeviceClass.RESTART,
),
ChargePointButtonEntityDescription(
key="reboot",
translation_key="reboot",
function=lambda client, evse_id: client.reboot(evse_id),
device_class=ButtonDeviceClass.RESTART,
),
ChargePointButtonEntityDescription(
key="stop_charge_session",
translation_key="stop_charge_session",
function=lambda client, evse_id: client.stop_session(evse_id),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: BlueCurrentConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Blue Current buttons."""
connector: Connector = entry.runtime_data
async_add_entities(
ChargePointButton(
connector,
button,
evse_id,
)
for evse_id in connector.charge_points
for button in CHARGE_POINT_BUTTONS
)
class ChargePointButton(ChargepointEntity, ButtonEntity):
"""Define a charge point button."""
has_value = True
entity_description: ChargePointButtonEntityDescription
def __init__(
self,
connector: Connector,
description: ChargePointButtonEntityDescription,
evse_id: str,
) -> None:
"""Initialize the button."""
super().__init__(connector, evse_id)
self.entity_description = description
self._attr_unique_id = f"{description.key}_{evse_id}"
async def async_press(self) -> None:
"""Handle the button press."""
await self.entity_description.function(self.connector.client, self.evse_id)

View File

@@ -1,5 +1,7 @@
"""Entity representing a Blue Current charge point."""
from abc import abstractmethod
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
@@ -15,12 +17,12 @@ class BlueCurrentEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
has_value = False
def __init__(self, connector: Connector, signal: str) -> None:
"""Initialize the entity."""
self.connector = connector
self.signal = signal
self.has_value = False
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@@ -41,6 +43,7 @@ class BlueCurrentEntity(Entity):
return self.connector.connected and self.has_value
@callback
@abstractmethod
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""

View File

@@ -19,17 +19,6 @@
"current_left": {
"default": "mdi:gauge"
}
},
"button": {
"reset": {
"default": "mdi:restart"
},
"reboot": {
"default": "mdi:restart-alert"
},
"stop_charge_session": {
"default": "mdi:stop"
}
}
}
}

View File

@@ -113,17 +113,6 @@
"grid_max_current": {
"name": "Max grid current"
}
},
"button": {
"stop_charge_session": {
"name": "Stop charge session"
},
"reboot": {
"name": "Reboot"
},
"reset": {
"name": "Reset"
}
}
}
}

View File

@@ -22,7 +22,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.ssl import get_default_context
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
from .const import (
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
DOMAIN as BMW_DOMAIN,
SCAN_INTERVALS,
)
_LOGGER = logging.getLogger(__name__)
@@ -57,7 +63,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
hass,
_LOGGER,
config_entry=config_entry,
name=f"{DOMAIN}-{config_entry.data[CONF_USERNAME]}",
name=f"{BMW_DOMAIN}-{config_entry.data[CONF_USERNAME]}",
update_interval=timedelta(
seconds=SCAN_INTERVALS[config_entry.data[CONF_REGION]]
),
@@ -75,26 +81,26 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
except MyBMWCaptchaMissingError as err:
# If a captcha is required (user/password login flow), always trigger the reauth flow
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="missing_captcha",
) from err
except MyBMWAuthError as err:
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
if self.last_update_success:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err
# Clear refresh token and trigger reauth if previous update failed as well
self._update_config_entry_refresh_token(None)
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="invalid_auth",
) from err
except (MyBMWAPIError, RequestError) as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err

View File

@@ -7,17 +7,15 @@ from ssl import SSLError
from bosch_alarm_mode2 import Panel
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
PLATFORMS: list[Platform] = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SWITCH,
]
@@ -54,11 +52,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
device_registry = dr.async_get(hass)
mac = entry.data.get(CONF_MAC)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
manufacturer="Bosch Security Systems",

View File

@@ -34,9 +34,6 @@ async def async_setup_entry(
)
PARALLEL_UPDATES = 0
class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
"""An alarm control panel entity for a bosch alarm panel."""

View File

@@ -1,220 +0,0 @@
"""Support for Bosch Alarm Panel binary sensors."""
from __future__ import annotations
from dataclasses import dataclass
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.const import ALARM_PANEL_FAULTS
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity, BoschAlarmEntity, BoschAlarmPointEntity
@dataclass(kw_only=True, frozen=True)
class BoschAlarmFaultEntityDescription(BinarySensorEntityDescription):
"""Describes Bosch Alarm sensor entity."""
fault: int
FAULT_TYPES = [
BoschAlarmFaultEntityDescription(
key="panel_fault_battery_low",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.BATTERY,
fault=ALARM_PANEL_FAULTS.BATTERY_LOW,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_battery_mising",
translation_key="panel_fault_battery_mising",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.BATTERY_MISING,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_ac_fail",
translation_key="panel_fault_ac_fail",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.AC_FAIL,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_phone_line_failure",
translation_key="panel_fault_phone_line_failure",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
fault=ALARM_PANEL_FAULTS.PHONE_LINE_FAILURE,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_parameter_crc_fail_in_pif",
translation_key="panel_fault_parameter_crc_fail_in_pif",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.PARAMETER_CRC_FAIL_IN_PIF,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_communication_fail_since_rps_hang_up",
translation_key="panel_fault_communication_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.COMMUNICATION_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_sdi_fail_since_rps_hang_up",
translation_key="panel_fault_sdi_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.SDI_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_user_code_tamper_since_rps_hang_up",
translation_key="panel_fault_user_code_tamper_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.USER_CODE_TAMPER_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_fail_to_call_rps_since_rps_hang_up",
translation_key="panel_fault_fail_to_call_rps_since_rps_hang_up",
entity_registry_enabled_default=False,
fault=ALARM_PANEL_FAULTS.FAIL_TO_CALL_RPS_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_point_bus_fail_since_rps_hang_up",
translation_key="panel_fault_point_bus_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.POINT_BUS_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_log_overflow",
translation_key="panel_fault_log_overflow",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.LOG_OVERFLOW,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_log_threshold",
translation_key="panel_fault_log_threshold",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.LOG_THRESHOLD,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BoschAlarmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up binary sensors for alarm points and the connection status."""
panel = config_entry.runtime_data
entities: list[BinarySensorEntity] = [
PointSensor(panel, point_id, config_entry.unique_id or config_entry.entry_id)
for point_id in panel.points
]
entities.extend(
PanelFaultsSensor(
panel,
config_entry.unique_id or config_entry.entry_id,
fault_type,
)
for fault_type in FAULT_TYPES
)
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "away"
)
for area_id in panel.areas
)
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "home"
)
for area_id in panel.areas
)
async_add_entities(entities)
PARALLEL_UPDATES = 0
class PanelFaultsSensor(BoschAlarmEntity, BinarySensorEntity):
"""A binary sensor entity for each fault type in a bosch alarm panel."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
entity_description: BoschAlarmFaultEntityDescription
def __init__(
self,
panel: Panel,
unique_id: str,
entity_description: BoschAlarmFaultEntityDescription,
) -> None:
"""Set up a binary sensor entity for each fault type in a bosch alarm panel."""
super().__init__(panel, unique_id, True)
self.entity_description = entity_description
self._fault_type = entity_description.fault
self._attr_unique_id = f"{unique_id}_fault_{entity_description.key}"
@property
def is_on(self) -> bool:
"""Return if this fault has occurred."""
return self._fault_type in self.panel.panel_faults_ids
class AreaReadyToArmSensor(BoschAlarmAreaEntity, BinarySensorEntity):
"""A binary sensor entity showing if a panel is ready to arm."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self, panel: Panel, area_id: int, unique_id: str, arm_type: str
) -> None:
"""Set up a binary sensor entity for the arming status in a bosch alarm panel."""
super().__init__(panel, area_id, unique_id, False, False, True)
self.panel = panel
self._arm_type = arm_type
self._attr_translation_key = f"area_ready_to_arm_{arm_type}"
self._attr_unique_id = f"{self._area_unique_id}_ready_to_arm_{arm_type}"
@property
def is_on(self) -> bool:
"""Return if this panel is ready to arm."""
if self._arm_type == "away":
return self._area.all_ready
if self._arm_type == "home":
return self._area.all_ready or self._area.part_ready
return False
class PointSensor(BoschAlarmPointEntity, BinarySensorEntity):
"""A binary sensor entity for a point in a bosch alarm panel."""
_attr_name = None
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
"""Set up a binary sensor entity for a point in a bosch alarm panel."""
super().__init__(panel, point_id, unique_id)
self._attr_unique_id = self._point_unique_id
@property
def is_on(self) -> bool:
"""Return if this point sensor is on."""
return self._point.is_open()

View File

@@ -6,13 +6,12 @@ import asyncio
from collections.abc import Mapping
import logging
import ssl
from typing import Any, Self
from typing import Any
from bosch_alarm_mode2 import Panel
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_DHCP,
SOURCE_RECONFIGURE,
SOURCE_USER,
ConfigFlow,
@@ -21,14 +20,11 @@ from homeassistant.config_entries import (
from homeassistant.const import (
CONF_CODE,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_PASSWORD,
CONF_PORT,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
@@ -92,12 +88,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Init config flow."""
self._data: dict[str, Any] = {}
self.mac: str | None = None
self.host: str | None = None
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return self.mac == other_flow.mac or self.host == other_flow.host
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -106,12 +96,9 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
self.host = user_input[CONF_HOST]
if self.source == SOURCE_USER:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, _) = await try_connect(user_input, 0)
(model, serial) = await try_connect(user_input, 0)
except (
OSError,
ConnectionRefusedError,
@@ -142,55 +129,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
self.mac = format_mac(discovery_info.macaddress)
self.host = discovery_info.ip
if self.hass.config_entries.flow.async_has_matching_flow(self):
return self.async_abort(reason="already_in_progress")
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.data[CONF_MAC] == self.mac:
result = self.hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_HOST: discovery_info.ip,
},
)
if result:
self.hass.config_entries.async_schedule_reload(entry.entry_id)
return self.async_abort(reason="already_configured")
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, _) = await try_connect(
{CONF_HOST: discovery_info.ip, CONF_PORT: 7700}, 0
)
except (
OSError,
ConnectionRefusedError,
ssl.SSLError,
asyncio.exceptions.TimeoutError,
):
return self.async_abort(reason="cannot_connect")
except Exception:
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
self.context["title_placeholders"] = {
"model": model,
"host": discovery_info.ip,
}
self._data = {
CONF_HOST: discovery_info.ip,
CONF_MAC: self.mac,
CONF_MODEL: model,
CONF_PORT: 7700,
}
return await self.async_step_auth()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -234,7 +172,7 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
else:
if serial_number:
await self.async_set_unique_id(str(serial_number))
if self.source in (SOURCE_USER, SOURCE_DHCP):
if self.source == SOURCE_USER:
if serial_number:
self._abort_if_unique_id_configured()
else:
@@ -246,7 +184,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
)
if serial_number:
self._abort_if_unique_id_mismatch(reason="device_mismatch")
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data=self._data,

View File

@@ -17,13 +17,9 @@ class BoschAlarmEntity(Entity):
_attr_has_entity_name = True
def __init__(
self, panel: Panel, unique_id: str, observe_faults: bool = False
) -> None:
def __init__(self, panel: Panel, unique_id: str) -> None:
"""Set up a entity for a bosch alarm panel."""
self.panel = panel
self._observe_faults = observe_faults
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model}",
@@ -38,14 +34,10 @@ class BoschAlarmEntity(Entity):
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
if self._observe_faults:
self.panel.faults_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)
if self._observe_faults:
self.panel.faults_observer.attach(self.schedule_update_ha_state)
class BoschAlarmAreaEntity(BoschAlarmEntity):
@@ -96,33 +88,6 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
self._area.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmPointEntity(BoschAlarmEntity):
"""A base entity for point related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
"""Set up a area related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._point_id = point_id
self._point_unique_id = f"{unique_id}_point_{point_id}"
self._point = panel.points[point_id]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._point_unique_id)},
name=self._point.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
)
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
await super().async_added_to_hass()
self._point.status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
await super().async_added_to_hass()
self._point.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmDoorEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""

View File

@@ -1,15 +1,6 @@
{
"entity": {
"sensor": {
"alarms_gas": {
"default": "mdi:alert-circle"
},
"alarms_fire": {
"default": "mdi:alert-circle"
},
"alarms_burglary": {
"default": "mdi:alert-circle"
},
"faulting_points": {
"default": "mdi:alert-circle"
}
@@ -33,44 +24,6 @@
"on": "mdi:lock-open"
}
}
},
"binary_sensor": {
"panel_fault_parameter_crc_fail_in_pif": {
"default": "mdi:alert-circle"
},
"panel_fault_phone_line_failure": {
"default": "mdi:alert-circle"
},
"panel_fault_sdi_fail_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_user_code_tamper_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_point_bus_fail_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_log_overflow": {
"default": "mdi:alert-circle"
},
"panel_fault_log_threshold": {
"default": "mdi:alert-circle"
},
"area_ready_to_arm_away": {
"default": "mdi:shield",
"state": {
"on": "mdi:shield-lock"
}
},
"area_ready_to_arm_home": {
"default": "mdi:shield",
"state": {
"on": "mdi:shield-home"
}
}
}
}
}

View File

@@ -3,11 +3,6 @@
"name": "Bosch Alarm",
"codeowners": ["@mag1024", "@sanjay900"],
"config_flow": true,
"dhcp": [
{
"macaddress": "000463*"
}
],
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
"integration_type": "device",
"iot_class": "local_push",

View File

@@ -39,15 +39,15 @@ rules:
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
parallel-updates: todo
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: done
discovery: done
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo

View File

@@ -6,7 +6,6 @@ from collections.abc import Callable
from dataclasses import dataclass
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.const import ALARM_MEMORY_PRIORITIES
from bosch_alarm_mode2.panel import Area
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
@@ -16,53 +15,18 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity
ALARM_TYPES = {
"burglary": {
ALARM_MEMORY_PRIORITIES.BURGLARY_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.BURGLARY_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.BURGLARY_ALARM: "alarm",
},
"gas": {
ALARM_MEMORY_PRIORITIES.GAS_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.GAS_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.GAS_ALARM: "alarm",
},
"fire": {
ALARM_MEMORY_PRIORITIES.FIRE_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.FIRE_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.FIRE_ALARM: "alarm",
},
}
@dataclass(kw_only=True, frozen=True)
class BoschAlarmSensorEntityDescription(SensorEntityDescription):
"""Describes Bosch Alarm sensor entity."""
value_fn: Callable[[Area], str | int]
value_fn: Callable[[Area], int]
observe_alarms: bool = False
observe_ready: bool = False
observe_status: bool = False
def priority_value_fn(priority_info: dict[int, str]) -> Callable[[Area], str]:
"""Build a value_fn for a given priority type."""
return lambda area: next(
(key for priority, key in priority_info.items() if priority in area.alarms_ids),
"no_issues",
)
SENSOR_TYPES: list[BoschAlarmSensorEntityDescription] = [
*[
BoschAlarmSensorEntityDescription(
key=f"alarms_{key}",
translation_key=f"alarms_{key}",
value_fn=priority_value_fn(priority_type),
observe_alarms=True,
)
for key, priority_type in ALARM_TYPES.items()
],
BoschAlarmSensorEntityDescription(
key="faulting_points",
translation_key="faulting_points",
@@ -117,6 +81,6 @@ class BoschAreaSensor(BoschAlarmAreaEntity, SensorEntity):
self._attr_unique_id = f"{self._area_unique_id}_{entity_description.key}"
@property
def native_value(self) -> str | int:
def native_value(self) -> int:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._area)

View File

@@ -1,6 +1,5 @@
{
"config": {
"flow_title": "{model} ({host})",
"step": {
"user": {
"data": {
@@ -43,7 +42,6 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
@@ -58,95 +56,22 @@
"message": "Incorrect credentials for panel."
},
"incorrect_door_state": {
"message": "Door cannot be manipulated while it is momentarily unlocked."
"message": "Door cannot be manipulated while it is being cycled."
}
},
"entity": {
"binary_sensor": {
"panel_fault_battery_mising": {
"name": "Battery missing"
},
"panel_fault_ac_fail": {
"name": "AC Failure"
},
"panel_fault_parameter_crc_fail_in_pif": {
"name": "CRC failure in panel configuration"
},
"panel_fault_phone_line_failure": {
"name": "Phone line failure"
},
"panel_fault_sdi_fail_since_rps_hang_up": {
"name": "SDI failure since RPS hang up"
},
"panel_fault_user_code_tamper_since_rps_hang_up": {
"name": "User code tamper since RPS hang up"
},
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
"name": "Failure to call RPS since RPS hang up"
},
"panel_fault_point_bus_fail_since_rps_hang_up": {
"name": "Point bus failure since RPS hang up"
},
"panel_fault_log_overflow": {
"name": "Log overflow"
},
"panel_fault_log_threshold": {
"name": "Log threshold reached"
},
"area_ready_to_arm_away": {
"name": "Area ready to arm away",
"state": {
"on": "Ready",
"off": "Not ready"
}
},
"area_ready_to_arm_home": {
"name": "Area ready to arm home",
"state": {
"on": "Ready",
"off": "Not ready"
}
}
},
"switch": {
"secured": {
"name": "Secured"
},
"cycling": {
"name": "Momentarily unlocked"
"name": "Cycling"
},
"locked": {
"name": "Locked"
}
},
"sensor": {
"alarms_gas": {
"name": "Gas alarm issues",
"state": {
"supervisory": "Supervisory",
"trouble": "Trouble",
"alarm": "Alarm",
"no_issues": "No issues"
}
},
"alarms_fire": {
"name": "Fire alarm issues",
"state": {
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
}
},
"alarms_burglary": {
"name": "Burglary alarm issues",
"state": {
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
}
},
"faulting_points": {
"name": "Faulting points",
"unit_of_measurement": "points"

View File

@@ -60,7 +60,7 @@ from .const import (
ADDED_CAST_DEVICES_KEY,
CAST_MULTIZONE_MANAGER_KEY,
CONF_IGNORE_CEC,
DOMAIN,
DOMAIN as CAST_DOMAIN,
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
@@ -315,7 +315,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self._cast_view_remove_handler: CALLBACK_TYPE | None = None
self._attr_unique_id = str(cast_info.uuid)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(cast_info.uuid).replace("-", ""))},
identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))},
manufacturer=str(cast_info.cast_info.manufacturer),
model=cast_info.cast_info.model_name,
name=str(cast_info.friendly_name),
@@ -591,7 +591,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
"""Generate root node."""
children = []
# Add media browsers
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
children.extend(
await platform.async_get_media_browser_root_object(
self.hass, self._chromecast.cast_type
@@ -650,7 +650,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
platform: CastProtocol
assert media_content_type is not None
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
browse_media = await platform.async_browse_media(
self.hass,
media_content_type,
@@ -680,7 +680,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
# Handle media supported by a known cast app
if media_type == DOMAIN:
if media_type == CAST_DOMAIN:
try:
app_data = json.loads(media_id)
if metadata := extra.get("metadata"):
@@ -712,7 +712,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return
# Try the cast platforms
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
result = await platform.async_play_media(
self.hass, self.entity_id, chromecast, media_type, media_id
)

View File

@@ -43,7 +43,7 @@ from homeassistant.util.dt import utcnow
from .const import (
CONF_ENTITY_CONFIG,
CONF_FILTER,
DOMAIN,
DOMAIN as CLOUD_DOMAIN,
PREF_ALEXA_REPORT_STATE,
PREF_ENABLE_ALEXA,
PREF_SHOULD_EXPOSE,
@@ -55,7 +55,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
CLOUD_ALEXA = f"{DOMAIN}.{ALEXA_DOMAIN}"
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
# Time to wait when entity preferences have changed before syncing it to
# the cloud.

View File

@@ -41,7 +41,7 @@ from .const import (
CONF_ENTITY_CONFIG,
CONF_FILTER,
DEFAULT_DISABLE_2FA,
DOMAIN,
DOMAIN as CLOUD_DOMAIN,
PREF_DISABLE_2FA,
PREF_SHOULD_EXPOSE,
)
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
CLOUD_GOOGLE = f"{DOMAIN}.{GOOGLE_DOMAIN}"
CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}"
SUPPORTED_DOMAINS = {

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.101.0"],
"requirements": ["hass-nabucasa==0.100.0"],
"single_config_entry": true
}

View File

@@ -134,9 +134,11 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
self._attr_current_temperature = values[0] / 10
self._attr_hvac_action = None
if _mode == ClimaComelitMode.OFF:
self._attr_hvac_action = HVACAction.OFF
if not _active:
self._attr_hvac_action = HVACAction.IDLE
elif _mode in API_STATUS:
if _mode in API_STATUS:
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
self._attr_hvac_mode = None

View File

@@ -7,7 +7,7 @@ from typing import Any, cast
from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
@@ -68,10 +68,16 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self._last_state in [None, "unknown"]:
return None
if self.device_status != STATE_COVER.index("stopped"):
return False
if self._last_action:
return self._last_action == STATE_COVER.index("closing")
return None
return self._last_state == CoverState.CLOSED
@property
def is_closing(self) -> bool:

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "bronze",
"requirements": ["aiocomelit==0.12.3"]
"requirements": ["aiocomelit==0.12.1"]
}

View File

@@ -55,8 +55,10 @@ rules:
docs-known-limitations:
status: exempt
comment: no known limitations, yet
docs-supported-devices: done
docs-supported-functions: done
docs-supported-devices:
status: todo
comment: review and complete missing ones
docs-supported-functions: todo
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:

View File

@@ -44,6 +44,7 @@ def websocket_list_areas(
vol.Optional("humidity_entity_id"): vol.Any(str, None),
vol.Optional("icon"): str,
vol.Optional("labels"): [str],
vol.Optional("motion_entity_id"): vol.Any(str, None),
vol.Required("name"): str,
vol.Optional("picture"): vol.Any(str, None),
vol.Optional("temperature_entity_id"): vol.Any(str, None),
@@ -112,6 +113,7 @@ def websocket_delete_area(
vol.Optional("humidity_entity_id"): vol.Any(str, None),
vol.Optional("icon"): vol.Any(str, None),
vol.Optional("labels"): [str],
vol.Optional("motion_entity_id"): vol.Any(str, None),
vol.Optional("name"): str,
vol.Optional("picture"): vol.Any(str, None),
vol.Optional("temperature_entity_id"): vol.Any(str, None),

View File

@@ -17,7 +17,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import CONF_MASTER_GATEWAY, DOMAIN, HASSIO_CONFIGURATION_URL, PLATFORMS
from ..const import (
CONF_MASTER_GATEWAY,
DOMAIN as DECONZ_DOMAIN,
HASSIO_CONFIGURATION_URL,
PLATFORMS,
)
from .config import DeconzConfig
if TYPE_CHECKING:
@@ -188,7 +193,7 @@ class DeconzHub:
config_entry_id=self.config_entry.entry_id,
configuration_url=configuration_url,
entry_type=dr.DeviceEntryType.SERVICE,
identifiers={(DOMAIN, self.api.config.bridge_id)},
identifiers={(DECONZ_DOMAIN, self.api.config.bridge_id)},
manufacturer="Dresden Elektronik",
model=self.api.config.model_id,
name=self.api.config.name,

View File

@@ -6,16 +6,12 @@ from datetime import datetime
from typing import Any
from homeassistant.components.media_player import (
BrowseMedia,
MediaClass,
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
RepeatMode,
SearchMedia,
SearchMediaQuery,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -411,18 +407,3 @@ class DemoSearchPlayer(AbstractDemoPlayer):
"""A Demo media player that supports searching."""
_attr_supported_features = SEARCH_PLAYER_SUPPORT
async def async_search_media(self, query: SearchMediaQuery) -> SearchMedia:
"""Demo implementation of search media."""
return SearchMedia(
result=[
BrowseMedia(
title="Search result",
media_class=MediaClass.MOVIE,
media_content_type=MediaType.MOVIE,
media_content_id="search_result_id",
can_play=True,
can_expand=False,
)
]
)

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.1.1"],
"requirements": ["denonavr==1.1.0"],
"ssdp": [
{
"manufacturer": "Denon",

View File

@@ -8,7 +8,11 @@ import voluptuous as vol
from homeassistant.const import CONF_DOMAIN
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.trigger import (
TriggerActionType,
TriggerInfo,
TriggerProtocol,
)
from homeassistant.helpers.typing import ConfigType
from . import (
@@ -21,28 +25,13 @@ from .helpers import async_validate_device_automation_config
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
class DeviceAutomationTriggerProtocol(Protocol):
class DeviceAutomationTriggerProtocol(TriggerProtocol, Protocol):
"""Define the format of device_trigger modules.
Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config.
Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config
from TriggerProtocol.
"""
TRIGGER_SCHEMA: vol.Schema
async def async_validate_trigger_config(
self, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
async def async_attach_trigger(
self,
hass: HomeAssistant,
config: ConfigType,
action: TriggerActionType,
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
async def async_get_trigger_capabilities(
self, hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:

View File

@@ -7,7 +7,6 @@ from collections.abc import Callable
from datetime import timedelta
from fnmatch import translate
from functools import lru_cache, partial
from ipaddress import IPv4Address
import itertools
import logging
import re
@@ -23,7 +22,6 @@ from aiodiscover.discovery import (
from cached_ipaddress import cached_ip_addresses
from homeassistant import config_entries
from homeassistant.components import network
from homeassistant.components.device_tracker import (
ATTR_HOST_NAME,
ATTR_IP,
@@ -423,33 +421,9 @@ class DHCPWatcher(WatcherBase):
response.ip_address, response.hostname, response.mac_address
)
async def async_get_adapter_indexes(self) -> list[int] | None:
"""Get the adapter indexes."""
adapters = await network.async_get_adapters(self.hass)
if network.async_only_default_interface_enabled(adapters):
return None
return [
adapter["index"]
for adapter in adapters
if (
adapter["enabled"]
and adapter["index"] is not None
and adapter["ipv4"]
and (
addresses := [IPv4Address(ip["address"]) for ip in adapter["ipv4"]]
)
and any(
ip for ip in addresses if not ip.is_loopback and not ip.is_global
)
)
]
async def async_start(self) -> None:
"""Start watching for dhcp packets."""
self._unsub = await aiodhcpwatcher.async_start(
self._async_process_dhcp_request,
await self.async_get_adapter_indexes(),
)
self._unsub = await aiodhcpwatcher.async_start(self._async_process_dhcp_request)
class RediscoveryWatcher(WatcherBase):

View File

@@ -2,7 +2,6 @@
"domain": "dhcp",
"name": "DHCP Discovery",
"codeowners": ["@bdraco"],
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/dhcp",
"integration_type": "system",
"iot_class": "local_push",

View File

@@ -2,13 +2,32 @@
from __future__ import annotations
from http import HTTPStatus
import os
import re
import threading
import requests
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
from .const import _LOGGER, CONF_DOWNLOAD_DIR
from .services import register_services
from .const import (
_LOGGER,
ATTR_FILENAME,
ATTR_OVERWRITE,
ATTR_SUBDIR,
ATTR_URL,
CONF_DOWNLOAD_DIR,
DOMAIN,
DOWNLOAD_COMPLETED_EVENT,
DOWNLOAD_FAILED_EVENT,
SERVICE_DOWNLOAD_FILE,
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -25,6 +44,127 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
return False
register_services(hass)
def download_file(service: ServiceCall) -> None:
"""Start thread to download file specified in the URL."""
def do_download() -> None:
"""Download the file."""
try:
url = service.data[ATTR_URL]
subdir = service.data.get(ATTR_SUBDIR)
filename = service.data.get(ATTR_FILENAME)
overwrite = service.data.get(ATTR_OVERWRITE)
if subdir:
# Check the path
raise_if_invalid_path(subdir)
final_path = None
req = requests.get(url, stream=True, timeout=10)
if req.status_code != HTTPStatus.OK:
_LOGGER.warning(
"Downloading '%s' failed, status_code=%d", url, req.status_code
)
hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
else:
if filename is None and "content-disposition" in req.headers:
match = re.findall(
r"filename=(\S+)", req.headers["content-disposition"]
)
if match:
filename = match[0].strip("'\" ")
if not filename:
filename = os.path.basename(url).strip()
if not filename:
filename = "ha_download"
# Check the filename
raise_if_invalid_filename(filename)
# Do we want to download to subdir, create if needed
if subdir:
subdir_path = os.path.join(download_path, subdir)
# Ensure subdir exist
os.makedirs(subdir_path, exist_ok=True)
final_path = os.path.join(subdir_path, filename)
else:
final_path = os.path.join(download_path, filename)
path, ext = os.path.splitext(final_path)
# If file exist append a number.
# We test filename, filename_2..
if not overwrite:
tries = 1
final_path = path + ext
while os.path.isfile(final_path):
tries += 1
final_path = f"{path}_{tries}.{ext}"
_LOGGER.debug("%s -> %s", url, final_path)
with open(final_path, "wb") as fil:
for chunk in req.iter_content(1024):
fil.write(chunk)
_LOGGER.debug("Downloading of %s done", url)
hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}",
{"url": url, "filename": filename},
)
except requests.exceptions.ConnectionError:
_LOGGER.exception("ConnectionError occurred for %s", url)
hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):
os.remove(final_path)
except ValueError:
_LOGGER.exception("Invalid value")
hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):
os.remove(final_path)
threading.Thread(target=do_download).start()
async_register_admin_service(
hass,
DOMAIN,
SERVICE_DOWNLOAD_FILE,
download_file,
schema=vol.Schema(
{
vol.Optional(ATTR_FILENAME): cv.string,
vol.Optional(ATTR_SUBDIR): cv.string,
vol.Required(ATTR_URL): cv.url,
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
}
),
)
return True

View File

@@ -1,159 +0,0 @@
"""Support for functionality to download files."""
from __future__ import annotations
from http import HTTPStatus
import os
import re
import threading
import requests
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
from .const import (
_LOGGER,
ATTR_FILENAME,
ATTR_OVERWRITE,
ATTR_SUBDIR,
ATTR_URL,
CONF_DOWNLOAD_DIR,
DOMAIN,
DOWNLOAD_COMPLETED_EVENT,
DOWNLOAD_FAILED_EVENT,
SERVICE_DOWNLOAD_FILE,
)
def download_file(service: ServiceCall) -> None:
"""Start thread to download file specified in the URL."""
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
download_path = entry.data[CONF_DOWNLOAD_DIR]
def do_download() -> None:
"""Download the file."""
try:
url = service.data[ATTR_URL]
subdir = service.data.get(ATTR_SUBDIR)
filename = service.data.get(ATTR_FILENAME)
overwrite = service.data.get(ATTR_OVERWRITE)
if subdir:
# Check the path
raise_if_invalid_path(subdir)
final_path = None
req = requests.get(url, stream=True, timeout=10)
if req.status_code != HTTPStatus.OK:
_LOGGER.warning(
"Downloading '%s' failed, status_code=%d", url, req.status_code
)
service.hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
else:
if filename is None and "content-disposition" in req.headers:
match = re.findall(
r"filename=(\S+)", req.headers["content-disposition"]
)
if match:
filename = match[0].strip("'\" ")
if not filename:
filename = os.path.basename(url).strip()
if not filename:
filename = "ha_download"
# Check the filename
raise_if_invalid_filename(filename)
# Do we want to download to subdir, create if needed
if subdir:
subdir_path = os.path.join(download_path, subdir)
# Ensure subdir exist
os.makedirs(subdir_path, exist_ok=True)
final_path = os.path.join(subdir_path, filename)
else:
final_path = os.path.join(download_path, filename)
path, ext = os.path.splitext(final_path)
# If file exist append a number.
# We test filename, filename_2..
if not overwrite:
tries = 1
final_path = path + ext
while os.path.isfile(final_path):
tries += 1
final_path = f"{path}_{tries}.{ext}"
_LOGGER.debug("%s -> %s", url, final_path)
with open(final_path, "wb") as fil:
for chunk in req.iter_content(1024):
fil.write(chunk)
_LOGGER.debug("Downloading of %s done", url)
service.hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}",
{"url": url, "filename": filename},
)
except requests.exceptions.ConnectionError:
_LOGGER.exception("ConnectionError occurred for %s", url)
service.hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):
os.remove(final_path)
except ValueError:
_LOGGER.exception("Invalid value")
service.hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):
os.remove(final_path)
threading.Thread(target=do_download).start()
def register_services(hass: HomeAssistant) -> None:
"""Register the services for the downloader component."""
async_register_admin_service(
hass,
DOMAIN,
SERVICE_DOWNLOAD_FILE,
download_file,
schema=vol.Schema(
{
vol.Optional(ATTR_FILENAME): cv.string,
vol.Optional(ATTR_SUBDIR): cv.string,
vol.Required(ATTR_URL): cv.url,
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
}
),
)

View File

@@ -148,6 +148,11 @@ class EcoNetThermostat(EcoNetEntity[Thermostat], ClimateEntity):
if target_temp_low or target_temp_high:
self._econet.set_set_point(None, target_temp_high, target_temp_low)
@property
def is_aux_heat(self) -> bool:
"""Return true if aux heater."""
return self._econet.mode == ThermostatOperationMode.EMERGENCY_HEAT
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool, mode.

View File

@@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
from .util import get_supported_entities
from .util import get_supported_entitites
@dataclass(kw_only=True, frozen=True)
@@ -49,7 +49,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
async_add_entities(
get_supported_entities(
get_supported_entitites(
config_entry.runtime_data, EcovacsBinarySensor, ENTITY_DESCRIPTIONS
)
)

View File

@@ -16,13 +16,13 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry
from .const import SUPPORTED_LIFESPANS
from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS
from .entity import (
EcovacsCapabilityEntityDescription,
EcovacsDescriptionEntity,
EcovacsEntity,
)
from .util import get_supported_entities
from .util import get_supported_entitites
@dataclass(kw_only=True, frozen=True)
@@ -62,7 +62,7 @@ STATION_ENTITY_DESCRIPTIONS = tuple(
key=f"station_action_{action.name.lower()}",
translation_key=f"station_action_{action.name.lower()}",
)
for action in StationAction
for action in SUPPORTED_STATION_ACTIONS
)
@@ -85,7 +85,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities: list[EcovacsEntity] = get_supported_entities(
entities: list[EcovacsEntity] = get_supported_entitites(
controller, EcovacsButtonEntity, ENTITY_DESCRIPTIONS
)
entities.extend(

View File

@@ -172,13 +172,7 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {}
if user_input:
self._async_abort_entries_match(
{
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_OVERRIDE_REST_URL: user_input.get(CONF_OVERRIDE_REST_URL),
CONF_OVERRIDE_MQTT_URL: user_input.get(CONF_OVERRIDE_MQTT_URL),
}
)
self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]})
errors = await _validate_input(self.hass, user_input)
@@ -220,9 +214,6 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=self.add_suggested_values_to_schema(
data_schema=vol.Schema(schema), suggested_values=user_input
),
description_placeholders={
"account_name": "Ecovacs",
},
errors=errors,
last_step=True,
)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==13.2.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"]
}

View File

@@ -25,7 +25,7 @@ from .entity import (
EcovacsEntity,
EventT,
)
from .util import get_supported_entities
from .util import get_supported_entitites
@dataclass(kw_only=True, frozen=True)
@@ -87,7 +87,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities: list[EcovacsEntity] = get_supported_entities(
entities: list[EcovacsEntity] = get_supported_entitites(
controller, EcovacsNumberEntity, ENTITY_DESCRIPTIONS
)
if entities:

View File

@@ -1,95 +0,0 @@
rules:
# Bronze
config-flow: done
test-before-configure: done
unique-config-entry: done
config-flow-test-coverage: done
runtime-data: done
test-before-setup:
status: todo
comment: Legacy code will not raise on setup currently
appropriate-polling:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client pulls only once at beginning and afterwards is pushed based
entity-unique-id: done
has-entity-name: done
entity-event-setup: done
dependency-transparency:
status: todo
comment: Currently unsure if all dependencies need to validated or only direct ones.
action-setup:
status: done
comment: "`raw_get_positions` is a entity service"
common-modules: done
docs-high-level-description: todo
docs-installation-instructions: todo
docs-removal-instructions: todo
docs-actions: todo
brands: done
# Silver
config-entry-unloading: done
log-when-unavailable: todo
entity-unavailable: done
action-exceptions: todo
reauthentication-flow: todo
parallel-updates:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client uses internally semaphores to prevent to many parallel requests
test-coverage: todo
integration-owner: done
docs-installation-parameters: todo
docs-configuration-parameters: todo
# Gold
entity-translations:
status: todo
comment: |
@mib1185 Legacy entities are not translated
entity-device-class: done
devices: done
entity-category: done
entity-disabled-by-default: done
discovery:
status: exempt
comment: Not supported as we don't talk directly to the devices
stale-devices: todo
diagnostics: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
dynamic-devices:
status: todo
comment: New devices are discovered only on boot currently
discovery-update-info:
status: exempt
comment: Not supported as we don't talk directly to the devices
repair-issues: todo
docs-use-cases: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-data-update: todo
docs-known-limitations: todo
docs-troubleshooting: todo
docs-examples: todo
# Platinum
async-dependency:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client is async
inject-websession:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client uses the passed websession
strict-typing:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client is typed

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
from .util import get_name_key, get_supported_entities
from .util import get_name_key, get_supported_entitites
@dataclass(kw_only=True, frozen=True)
@@ -59,7 +59,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities = get_supported_entities(
entities = get_supported_entitites(
controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS
)
if entities:

View File

@@ -6,8 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
from deebot_client.device import Device
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan
from deebot_client.events import (
BatteryEvent,
ErrorEvent,
@@ -35,7 +34,7 @@ from homeassistant.const import (
UnitOfArea,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -48,7 +47,7 @@ from .entity import (
EcovacsLegacyEntity,
EventT,
)
from .util import get_name_key, get_options, get_supported_entities
from .util import get_name_key, get_options, get_supported_entitites
@dataclass(kw_only=True, frozen=True)
@@ -60,15 +59,6 @@ class EcovacsSensorEntityDescription(
"""Ecovacs sensor entity description."""
value_fn: Callable[[EventT], StateType]
native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None
@callback
def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
"""Get the area native unit of measurement based on device type."""
if device_type is DeviceType.MOWER:
return UnitOfArea.SQUARE_CENTIMETERS
return UnitOfArea.SQUARE_METERS
ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
@@ -78,9 +68,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
capability_fn=lambda caps: caps.stats.clean,
value_fn=lambda e: e.area,
translation_key="stats_area",
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
EcovacsSensorEntityDescription[StatsEvent](
key="stats_time",
@@ -97,10 +85,8 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
value_fn=lambda e: e.area,
key="total_stats_area",
translation_key="total_stats_area",
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
EcovacsSensorEntityDescription[TotalStatsEvent](
capability_fn=lambda caps: caps.stats.total,
@@ -211,7 +197,7 @@ async def async_setup_entry(
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities: list[EcovacsEntity] = get_supported_entities(
entities: list[EcovacsEntity] = get_supported_entitites(
controller, EcovacsSensor, ENTITY_DESCRIPTIONS
)
entities.extend(
@@ -263,27 +249,6 @@ class EcovacsSensor(
entity_description: EcovacsSensorEntityDescription
def __init__(
self,
device: Device,
capability: CapabilityEvent,
entity_description: EcovacsSensorEntityDescription,
**kwargs: Any,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, entity_description, **kwargs)
if (
entity_description.native_unit_of_measurement_fn
and (
native_unit_of_measurement
:= entity_description.native_unit_of_measurement_fn(
device.capabilities.device_type
)
)
is not None
):
self._attr_native_unit_of_measurement = native_unit_of_measurement
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()

View File

@@ -22,12 +22,8 @@
"verify_mqtt_certificate": "Verify MQTT SSL certificate"
},
"data_description": {
"country": "The country of your {account_name} account.",
"override_rest_url": "Enter the REST URL of your self-hosted instance including the scheme (http/https).",
"override_mqtt_url": "Enter the MQTT URL of your self-hosted instance including the scheme (mqtt/mqtts).",
"password": "[%key:common::config_flow::data_description::password%]",
"username": "[%key:common::config_flow::data_description::username%]",
"verify_mqtt_certificate": "Should SSL certificates be verified? Uncheck this checkbox only if you are using a self-signed certificate."
"override_mqtt_url": "Enter the MQTT URL of your self-hosted instance including the scheme (mqtt/mqtts)."
}
},
"user": {

View File

@@ -17,7 +17,7 @@ from .entity import (
EcovacsDescriptionEntity,
EcovacsEntity,
)
from .util import get_supported_entities
from .util import get_supported_entitites
@dataclass(kw_only=True, frozen=True)
@@ -109,7 +109,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities: list[EcovacsEntity] = get_supported_entities(
entities: list[EcovacsEntity] = get_supported_entitites(
controller, EcovacsSwitchEntity, ENTITY_DESCRIPTIONS
)
if entities:

View File

@@ -32,7 +32,7 @@ def get_client_device_id(hass: HomeAssistant, self_hosted: bool) -> str:
)
def get_supported_entities(
def get_supported_entitites(
controller: EcovacsController,
entity_class: type[EcovacsDescriptionEntity],
descriptions: tuple[EcovacsCapabilityEntityDescription, ...],

View File

@@ -13,7 +13,6 @@ PLATFORMS = [
Platform.CLIMATE,
Platform.LIGHT,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TIME,

View File

@@ -15,12 +15,6 @@
},
"night_temperature_offset": {
"default": "mdi:thermometer"
},
"system_led": {
"default": "mdi:led-on",
"state": {
"0": "mdi:led-off"
}
}
},
"sensor": {

View File

@@ -109,20 +109,6 @@ HEATER_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalHeater], ..
),
)
GENERAL_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalDevice], ...] = (
EheimDigitalNumberDescription[EheimDigitalDevice](
key="system_led",
translation_key="system_led",
entity_category=EntityCategory.CONFIG,
native_min_value=0,
native_max_value=100,
native_step=PRECISION_WHOLE,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.sys_led,
set_value_fn=lambda device, value: device.set_sys_led(int(value)),
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -152,10 +138,6 @@ async def async_setup_entry(
)
for description in HEATER_DESCRIPTIONS
)
entities.extend(
EheimDigitalNumber[EheimDigitalDevice](coordinator, device, description)
for description in GENERAL_DESCRIPTIONS
)
async_add_entities(entities)

View File

@@ -1,102 +0,0 @@
"""EHEIM Digital select entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Generic, TypeVar, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import FilterMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity
PARALLEL_UPDATES = 0
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
@dataclass(frozen=True, kw_only=True)
class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]):
"""Class describing EHEIM Digital select entities."""
value_fn: Callable[[_DeviceT_co], str | None]
set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]]
CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalSelectDescription[EheimDigitalClassicVario], ...
] = (
EheimDigitalSelectDescription[EheimDigitalClassicVario](
key="filter_mode",
translation_key="filter_mode",
value_fn=(
lambda device: device.filter_mode.name.lower()
if device.filter_mode is not None
else None
),
set_value_fn=(
lambda device, value: device.set_filter_mode(FilterMode[value.upper()])
),
options=[name.lower() for name in FilterMode.__members__],
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: EheimDigitalConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the callbacks for the coordinator so select entities can be added as devices are found."""
coordinator = entry.runtime_data
def async_setup_device_entities(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the number entities for one or multiple devices."""
entities: list[EheimDigitalSelect[EheimDigitalDevice]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalClassicVario):
entities.extend(
EheimDigitalSelect[EheimDigitalClassicVario](
coordinator, device, description
)
for description in CLASSICVARIO_DESCRIPTIONS
)
async_add_entities(entities)
coordinator.add_platform_callback(async_setup_device_entities)
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalSelect(
EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co]
):
"""Represent an EHEIM Digital select entity."""
entity_description: EheimDigitalSelectDescription[_DeviceT_co]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT_co,
description: EheimDigitalSelectDescription[_DeviceT_co],
) -> None:
"""Initialize an EHEIM Digital select entity."""
super().__init__(coordinator, device)
self.entity_description = description
self._attr_unique_id = f"{self._device_address}_{description.key}"
@override
async def async_select_option(self, option: str) -> None:
return await self.entity_description.set_value_fn(self._device, option)
@override
def _async_update_attrs(self) -> None:
self._attr_current_option = self.entity_description.value_fn(self._device)

View File

@@ -62,19 +62,6 @@
},
"night_temperature_offset": {
"name": "Night temperature offset"
},
"system_led": {
"name": "System LED brightness"
}
},
"select": {
"filter_mode": {
"name": "Filter mode",
"state": {
"manual": "Manual",
"pulse": "Pulse",
"bio": "Bio"
}
}
},
"sensor": {

View File

@@ -20,8 +20,10 @@ from homeassistant.components.climate import (
from homeassistant.const import PRECISION_WHOLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from . import ElkM1ConfigEntry
from .const import DOMAIN
from .entity import ElkEntity, create_elk_entities
SUPPORT_HVAC = [
@@ -76,6 +78,7 @@ class ElkThermostat(ElkEntity, ClimateEntity):
_attr_precision = PRECISION_WHOLE
_attr_supported_features = (
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.AUX_HEAT
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
@@ -125,6 +128,11 @@ class ElkThermostat(ElkEntity, ClimateEntity):
"""Return the current humidity."""
return self._element.humidity
@property
def is_aux_heat(self) -> bool:
"""Return if aux heater is on."""
return self._element.mode == ThermostatMode.EMERGENCY_HEAT
@property
def fan_mode(self) -> str | None:
"""Return the fan setting."""
@@ -143,6 +151,34 @@ class ElkThermostat(ElkEntity, ClimateEntity):
thermostat_mode, fan_mode = HASS_TO_ELK_HVAC_MODES[hvac_mode]
self._elk_set(thermostat_mode, fan_mode)
async def async_turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
async_create_issue(
self.hass,
DOMAIN,
"migrate_aux_heat",
breaks_in_ha_version="2025.4.0",
is_fixable=True,
is_persistent=True,
translation_key="migrate_aux_heat",
severity=IssueSeverity.WARNING,
)
self._elk_set(ThermostatMode.EMERGENCY_HEAT, None)
async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
async_create_issue(
self.hass,
DOMAIN,
"migrate_aux_heat",
breaks_in_ha_version="2025.4.0",
is_fixable=True,
is_persistent=True,
translation_key="migrate_aux_heat",
severity=IssueSeverity.WARNING,
)
self._elk_set(ThermostatMode.HEAT, None)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
thermostat_mode, elk_fan_mode = HASS_TO_ELK_FAN_MODES[fan_mode]

View File

@@ -189,5 +189,18 @@
"name": "Sensor zone trigger",
"description": "Triggers zone."
}
},
"issues": {
"migrate_aux_heat": {
"title": "Migration of Elk-M1 set_aux_heat action",
"fix_flow": {
"step": {
"confirm": {
"description": "The Elk-M1 `set_aux_heat` action has been migrated. A new emergency heat switch entity is available for each thermostat.\n\nUpdate any automations to use the new emergency heat switch entity. When this is done, select **Submit** to fix this issue.",
"title": "[%key:component::elkm1::issues::migrate_aux_heat::title%]"
}
}
}
}
}
}

View File

@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.8"]
"requirements": ["sense-energy==0.13.7"]
}

View File

@@ -11,6 +11,7 @@ from pyephember2.pyephember2 import (
ZoneMode,
zone_current_temperature,
zone_is_active,
zone_is_boost_active,
zone_is_hotwater,
zone_mode,
zone_name,
@@ -101,6 +102,7 @@ class EphEmberThermostat(ClimateEntity):
self._attr_name = self._zone_name
if self._hot_water:
self._attr_supported_features = ClimateEntityFeature.AUX_HEAT
self._attr_target_temperature_step = None
else:
self._attr_target_temperature_step = 0.5
@@ -142,6 +144,22 @@ class EphEmberThermostat(ClimateEntity):
else:
_LOGGER.error("Invalid operation mode provided %s", hvac_mode)
@property
def is_aux_heat(self) -> bool:
"""Return true if aux heater."""
return zone_is_boost_active(self._zone)
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
self._ember.activate_boost_by_name(
self._zone_name, zone_target_temperature(self._zone)
)
def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
self._ember.deactivate_boost_by_name(self._zone_name)
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:

View File

@@ -239,6 +239,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
self._states = cast(dict[int, _StateT], entry_data.state[state_type])
assert entry_data.device_info is not None
device_info = entry_data.device_info
self._device_info = device_info
self._on_entry_data_changed()
self._key = entity_info.key
self._state_type = state_type
@@ -326,11 +327,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
@callback
def _on_entry_data_changed(self) -> None:
entry_data = self._entry_data
# Update the device info since it can change
# when the device is reconnected
if TYPE_CHECKING:
assert entry_data.device_info is not None
self._device_info = entry_data.device_info
self._api_version = entry_data.api_version
self._client = entry_data.client
if self._device_info.has_deep_sleep:

View File

@@ -63,7 +63,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
if self._supports_speed_levels:
data["speed_level"] = math.ceil(
percentage_to_ranged_value(
(1, self._static_info.supported_speed_count), percentage
(1, self._static_info.supported_speed_levels), percentage
)
)
else:
@@ -121,7 +121,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
)
return ranged_value_to_percentage(
(1, self._static_info.supported_speed_count), self._state.speed_level
(1, self._static_info.supported_speed_levels), self._state.speed_level
)
@property
@@ -164,7 +164,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
if not supports_speed_levels:
self._attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS)
else:
self._attr_speed_count = static_info.supported_speed_count
self._attr_speed_count = static_info.supported_speed_levels
async_setup_entry = partial(

View File

@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, cast
from aioesphomeapi import (
APIVersion,
ColorMode as ESPHomeColorMode,
EntityInfo,
LightColorCapability,
LightInfo,
@@ -107,15 +106,15 @@ def _mired_to_kelvin(mired_temperature: float) -> int:
@lru_cache
def _color_mode_to_ha(mode: ESPHomeColorMode) -> ColorMode:
def _color_mode_to_ha(mode: int) -> str:
"""Convert an esphome color mode to a HA color mode constant.
Choose the color mode that best matches the feature-set.
"""
candidates: list[tuple[ColorMode, LightColorCapability]] = []
candidates = []
for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items():
for caps in cap_lists:
if caps.value == mode:
if caps == mode:
# exact match
return ha_mode
if (mode & caps) == caps:
@@ -132,8 +131,8 @@ def _color_mode_to_ha(mode: ESPHomeColorMode) -> ColorMode:
@lru_cache
def _filter_color_modes(
supported: list[ESPHomeColorMode], features: LightColorCapability
) -> tuple[ESPHomeColorMode, ...]:
supported: list[int], features: LightColorCapability
) -> tuple[int, ...]:
"""Filter the given supported color modes.
Excluding all values that don't have the requested features.
@@ -157,7 +156,7 @@ def _least_complex_color_mode(color_modes: tuple[int, ...]) -> int:
class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
"""A light implementation for ESPHome."""
_native_supported_color_modes: tuple[ESPHomeColorMode, ...]
_native_supported_color_modes: tuple[int, ...]
_supports_color_mode = False
@property

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==31.1.0",
"aioesphomeapi==30.2.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.15.1"
],

View File

@@ -88,9 +88,9 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
return
if (
state_class == EsphomeSensorStateClass.MEASUREMENT
and static_info.legacy_last_reset_type == LastResetType.AUTO
and static_info.last_reset_type == LastResetType.AUTO
):
# Legacy, legacy_last_reset_type auto was the equivalent to the
# Legacy, last_reset_type auto was the equivalent to the
# TOTAL_INCREASING state class
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
else:

View File

@@ -35,7 +35,7 @@ async def validate_host(
hass: HomeAssistant, host: str
) -> tuple[str, FroniusConfigEntryData]:
"""Validate the user input allows us to connect."""
fronius = Fronius(async_get_clientsession(hass, verify_ssl=False), host)
fronius = Fronius(async_get_clientsession(hass), host)
try:
datalogger_info: dict[str, Any]

View File

@@ -4,13 +4,13 @@
"current_dc": {
"default": "mdi:current-dc"
},
"current_dc_mppt_no": {
"current_dc_2": {
"default": "mdi:current-dc"
},
"voltage_dc": {
"default": "mdi:current-dc"
},
"voltage_dc_mppt_no": {
"voltage_dc_2": {
"default": "mdi:current-dc"
},
"co2_factor": {

View File

@@ -168,26 +168,6 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_dc_mppt_no",
translation_placeholders={"mppt_no": "2"},
),
FroniusSensorEntityDescription(
key="current_dc_3",
default_value=0,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_dc_mppt_no",
translation_placeholders={"mppt_no": "3"},
),
FroniusSensorEntityDescription(
key="current_dc_4",
default_value=0,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_dc_mppt_no",
translation_placeholders={"mppt_no": "4"},
),
FroniusSensorEntityDescription(
key="power_ac",
@@ -217,26 +197,6 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_dc_mppt_no",
translation_placeholders={"mppt_no": "2"},
),
FroniusSensorEntityDescription(
key="voltage_dc_3",
default_value=0,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_dc_mppt_no",
translation_placeholders={"mppt_no": "3"},
),
FroniusSensorEntityDescription(
key="voltage_dc_4",
default_value=0,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_dc_mppt_no",
translation_placeholders={"mppt_no": "4"},
),
# device status entities
FroniusSensorEntityDescription(
@@ -767,7 +727,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
self.response_key = description.response_key or description.key
self.solar_net_id = solar_net_id
self._attr_native_value = self._get_entity_value()
self._attr_translation_key = description.translation_key or description.key
self._attr_translation_key = description.key
def _device_data(self) -> dict[str, Any]:
"""Extract information for SolarNet device from coordinator data."""

View File

@@ -52,8 +52,8 @@
"current_dc": {
"name": "DC current"
},
"current_dc_mppt_no": {
"name": "DC current {mppt_no}"
"current_dc_2": {
"name": "DC current 2"
},
"power_ac": {
"name": "AC power"
@@ -64,8 +64,8 @@
"voltage_dc": {
"name": "DC voltage"
},
"voltage_dc_mppt_no": {
"name": "DC voltage {mppt_no}"
"voltage_dc_2": {
"name": "DC voltage 2"
},
"inverter_state": {
"name": "Inverter state"

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250516.0"]
"requirements": ["home-assistant-frontend==20250509.0"]
}

View File

@@ -20,12 +20,9 @@ from homeassistant.helpers import config_entry_flow, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
type GeofencyConfigEntry = ConfigEntry[set[str]]
PLATFORMS = [Platform.DEVICE_TRACKER]
CONF_MOBILE_BEACONS = "mobile_beacons"
@@ -78,13 +75,16 @@ WEBHOOK_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
_DATA_GEOFENCY: HassKey[list[str]] = HassKey(DOMAIN)
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Geofency component."""
mobile_beacons = hass_config.get(DOMAIN, {}).get(CONF_MOBILE_BEACONS, [])
hass.data[_DATA_GEOFENCY] = [slugify(beacon) for beacon in mobile_beacons]
config = hass_config.get(DOMAIN, {})
mobile_beacons = config.get(CONF_MOBILE_BEACONS, [])
hass.data[DOMAIN] = {
"beacons": [slugify(beacon) for beacon in mobile_beacons],
"devices": set(),
"unsub_device_tracker": {},
}
return True
@@ -99,7 +99,7 @@ async def handle_webhook(
text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY
)
if _is_mobile_beacon(data, hass.data[_DATA_GEOFENCY]):
if _is_mobile_beacon(data, hass.data[DOMAIN]["beacons"]):
return _set_location(hass, data, None)
if data["entry"] == LOCATION_ENTRY:
location_name = data["name"]
@@ -140,9 +140,8 @@ def _set_location(hass, data, location_name):
return web.Response(text=f"Setting location for {device}")
async def async_setup_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure based on config entry."""
entry.runtime_data = set()
webhook.async_register(
hass, DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
@@ -151,9 +150,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) ->
return True
async def async_unload_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,6 +1,7 @@
"""Support for the Geofency device tracker platform."""
from homeassistant.components.device_tracker import TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
@@ -9,13 +10,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import TRACKER_UPDATE, GeofencyConfigEntry
from .const import DOMAIN
from . import DOMAIN, TRACKER_UPDATE
async def async_setup_entry(
hass: HomeAssistant,
config_entry: GeofencyConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Geofency config entry."""
@@ -23,16 +23,14 @@ async def async_setup_entry(
@callback
def _receive_data(device, gps, location_name, attributes):
"""Fire HA event to set location."""
if device in config_entry.runtime_data:
if device in hass.data[DOMAIN]["devices"]:
return
config_entry.runtime_data.add(device)
hass.data[DOMAIN]["devices"].add(device)
async_add_entities(
[GeofencyEntity(config_entry, device, gps, location_name, attributes)]
)
async_add_entities([GeofencyEntity(device, gps, location_name, attributes)])
config_entry.async_on_unload(
hass.data[DOMAIN]["unsub_device_tracker"][config_entry.entry_id] = (
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
)
@@ -47,8 +45,8 @@ async def async_setup_entry(
}
if dev_ids:
config_entry.runtime_data.update(dev_ids)
async_add_entities(GeofencyEntity(config_entry, dev_id) for dev_id in dev_ids)
hass.data[DOMAIN]["devices"].update(dev_ids)
async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids)
class GeofencyEntity(TrackerEntity, RestoreEntity):
@@ -57,9 +55,8 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
_attr_has_entity_name = True
_attr_name = None
def __init__(self, entry, device, gps=None, location_name=None, attributes=None):
def __init__(self, device, gps=None, location_name=None, attributes=None):
"""Set up Geofency entity."""
self._entry = entry
self._attr_extra_state_attributes = attributes or {}
self._name = device
self._attr_location_name = location_name
@@ -96,7 +93,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
"""Clean up after entity before removal."""
await super().async_will_remove_from_hass()
self._unsub_dispatcher()
self._entry.runtime_data.remove(self.unique_id)
self.hass.data[DOMAIN]["devices"].remove(self.unique_id)
@callback
def _async_receive_data(self, device, gps, location_name, attributes):

View File

@@ -8,6 +8,6 @@
"integration_type": "system",
"iot_class": "local_polling",
"quality_scale": "internal",
"requirements": ["go2rtc-client==0.1.3b0"],
"requirements": ["go2rtc-client==0.1.2"],
"single_config_entry": true
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"]
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/google_travel_time",
"iot_class": "cloud_polling",
"loggers": ["google", "homeassistant.helpers.location"],
"requirements": ["google-maps-routing==0.6.15"]
"requirements": ["google-maps-routing==0.6.14"]
}

View File

@@ -24,8 +24,6 @@ from .const import (
DOMAIN,
)
type GPSLoggerConfigEntry = ConfigEntry[set[str]]
PLATFORMS = [Platform.DEVICE_TRACKER]
TRACKER_UPDATE = f"{DOMAIN}_tracker_update"
@@ -90,9 +88,9 @@ async def handle_webhook(
return web.Response(text=f"Setting location for {device}")
async def async_setup_entry(hass: HomeAssistant, entry: GPSLoggerConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure based on config entry."""
entry.runtime_data = set()
hass.data.setdefault(DOMAIN, {"devices": set(), "unsub_device_tracker": {}})
webhook.async_register(
hass, DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
@@ -105,6 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GPSLoggerConfigEntry) ->
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,6 +1,7 @@
"""Support for the GPSLogger device tracking."""
from homeassistant.components.device_tracker import TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_GPS_ACCURACY,
@@ -14,20 +15,19 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import TRACKER_UPDATE, GPSLoggerConfigEntry
from . import DOMAIN, TRACKER_UPDATE
from .const import (
ATTR_ACTIVITY,
ATTR_ALTITUDE,
ATTR_DIRECTION,
ATTR_PROVIDER,
ATTR_SPEED,
DOMAIN,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: GPSLoggerConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Configure a dispatcher connection based on a config entry."""
@@ -35,14 +35,16 @@ async def async_setup_entry(
@callback
def _receive_data(device, gps, battery, accuracy, attrs):
"""Receive set location."""
if device in entry.runtime_data:
if device in hass.data[DOMAIN]["devices"]:
return
entry.runtime_data.add(device)
hass.data[DOMAIN]["devices"].add(device)
async_add_entities([GPSLoggerEntity(device, gps, battery, accuracy, attrs)])
entry.async_on_unload(async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data))
hass.data[DOMAIN]["unsub_device_tracker"][entry.entry_id] = (
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
)
# Restore previously loaded devices
dev_reg = dr.async_get(hass)
@@ -56,7 +58,7 @@ async def async_setup_entry(
entities = []
for dev_id in dev_ids:
entry.runtime_data.add(dev_id)
hass.data[DOMAIN]["devices"].add(dev_id)
entity = GPSLoggerEntity(dev_id, None, None, None, None)
entities.append(entity)

View File

@@ -1,29 +1,33 @@
"""The Gree Climate integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_track_time_interval
from .const import DISCOVERY_SCAN_INTERVAL
from .coordinator import DiscoveryService, GreeConfigEntry, GreeRuntimeData
from .const import (
COORDINATORS,
DATA_DISCOVERY_SERVICE,
DISCOVERY_SCAN_INTERVAL,
DISPATCHERS,
DOMAIN,
)
from .coordinator import DiscoveryService
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Gree Climate from a config entry."""
hass.data.setdefault(DOMAIN, {})
gree_discovery = DiscoveryService(hass, entry)
entry.runtime_data = GreeRuntimeData(
discovery_service=gree_discovery, coordinators=[]
)
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
async def _async_scan_update(_=None):
bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
@@ -43,6 +47,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool
return True
async def async_unload_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
hass.data.pop(DATA_DISCOVERY_SERVICE)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(COORDINATORS, None)
hass.data[DOMAIN].pop(DISPATCHERS, None)
return unload_ok

View File

@@ -36,18 +36,21 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
COORDINATORS,
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
FAN_MEDIUM_HIGH,
FAN_MEDIUM_LOW,
TARGET_TEMPERATURE_STEP,
)
from .coordinator import DeviceDataUpdateCoordinator, GreeConfigEntry
from .coordinator import DeviceDataUpdateCoordinator
from .entity import GreeEntity
_LOGGER = logging.getLogger(__name__)
@@ -84,17 +87,17 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH]
async def async_setup_entry(
hass: HomeAssistant,
entry: GreeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Gree HVAC device from a config entry."""
@callback
def init_device(coordinator: DeviceDataUpdateCoordinator) -> None:
def init_device(coordinator):
"""Register the device."""
async_add_entities([GreeClimateEntity(coordinator)])
for coordinator in entry.runtime_data.coordinators:
for coordinator in hass.data[DOMAIN][COORDINATORS]:
init_device(coordinator)
entry.async_on_unload(

View File

@@ -1,10 +1,16 @@
"""Constants for the Gree Climate integration."""
COORDINATORS = "coordinators"
DATA_DISCOVERY_SERVICE = "gree_discovery"
DISCOVERY_SCAN_INTERVAL = 300
DISCOVERY_TIMEOUT = 8
DISPATCH_DEVICE_DISCOVERED = "gree_device_discovered"
DISPATCHERS = "dispatchers"
DOMAIN = "gree"
COORDINATOR = "coordinator"
FAN_MEDIUM_LOW = "medium low"
FAN_MEDIUM_HIGH = "medium high"

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import copy
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from typing import Any
@@ -21,6 +20,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from homeassistant.util.dt import utcnow
from .const import (
COORDINATORS,
DISCOVERY_TIMEOUT,
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
@@ -31,24 +31,14 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
type GreeConfigEntry = ConfigEntry[GreeRuntimeData]
@dataclass
class GreeRuntimeData:
"""RUntime data for Gree Climate integration."""
discovery_service: DiscoveryService
coordinators: list[DeviceDataUpdateCoordinator]
class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Manages polling for state changes from the device."""
config_entry: GreeConfigEntry
config_entry: ConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: GreeConfigEntry, device: Device
self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device
) -> None:
"""Initialize the data update coordinator."""
super().__init__(
@@ -138,7 +128,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
class DiscoveryService(Listener):
"""Discovery event handler for gree devices."""
def __init__(self, hass: HomeAssistant, entry: GreeConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize discovery service."""
super().__init__()
self.hass = hass
@@ -147,6 +137,8 @@ class DiscoveryService(Listener):
self.discovery = Discovery(DISCOVERY_TIMEOUT)
self.discovery.add_listener(self)
hass.data[DOMAIN].setdefault(COORDINATORS, [])
async def device_found(self, device_info: DeviceInfo) -> None:
"""Handle new device found on the network."""
@@ -165,14 +157,14 @@ class DiscoveryService(Listener):
device.device_info.port,
)
coordo = DeviceDataUpdateCoordinator(self.hass, self.entry, device)
self.entry.runtime_data.coordinators.append(coordo)
self.hass.data[DOMAIN][COORDINATORS].append(coordo)
await coordo.async_refresh()
async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo)
async def device_update(self, device_info: DeviceInfo) -> None:
"""Handle updates in device information, update if ip has changed."""
for coordinator in self.entry.runtime_data.coordinators:
for coordinator in self.hass.data[DOMAIN][COORDINATORS]:
if coordinator.device.device_info.mac == device_info.mac:
coordinator.device.device_info.ip = device_info.ip
await coordinator.async_refresh()

View File

@@ -13,13 +13,13 @@ from homeassistant.components.switch import (
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DISPATCH_DEVICE_DISCOVERED
from .coordinator import GreeConfigEntry
from .entity import DeviceDataUpdateCoordinator, GreeEntity
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN
from .entity import GreeEntity
@dataclass(kw_only=True, frozen=True)
@@ -92,13 +92,13 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: GreeConfigEntry,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Gree HVAC device from a config entry."""
@callback
def init_device(coordinator: DeviceDataUpdateCoordinator) -> None:
def init_device(coordinator):
"""Register the device."""
async_add_entities(
@@ -106,7 +106,7 @@ async def async_setup_entry(
for description in GREE_SWITCHES
)
for coordinator in entry.runtime_data.coordinators:
for coordinator in hass.data[DOMAIN][COORDINATORS]:
init_device(coordinator)
entry.async_on_unload(

View File

@@ -1,14 +1,5 @@
"""Shared constants for the greeneye_monitor integration."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from greeneye import Monitors
CONF_CHANNELS = "channels"
CONF_COUNTED_QUANTITY = "counted_quantity"
CONF_COUNTED_QUANTITY_PER_PULSE = "counted_quantity_per_pulse"
@@ -22,8 +13,8 @@ CONF_TEMPERATURE_SENSORS = "temperature_sensors"
CONF_TIME_UNIT = "time_unit"
CONF_VOLTAGE_SENSORS = "voltage"
DATA_GREENEYE_MONITOR = "greeneye_monitor"
DOMAIN = "greeneye_monitor"
DATA_GREENEYE_MONITOR: HassKey[Monitors] = HassKey(DOMAIN)
SENSOR_TYPE_CURRENT = "current_sensor"
SENSOR_TYPE_PULSE_COUNTER = "pulse_counter"

View File

@@ -109,7 +109,7 @@ async def async_setup_platform(
if len(monitor_configs) == 0:
monitors.remove_listener(on_new_monitor)
monitors = hass.data[DATA_GREENEYE_MONITOR]
monitors: greeneye.Monitors = hass.data[DATA_GREENEYE_MONITOR]
monitors.add_listener(on_new_monitor)
for monitor in monitors.monitors.values():
on_new_monitor(monitor)

View File

@@ -52,10 +52,10 @@ type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
"""Habitica Data Update Coordinator."""
config_entry: HabiticaConfigEntry
config_entry: ConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: HabiticaConfigEntry, habitica: Habitica
self, hass: HomeAssistant, config_entry: ConfigEntry, habitica: Habitica
) -> None:
"""Initialize the Habitica data coordinator."""
super().__init__(

View File

@@ -234,7 +234,7 @@
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
"consumer_products_coffee_maker_program_beverage_hot_water": "Hot water",
"dishcare_dishwasher_program_pre_rinse": "Pre-rinse",
"dishcare_dishwasher_program_pre_rinse": "Pre_rinse",
"dishcare_dishwasher_program_auto_1": "Auto 1",
"dishcare_dishwasher_program_auto_2": "Auto 2",
"dishcare_dishwasher_program_auto_3": "Auto 3",
@@ -252,7 +252,7 @@
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
"dishcare_dishwasher_program_magic_daily": "Magic daily",
"dishcare_dishwasher_program_super_60": "Super 60ºC",
"dishcare_dishwasher_program_kurz_60": "Speed 60ºC",
"dishcare_dishwasher_program_kurz_60": "Kurz 60ºC",
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
"dishcare_dishwasher_program_machine_care": "Machine care",
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",

View File

@@ -90,17 +90,16 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
minor_version=2,
)
if config_entry.minor_version <= 3:
# Add a `firmware_version` key if it doesn't exist to handle entries created
# with minor version 1.3 where the firmware version was not set.
if config_entry.minor_version == 2:
# Add a `firmware_version` key
hass.config_entries.async_update_entry(
config_entry,
data={
**config_entry.data,
FIRMWARE_VERSION: config_entry.data.get(FIRMWARE_VERSION),
FIRMWARE_VERSION: None,
},
version=1,
minor_version=4,
minor_version=3,
)
_LOGGER.debug(

View File

@@ -62,7 +62,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
"""Handle a config flow for Home Assistant Yellow."""
VERSION = 1
MINOR_VERSION = 4
MINOR_VERSION = 3
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Instantiate config flow."""
@@ -116,11 +116,6 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
if self._probed_firmware_info is not None
else ApplicationType.EZSP
).value,
FIRMWARE_VERSION: (
self._probed_firmware_info.firmware_version
if self._probed_firmware_info is not None
else None
),
},
)

View File

@@ -15,7 +15,6 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,

View File

@@ -1,138 +0,0 @@
"""The Homee alarm control panel platform."""
from dataclasses import dataclass
from pyHomee.const import AttributeChangedBy, AttributeType
from pyHomee.model import HomeeAttribute
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityDescription,
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, HomeeConfigEntry
from .entity import HomeeEntity
from .helpers import get_name_for_enum
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class HomeeAlarmControlPanelEntityDescription(AlarmControlPanelEntityDescription):
"""A class that describes Homee alarm control panel entities."""
code_arm_required: bool = False
state_list: list[AlarmControlPanelState]
ALARM_DESCRIPTIONS = {
AttributeType.HOMEE_MODE: HomeeAlarmControlPanelEntityDescription(
key="homee_mode",
code_arm_required=False,
state_list=[
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelState.ARMED_VACATION,
],
)
}
def get_supported_features(
state_list: list[AlarmControlPanelState],
) -> AlarmControlPanelEntityFeature:
"""Return supported features based on the state list."""
supported_features = AlarmControlPanelEntityFeature(0)
if AlarmControlPanelState.ARMED_HOME in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_HOME
if AlarmControlPanelState.ARMED_AWAY in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY
if AlarmControlPanelState.ARMED_NIGHT in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT
if AlarmControlPanelState.ARMED_VACATION in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_VACATION
return supported_features
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the Homee platform for the alarm control panel component."""
async_add_entities(
HomeeAlarmPanel(attribute, config_entry, ALARM_DESCRIPTIONS[attribute.type])
for node in config_entry.runtime_data.nodes
for attribute in node.attributes
if attribute.type in ALARM_DESCRIPTIONS and attribute.editable
)
class HomeeAlarmPanel(HomeeEntity, AlarmControlPanelEntity):
"""Representation of a Homee alarm control panel."""
entity_description: HomeeAlarmControlPanelEntityDescription
def __init__(
self,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: HomeeAlarmControlPanelEntityDescription,
) -> None:
"""Initialize a Homee alarm control panel entity."""
super().__init__(attribute, entry)
self.entity_description = description
self._attr_code_arm_required = description.code_arm_required
self._attr_supported_features = get_supported_features(description.state_list)
self._attr_translation_key = description.key
@property
def alarm_state(self) -> AlarmControlPanelState:
"""Return current state."""
return self.entity_description.state_list[int(self._attribute.current_value)]
@property
def changed_by(self) -> str:
"""Return by whom or what the entity was last changed."""
changed_by_name = get_name_for_enum(
AttributeChangedBy, self._attribute.changed_by
)
return f"{changed_by_name} - {self._attribute.changed_by_id}"
async def _async_set_alarm_state(self, state: AlarmControlPanelState) -> None:
"""Set the alarm state."""
if state in self.entity_description.state_list:
await self.async_set_homee_value(
self.entity_description.state_list.index(state)
)
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
# Since disarm is always present in the UI, we raise an error.
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="disarm_not_supported",
)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_HOME)
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_NIGHT)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_AWAY)
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
"""Send arm vacation command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_VACATION)

View File

@@ -27,20 +27,14 @@ class HomeeEntity(Entity):
)
self._entry = entry
node = entry.runtime_data.get_node_by_id(attribute.node_id)
# Homee hub itself has node-id -1
if node.id == -1:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.runtime_data.settings.uid)},
)
else:
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
},
name=node.name,
model=get_name_for_enum(NodeProfile, node.profile),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
},
name=node.name,
model=get_name_for_enum(NodeProfile, node.profile),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)
self._host_connected = entry.runtime_data.connected

View File

@@ -26,11 +26,6 @@
}
},
"entity": {
"alarm_control_panel": {
"homee_mode": {
"name": "Status"
}
},
"binary_sensor": {
"blackout_alarm": {
"name": "Blackout"
@@ -375,9 +370,6 @@
"connection_closed": {
"message": "Could not connect to homee while setting attribute."
},
"disarm_not_supported": {
"message": "Disarm is not supported by homee."
},
"invalid_preset_mode": {
"message": "Invalid preset mode: {preset_mode}. Turning on is only supported with preset mode 'Manual'."
}

View File

@@ -3,6 +3,7 @@
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
@@ -20,7 +21,7 @@ from .const import (
HMIPC_HAPID,
HMIPC_NAME,
)
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
from .services import async_setup_services, async_unload_services
CONFIG_SCHEMA = vol.Schema(
@@ -44,6 +45,8 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the HomematicIP Cloud component."""
hass.data[DOMAIN] = {}
accesspoints = config.get(DOMAIN, [])
for conf in accesspoints:
@@ -66,7 +69,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up an access point from a config entry."""
# 0.104 introduced config entry unique id, this makes upgrading possible
@@ -78,8 +81,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
)
hap = HomematicipHAP(hass, entry)
hass.data[DOMAIN][entry.unique_id] = hap
entry.runtime_data = hap
if not await hap.async_setup():
return False
@@ -107,12 +110,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: HomematicIPConfigEntry
) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
hap = entry.runtime_data
assert hap.reset_connection_listener is not None
hap = hass.data[DOMAIN].pop(entry.unique_id)
hap.reset_connection_listener()
await async_unload_services(hass)
@@ -122,7 +122,7 @@ async def async_unload_entry(
@callback
def _async_remove_obsolete_entities(
hass: HomeAssistant, entry: HomematicIPConfigEntry, hap: HomematicipHAP
hass: HomeAssistant, entry: ConfigEntry, hap: HomematicipHAP
):
"""Remove obsolete entities from entity registry."""

View File

@@ -11,12 +11,13 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .hap import AsyncHome, HomematicIPConfigEntry, HomematicipHAP
from .hap import AsyncHome, HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -25,11 +26,11 @@ CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP alrm control panel from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
async_add_entities([HomematicipAlarmControlPanelEntity(hap)])

View File

@@ -34,13 +34,14 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode"
ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position"
@@ -74,11 +75,11 @@ SAM_DEVICE_ATTRIBUTES = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
entities: list[HomematicipGenericEntity] = [HomematicipCloudConnectionSensor(hap)]
for device in hap.home.devices:
if isinstance(device, AccelerationSensor):

View File

@@ -5,20 +5,22 @@ from __future__ import annotations
from homematicip.device import WallMountedGarageDoorController
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP button from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
async_add_entities(
HomematicipGarageDoorControllerButton(hap, device)

View File

@@ -24,6 +24,7 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@@ -31,7 +32,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2}
COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5}
@@ -54,11 +55,11 @@ HMIP_ECO_CM = "ECO"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP climate from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
async_add_entities(
HomematicipHeatingGroup(hap, device)

View File

@@ -21,11 +21,13 @@ from homeassistant.components.cover import (
CoverDeviceClass,
CoverEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
HMIP_COVER_OPEN = 0
HMIP_COVER_CLOSED = 1
@@ -35,11 +37,11 @@ HMIP_SLATS_CLOSED = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP cover from a config entry."""
hap = config_entry.runtime_data
hap = hass.data[DOMAIN][config_entry.unique_id]
entities: list[HomematicipGenericEntity] = [
HomematicipCoverShutterGroup(hap, group)
for group in hap.home.groups

View File

@@ -1,11 +1,8 @@
"""Support for HomematicIP Cloud events."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from homematicip.base.channel_event import ChannelEvent
from homematicip.base.functionalChannels import FunctionalChannel
from homematicip.device import Device
from homeassistant.components.event import (
@@ -13,20 +10,19 @@ from homeassistant.components.event import (
EventEntity,
EventEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .hap import HomematicipHAP
@dataclass(frozen=True, kw_only=True)
class HmipEventEntityDescription(EventEntityDescription):
"""Description of a HomematicIP Cloud event."""
channel_event_types: list[str] | None = None
channel_selector_fn: Callable[[FunctionalChannel], bool] | None = None
EVENT_DESCRIPTIONS = {
"doorbell": HmipEventEntityDescription(
@@ -34,42 +30,35 @@ EVENT_DESCRIPTIONS = {
translation_key="doorbell",
device_class=EventDeviceClass.DOORBELL,
event_types=["ring"],
channel_event_types=["DOOR_BELL_SENSOR_EVENT"],
channel_selector_fn=lambda channel: channel.channelRole == "DOOR_BELL_INPUT",
),
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomematicIPConfigEntry,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP cover from a config entry."""
hap = config_entry.runtime_data
entities: list[HomematicipGenericEntity] = []
hap = hass.data[DOMAIN][config_entry.unique_id]
entities.extend(
async_add_entities(
HomematicipDoorBellEvent(
hap,
device,
channel.index,
description,
EVENT_DESCRIPTIONS["doorbell"],
)
for description in EVENT_DESCRIPTIONS.values()
for device in hap.home.devices
for channel in device.functionalChannels
if description.channel_selector_fn and description.channel_selector_fn(channel)
if channel.channelRole == "DOOR_BELL_INPUT"
)
async_add_entities(entities)
class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
"""Event class for HomematicIP doorbell events."""
_attr_device_class = EventDeviceClass.DOORBELL
entity_description: HmipEventEntityDescription
def __init__(
self,
@@ -97,27 +86,9 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
@callback
def _async_handle_event(self, *args, **kwargs) -> None:
"""Handle the event fired by the functional channel."""
raised_channel_event = self._get_channel_event_from_args(*args)
if not self._should_raise(raised_channel_event):
return
event_types = self.entity_description.event_types
if TYPE_CHECKING:
assert event_types is not None
self._trigger_event(event_type=event_types[0])
self.async_write_ha_state()
def _should_raise(self, event_type: str) -> bool:
"""Check if the event should be raised."""
if self.entity_description.channel_event_types is None:
return False
return event_type in self.entity_description.channel_event_types
def _get_channel_event_from_args(self, *args) -> str:
"""Get the channel event."""
if isinstance(args[0], ChannelEvent):
return args[0].channelEventType
return ""

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