Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/duco/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"iot_class": "local_polling",
"loggers": ["duco"],
"quality_scale": "platinum",
"requirements": ["python-duco-client==0.3.4"],
"requirements": ["python-duco-client==0.3.6"],
"zeroconf": [
{
"name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*",
Expand Down
22 changes: 22 additions & 0 deletions homeassistant/components/switchbot_cloud/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,25 @@ class Humidifier2Mode(Enum):
def get_modes(cls) -> list[str]:
"""Return a list of available humidifier2 modes as lowercase strings."""
return [mode.name.lower() for mode in cls]


class SwitchbotCloudDeviceLockState(Enum):
"""Lock State."""

LOCKED = "locked"
UNLOCKED = "unlocked"
LOCKING = "locking"
UNLOCKING = "unlocking"
JAMMED = "jammed"
LATCH_BOLT_LOCKED = "latchBoltLocked"
HALF_LOCKED = "halfLocked"

@classmethod
def get_states(cls) -> list[SwitchbotCloudDeviceLockState]:
"""Get lock states."""
return list(cls)

@classmethod
def get_values(cls) -> list[str]:
"""Get lock value."""
return [mode.value for mode in cls]
25 changes: 22 additions & 3 deletions homeassistant/components/switchbot_cloud/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from . import SwitchbotCloudConfigEntry
from .const import DOMAIN
from .const import DOMAIN, SwitchbotCloudDeviceLockState
from .coordinator import SwitchBotCoordinator
from .entity import SwitchBotCloudEntity

Expand All @@ -47,6 +47,8 @@
RELAY_SWITCH_2PM_SENSOR_TYPE_CURRENT = "ElectricCurrent"
RELAY_SWITCH_2PM_SENSOR_TYPE_ELECTRICITY = "UsedElectricity"

LOCK_SENSOR_TYPE_LOCK_STATE = "lockState"


@dataclass(frozen=True, kw_only=True)
class SwitchbotCloudSensorEntityDescription(SensorEntityDescription):
Expand Down Expand Up @@ -165,6 +167,21 @@ class SwitchbotCloudSensorEntityDescription(SensorEntityDescription):
state_class=SensorStateClass.MEASUREMENT,
)


LOCK_SENSOR_TYPE_LOCK_STATE_DESCRIPTION = SwitchbotCloudSensorEntityDescription(
key=LOCK_SENSOR_TYPE_LOCK_STATE,
device_class=SensorDeviceClass.ENUM,
translation_key="lock_state",
options=[
value.name.lower() for value in SwitchbotCloudDeviceLockState.get_states()
],
value_fn=lambda value: (
SwitchbotCloudDeviceLockState(value).name.lower()
if value in SwitchbotCloudDeviceLockState.get_values()
else None
),
)

SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
"Bot": (BATTERY_DESCRIPTION,),
"Battery Circulator Fan": (BATTERY_DESCRIPTION,),
Expand Down Expand Up @@ -224,7 +241,10 @@ class SwitchbotCloudSensorEntityDescription(SensorEntityDescription):
"Smart Lock": (BATTERY_DESCRIPTION,),
"Smart Lock Lite": (BATTERY_DESCRIPTION,),
"Smart Lock Pro": (BATTERY_DESCRIPTION,),
"Smart Lock Ultra": (BATTERY_DESCRIPTION,),
"Smart Lock Ultra": (
BATTERY_DESCRIPTION,
LOCK_SENSOR_TYPE_LOCK_STATE_DESCRIPTION,
),
"Smart Lock Vision": (BATTERY_DESCRIPTION,),
"Smart Lock Vision Pro": (BATTERY_DESCRIPTION,),
"Lock Vision": (BATTERY_DESCRIPTION,),
Expand Down Expand Up @@ -314,7 +334,6 @@ def _set_attributes(self) -> None:
if not self.coordinator.data:
return
value = self.coordinator.data.get(self.entity_description.key)

self._attr_native_value = self.entity_description.value_fn(value)


Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/switchbot_cloud/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@
"sensor": {
"light_level": {
"name": "Light level"
},
"lock_state": {
"name": "Lock state",
"state": {
"half_locked": "Half locked",
"jammed": "Jammed",
"latch_bolt_locked": "Latch bolt locked",
"locked": "[%key:common::state::locked%]",
"locking": "Locking",
"unlocked": "[%key:common::state::unlocked%]",
"unlocking": "Unlocking"
}
}
}
}
Expand Down
54 changes: 54 additions & 0 deletions homeassistant/components/victron_gx/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,60 @@ async def async_step_ssdp_auth(
description_placeholders={CONF_HOST: self.hostname},
)

async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of a Victron GX device."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()

if user_input is not None:
data = {
**reconfigure_entry.data,
**user_input,
}
if CONF_USERNAME in user_input:
data[CONF_USERNAME] = user_input[CONF_USERNAME] or None
if CONF_PASSWORD in user_input:
data[CONF_PASSWORD] = user_input[CONF_PASSWORD] or None
try:
installation_id = await validate_input(data)
except AuthenticationError:
errors["base"] = "invalid_auth"
except CannotConnectError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected error during reconfiguration")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(installation_id)
self._abort_if_unique_id_mismatch(reason="different_device")
return self.async_update_reload_and_abort(
reconfigure_entry,
title=ENTRY_TITLE_FORMAT.format(
installation_id=installation_id,
host=user_input[CONF_HOST],
port=user_input[CONF_PORT],
),
data_updates=data,
)

