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/adguard/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["adguardhome"],
"requirements": ["adguardhome==0.7.0"]
"requirements": ["adguardhome==0.8.0"]
}
28 changes: 26 additions & 2 deletions homeassistant/components/shelly/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
from .utils import (
async_remove_orphaned_entities,
async_remove_shelly_entity,
get_block_channel,
get_block_custom_name,
get_device_entry_gen,
get_rpc_component_name,
get_rpc_entity_name,
get_rpc_key_instances,
is_block_momentary_input,
Expand Down Expand Up @@ -74,7 +77,6 @@ class ShellyRpcEventDescription(EventEntityDescription):
SCRIPT_EVENT: Final = ShellyRpcEventDescription(
key="script",
translation_key="script",
device_class=None,
entity_registry_enabled_default=False,
)

Expand Down Expand Up @@ -195,6 +197,17 @@ def __init__(
self._attr_event_types = list(BASIC_INPUTS_EVENTS_TYPES)
self.entity_description = description

if (
hasattr(self, "_attr_name")
and self._attr_name
and not get_block_custom_name(coordinator.device, block)
):
self._attr_translation_placeholders = {
"input_number": get_block_channel(block)
}

delattr(self, "_attr_name")

async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
Expand Down Expand Up @@ -227,9 +240,20 @@ def __init__(
self.event_id = int(key.split(":")[-1])
self._attr_device_info = get_entity_rpc_device_info(coordinator, key)
self._attr_unique_id = f"{coordinator.mac}-{key}"
self._attr_name = get_rpc_entity_name(coordinator.device, key)
self.entity_description = description

if description.key == "input":
component = key.split(":")[0]
component_id = key.split(":")[-1]
if not get_rpc_component_name(coordinator.device, key) and (
component.lower() == "input" and component_id.isnumeric()
):
self._attr_translation_placeholders = {"input_number": component_id}
else:
self._attr_name = get_rpc_entity_name(coordinator.device, key)
elif description.key == "script":
self._attr_name = get_rpc_entity_name(coordinator.device, key)

async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/shelly/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
},
"event": {
"input": {
"name": "Input {input_number}",
"state_attributes": {
"event_type": {
"state": {
Expand Down
74 changes: 44 additions & 30 deletions homeassistant/components/shelly/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,35 @@ def get_number_of_channels(device: BlockDevice, block: Block) -> int:
def get_block_entity_name(
device: BlockDevice,
block: Block | None,
description: str | UndefinedType | None = None,
name: str | UndefinedType | None = None,
) -> str | None:
"""Naming for block based switch and sensors."""
channel_name = get_block_channel_name(device, block)

if description is not UNDEFINED and description:
return f"{channel_name} {description.lower()}" if channel_name else description
if name is not UNDEFINED and name:
return f"{channel_name} {name.lower()}" if channel_name else name

return channel_name


def get_block_custom_name(device: BlockDevice, block: Block | None) -> str | None:
"""Get custom name from device settings."""
if block and (key := cast(str, block.type) + "s") and key in device.settings:
assert block.channel

if name := device.settings[key][int(block.channel)].get("name"):
return cast(str, name)

return None


def get_block_channel(block: Block | None, base: str = "1") -> str:
"""Get block channel."""
assert block and block.channel

return chr(int(block.channel) + ord(base))


def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | None:
"""Get name based on device and channel name."""
if (
Expand All @@ -140,38 +158,24 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | No
):
return None

assert block.channel
if custom_name := get_block_custom_name(device, block):
return custom_name

channel_name: str | None = None
mode = cast(str, block.type) + "s"
if mode in device.settings:
channel_name = device.settings[mode][int(block.channel)].get("name")

if channel_name:
return channel_name

base = ord("1")

return f"Channel {chr(int(block.channel) + base)}"
return f"Channel {get_block_channel(block)}"


def get_block_sub_device_name(device: BlockDevice, block: Block) -> str:
"""Get name of block sub-device."""
if TYPE_CHECKING:
assert block.channel

mode = cast(str, block.type) + "s"
if mode in device.settings:
if channel_name := device.settings[mode][int(block.channel)].get("name"):
return cast(str, channel_name)
if custom_name := get_block_custom_name(device, block):
return custom_name

if device.settings["device"]["type"] == MODEL_EM3:
base = ord("A")
return f"{device.name} Phase {chr(int(block.channel) + base)}"
return f"{device.name} Phase {get_block_channel(block, 'A')}"

base = ord("1")

return f"{device.name} Channel {chr(int(block.channel) + base)}"
return f"{device.name} Channel {get_block_channel(block)}"


def is_block_momentary_input(
Expand Down Expand Up @@ -387,6 +391,18 @@ def get_shelly_model_name(
return cast(str, MODEL_NAMES.get(model))


def get_rpc_component_name(device: RpcDevice, key: str) -> str | None:
"""Get component name from device config."""
if (
key in device.config
and key != "em:0" # workaround for Pro 3EM, we don't want to get name for em:0
and (name := device.config[key].get("name"))
):
return cast(str, name)

return None


def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None:
"""Get name based on device and channel name."""
if BLU_TRV_IDENTIFIER in key:
Expand All @@ -398,13 +414,11 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None:
component = key.split(":")[0]
component_id = key.split(":")[-1]

if key in device.config and key != "em:0":
# workaround for Pro 3EM, we don't want to get name for em:0
if component_name := device.config[key].get("name"):
if component in (*VIRTUAL_COMPONENTS, "input", "presencezone", "script"):
return cast(str, component_name)
if component_name := get_rpc_component_name(device, key):
if component in (*VIRTUAL_COMPONENTS, "input", "presencezone", "script"):
return component_name

return cast(str, component_name) if instances == 1 else None
return component_name if instances == 1 else None

if component in (*VIRTUAL_COMPONENTS, "input"):
return f"{component.title()} {component_id}"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/thermopro/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/thermopro",
"iot_class": "local_push",
"requirements": ["thermopro-ble==0.13.1"]
"requirements": ["thermopro-ble==1.1.2"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/tplink_omada/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tplink_omada",
"integration_type": "hub",
"iot_class": "local_polling",
"requirements": ["tplink-omada-client==1.4.4"]
"requirements": ["tplink-omada-client==1.5.3"]
}
15 changes: 12 additions & 3 deletions homeassistant/components/tplink_omada/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
from functools import partial
from typing import Any, Generic, TypeVar

from tplink_omada_client import OmadaSiteClient, SwitchPortOverrides
from tplink_omada_client import (
GatewayPortSettings,
OmadaSiteClient,
PortProfileOverrides,
SwitchPortSettings,
)
from tplink_omada_client.definitions import GatewayPortMode, PoEMode, PortType
from tplink_omada_client.devices import (
OmadaDevice,
Expand All @@ -17,7 +22,6 @@
OmadaSwitch,
OmadaSwitchPortDetails,
)
from tplink_omada_client.omadasiteclient import GatewayPortSettings

from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
Expand Down Expand Up @@ -184,7 +188,12 @@ async def _wan_connect_disconnect(
),
set_func=(
lambda client, device, port, enable: client.update_switch_port(
device, port, overrides=SwitchPortOverrides(enable_poe=enable)
device,
port,
settings=SwitchPortSettings(
profile_override_enabled=True,
profile_overrides=PortProfileOverrides(enable_poe=enable),
),
)
),
update_func=lambda p: p.poe_mode != PoEMode.DISABLED,
Expand Down
79 changes: 33 additions & 46 deletions homeassistant/components/tuya/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .entity import TuyaEntity
from .models import IntegerTypeData, find_dpcode
from .models import DPCodeIntegerWrapper, IntegerTypeData, find_dpcode
from .util import get_dpcode

TUYA_HVAC_TO_HA = {
Expand All @@ -41,6 +41,16 @@
}


class _RoundedIntegerWrapper(DPCodeIntegerWrapper):
"""An integer that always rounds its value."""

def read_device_status(self, device: CustomerDevice) -> int | None:
"""Read and round the device status."""
if (value := super().read_device_status(device)) is None:
return None
return round(value)


@dataclass(frozen=True, kw_only=True)
class TuyaClimateEntityDescription(ClimateEntityDescription):
"""Describe an Tuya climate entity."""
Expand Down Expand Up @@ -97,6 +107,12 @@ def async_discover_device(device_ids: list[str]) -> None:
manager,
CLIMATE_DESCRIPTIONS[device.category],
hass.config.units.temperature_unit,
current_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode(
device, DPCode.HUMIDITY_CURRENT
),
target_humidity_wrapper=_RoundedIntegerWrapper.find_dpcode(
device, DPCode.HUMIDITY_SET, prefer_function=True
),
)
)
async_add_entities(entities)
Expand All @@ -111,10 +127,8 @@ def async_discover_device(device_ids: list[str]) -> None:
class TuyaClimateEntity(TuyaEntity, ClimateEntity):
"""Tuya Climate Device."""

_current_humidity: IntegerTypeData | None = None
_current_temperature: IntegerTypeData | None = None
_hvac_to_tuya: dict[str, str]
_set_humidity: IntegerTypeData | None = None
_set_temperature: IntegerTypeData | None = None
entity_description: TuyaClimateEntityDescription
_attr_name = None
Expand All @@ -125,12 +139,17 @@ def __init__(
device_manager: Manager,
description: TuyaClimateEntityDescription,
system_temperature_unit: UnitOfTemperature,
*,
current_humidity_wrapper: _RoundedIntegerWrapper | None = None,
target_humidity_wrapper: _RoundedIntegerWrapper | None = None,
) -> None:
"""Determine which values to use."""
self._attr_target_temperature_step = 1.0
self.entity_description = description

super().__init__(device, device_manager)
self._current_humidity_wrapper = current_humidity_wrapper
self._target_humidity_wrapper = target_humidity_wrapper

# If both temperature values for celsius and fahrenheit are present,
# use whatever the device is set to, with a fallback to celsius.
Expand Down Expand Up @@ -227,21 +246,14 @@ def __init__(
]

# Determine dpcode to use for setting the humidity
if int_type := find_dpcode(
self.device,
DPCode.HUMIDITY_SET,
dptype=DPType.INTEGER,
prefer_function=True,
):
if target_humidity_wrapper:
self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY
self._set_humidity = int_type
self._attr_min_humidity = int(int_type.min_scaled)
self._attr_max_humidity = int(int_type.max_scaled)

# Determine dpcode to use for getting the current humidity
self._current_humidity = find_dpcode(
self.device, DPCode.HUMIDITY_CURRENT, dptype=DPType.INTEGER
)
self._attr_min_humidity = round(
target_humidity_wrapper.type_information.min_scaled
)
self._attr_max_humidity = round(
target_humidity_wrapper.type_information.max_scaled
)

# Determine fan modes
self._fan_mode_dp_code: str | None = None
Expand Down Expand Up @@ -303,20 +315,9 @@ def set_fan_mode(self, fan_mode: str) -> None:

self._send_command([{"code": self._fan_mode_dp_code, "value": fan_mode}])

def set_humidity(self, humidity: int) -> None:
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
if TYPE_CHECKING:
# guarded by ClimateEntityFeature.TARGET_HUMIDITY
assert self._set_humidity is not None

self._send_command(
[
{
"code": self._set_humidity.dpcode,
"value": self._set_humidity.scale_value_back(humidity),
}
]
)
await self._async_send_dpcode_update(self._target_humidity_wrapper, humidity)

def set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing operation."""
Expand Down Expand Up @@ -382,14 +383,7 @@ def current_temperature(self) -> float | None:
@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
if self._current_humidity is None:
return None

humidity = self.device.status.get(self._current_humidity.dpcode)
if humidity is None:
return None

return round(self._current_humidity.scale_value(humidity))
return self._read_wrapper(self._current_humidity_wrapper)

@property
def target_temperature(self) -> float | None:
Expand All @@ -406,14 +400,7 @@ def target_temperature(self) -> float | None:
@property
def target_humidity(self) -> int | None:
"""Return the humidity currently set to be reached."""
if self._set_humidity is None:
return None

humidity = self.device.status.get(self._set_humidity.dpcode)
if humidity is None:
return None

return round(self._set_humidity.scale_value(humidity))
return self._read_wrapper(self._target_humidity_wrapper)

@property
def hvac_mode(self) -> HVACMode:
Expand Down
Loading
Loading