suggested_values = {
CONF_HOST: reconfigure_entry.data[CONF_HOST],
CONF_PORT: reconfigure_entry.data[CONF_PORT],
CONF_USERNAME: reconfigure_entry.data.get(CONF_USERNAME),
CONF_SSL: reconfigure_entry.data.get(CONF_SSL, False),
}
if user_input is not None:
suggested_values.update(user_input)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, suggested_values
),
errors=errors,
)

async def async_step_reauth(self, _: Mapping[str, Any]) -> ConfigFlowResult:
"""Handle reauthentication."""
return await self.async_step_reauth_confirm()
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/victron_gx/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ rules:
status: exempt
comment: |
Not relevant.
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues: todo
stale-devices: done

Expand Down
18 changes: 18 additions & 0 deletions homeassistant/components/victron_gx/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"different_device": "The device at this address is different from the originally configured device.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
Expand All @@ -123,6 +125,22 @@
"description": "Please re-authenticate with {host}.",
"title": "[%key:common::config_flow::title::reauth%]"
},
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]",
"ssl": "[%key:common::config_flow::data::ssl%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"host": "[%key:component::victron_gx::config::step::user::data_description::host%]",
"password": "[%key:component::victron_gx::config::step::user::data_description::password%]",
"port": "[%key:component::victron_gx::config::step::user::data_description::port%]",
"ssl": "[%key:component::victron_gx::config::step::user::data_description::ssl%]",
"username": "[%key:component::victron_gx::config::step::user::data_description::username%]"
}
},
"ssdp_auth": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions tests/components/duco/snapshots/test_diagnostics.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
'iaq_co2': None,
'iaq_rh': None,
'rh': None,
'temp': None,
}),
'ventilation': dict({
'flow_lvl_tgt': 0,
Expand All @@ -70,6 +71,7 @@
'iaq_co2': None,
'iaq_rh': 85,
'rh': 42.0,
'temp': None,
}),
'ventilation': dict({
'flow_lvl_tgt': None,
Expand All @@ -95,6 +97,7 @@
'iaq_co2': 80,
'iaq_rh': None,
'rh': None,
'temp': None,
}),
'ventilation': dict({
'flow_lvl_tgt': None,
Expand All @@ -120,6 +123,7 @@
'iaq_co2': None,
'iaq_rh': 90,
'rh': 61.0,
'temp': None,
}),
'ventilation': dict({
'flow_lvl_tgt': None,
Expand Down
8 changes: 8 additions & 0 deletions tests/components/switchbot_cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,11 @@ async def configure_integration(hass: HomeAssistant) -> MockConfigEntry:
deviceType="Humidifier2",
hubDeviceId="test-hub-id",
)

LOCK_ULTRA_INFO = Device(
version="V1.0",
deviceId="lock-id-1",
deviceName="Lock Ultra",
deviceType="Smart Lock Ultra",
hubDeviceId="test-hub-id",
)
36 changes: 31 additions & 5 deletions tests/components/switchbot_cloud/test_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from unittest.mock import patch

import pytest
from switchbot_api import Device

from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState
Expand All @@ -18,14 +19,28 @@
from . import configure_integration


async def test_lock(hass: HomeAssistant, mock_list_devices, mock_get_status) -> None:
@pytest.mark.parametrize(
("device_info", "test_index"),
[
("Smart Lock", 0),
("Smart Lock Lite", 1),
("Smart Lock Pro", 2),
("Smart Lock Ultra", 3),
("Lock Vision", 4),
("Lock Vision Pro", 5),
("Smart Lock Pro Wifi", 6),
],
)
async def test_lock(
hass: HomeAssistant, mock_list_devices, mock_get_status, device_info, test_index
) -> None:
"""Test locking and unlocking."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="lock-id-1",
deviceName="lock-1",
deviceType="Smart Lock",
deviceType=device_info,
hubDeviceId="test-hub-id",
),
]
Expand All @@ -52,16 +67,27 @@ async def test_lock(hass: HomeAssistant, mock_list_devices, mock_get_status) ->
assert hass.states.get(lock_id).state == LockState.LOCKED


@pytest.mark.parametrize(
("device_info", "test_index"),
[
("Smart Lock", 0),
("Smart Lock Pro", 1),
("Smart Lock Ultra", 2),
("Lock Vision", 3),
("Lock Vision Pro", 4),
("Smart Lock Pro Wifi", 5),
],
)
async def test_lock_open(
hass: HomeAssistant, mock_list_devices, mock_get_status
hass: HomeAssistant, mock_list_devices, mock_get_status, device_info, test_index
) -> None:
"""Test lock open."""
"""Test locking and unlocking."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="lock-id-1",
deviceName="lock-1",
deviceType="Smart Lock Pro",
deviceType=device_info,
hubDeviceId="test-hub-id",
),
]
Expand Down
2 changes: 2 additions & 0 deletions tests/components/switchbot_cloud/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from . import (
CONTACT_SENSOR_INFO,
HUB3_INFO,
LOCK_ULTRA_INFO,
METER_INFO,
MOTION_SENSOR_INFO,
WATER_DETECTOR_INFO,
Expand All @@ -32,6 +33,7 @@
(HUB3_INFO, 3),
(MOTION_SENSOR_INFO, 4),
(WATER_DETECTOR_INFO, 5),
(LOCK_ULTRA_INFO, 5),
],
)
async def test_meter(
Expand Down
Loading
Loading