Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core v Beta progress update #361

Merged
merged 21 commits into from
Mar 7, 2023
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
14 changes: 7 additions & 7 deletions custom_components/plugwise/__init__.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
"""Plugwise beta for Home Assistant Core."""
"""Plugwise platform for Home Assistant Core."""

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant

from .const import CONF_USB_PATH
from .const import CONF_USB_PATH # pw-beta usb
from .gateway import async_setup_entry_gw, async_unload_entry_gw
from .usb import async_setup_entry_usb, async_unload_entry_usb
from .usb import async_setup_entry_usb, async_unload_entry_usb # pw-beta usb


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Plugwise components from a config entry."""
if entry.data.get(CONF_HOST):
return await async_setup_entry_gw(hass, entry)
if entry.data.get(CONF_USB_PATH):
return await async_setup_entry_usb(hass, entry)
if entry.data.get(CONF_USB_PATH): # pw-beta usb
return await async_setup_entry_usb(hass, entry) # pw-beta usb
return False # pragma: no cover


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload the Plugwise components."""
if entry.data.get(CONF_HOST):
return await async_unload_entry_gw(hass, entry)
if entry.data.get(CONF_USB_PATH):
return await async_unload_entry_usb(hass, entry)
if entry.data.get(CONF_USB_PATH): # pw-beta usb
return await async_unload_entry_usb(hass, entry) # pw-beta usb
return False # pragma: no cover
37 changes: 23 additions & 14 deletions custom_components/plugwise/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from plugwise.nodes import PlugwiseNode
from plugwise.nodes import PlugwiseNode # pw-beta usb

from .const import DOMAIN, LOGGER, SEVERITIES

# isort: off
from .const import COORDINATOR, PW_TYPE # pw-beta

from .const import (
ATTR_SCAN_DAYLIGHT_MODE,
Expand All @@ -22,23 +27,21 @@
ATTR_SED_SLEEP_FOR,
ATTR_SED_STAY_ACTIVE,
CB_NEW_NODE,
COORDINATOR,
DOMAIN,
LOGGER,
PW_TYPE,
SERVICE_USB_SCAN_CONFIG,
SERVICE_USB_SCAN_CONFIG_SCHEMA,
SERVICE_USB_SED_BATTERY_CONFIG,
SERVICE_USB_SED_BATTERY_CONFIG_SCHEMA,
SEVERITIES,
STICK,
USB,
USB_MOTION_ID,
)
) # pw-beta usb

# isort: on

from .coordinator import PlugwiseDataUpdateCoordinator
from .entity import PlugwiseEntity
from .models import PW_BINARY_SENSOR_TYPES, PlugwiseBinarySensorEntityDescription
from .usb import PlugwiseUSBEntity
from .usb import PlugwiseUSBEntity # pw-beta

PARALLEL_UPDATES = 0

Expand All @@ -55,7 +58,7 @@ async def async_setup_entry(
return await async_setup_entry_gateway(hass, config_entry, async_add_entities)


async def async_setup_entry_usb(hass, config_entry, async_add_entities):
async def async_setup_entry_usb(hass, config_entry, async_add_entities): # pw-beta
"""Set up Plugwise binary sensor based on config_entry."""
api_stick = hass.data[DOMAIN][config_entry.entry_id][STICK]
platform = entity_platform.current_platform.get()
Expand Down Expand Up @@ -150,8 +153,9 @@ def __init__(
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
# pw-beta: show Plugwise notifications as HA persistent notifications
if self._notification:
if (
self._notification
): # pw-beta: show Plugwise notifications as HA persistent notifications
for notify_id, message in self._notification.items():
self.hass.components.persistent_notification.async_create(
message, "Plugwise Notification:", f"{DOMAIN}.{notify_id}"
Expand All @@ -172,16 +176,20 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None:
if self.entity_description.key != "plugwise_notification":
return None

attrs: dict[str, list[str]] = {}
# pw-beta adjustment with attrs is to only represent severities *with* content
# not all severities including those without content as empty lists
attrs: dict[str, list[str]] = {} # pw-beta Re-evaluate against Core
self._notification = {} # pw-beta
if notify := self.coordinator.data.gateway["notifications"]:
for notify_id, details in notify.items():
for notify_id, details in notify.items(): # pw-beta uses notify_id
for msg_type, msg in details.items():
msg_type = msg_type.lower()
if msg_type not in SEVERITIES:
msg_type = "other" # pragma: no cover

if f"{msg_type}_msg" not in attrs:
if (
f"{msg_type}_msg" not in attrs
): # pw-beta Re-evaluate against Core
attrs[f"{msg_type}_msg"] = []
attrs[f"{msg_type}_msg"].append(msg)

Expand All @@ -192,6 +200,7 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None:
return attrs


# pw-beta
# Github issue #265
class USBBinarySensor(PlugwiseUSBEntity, BinarySensorEntity): # type: ignore[misc]
"""Representation of a Plugwise USB Binary Sensor."""
Expand Down
39 changes: 24 additions & 15 deletions custom_components/plugwise/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

from typing import Any

from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
Expand All @@ -25,7 +25,8 @@
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import CONF_HOMEKIT_EMULATION # pw-beta homekit emulation
from .const import COORDINATOR, DOMAIN, MASTER_THERMOSTATS
from .const import COORDINATOR # pw-beta
from .const import DOMAIN, MASTER_THERMOSTATS
from .coordinator import PlugwiseDataUpdateCoordinator
from .entity import PlugwiseEntity
from .util import plugwise_command
Expand All @@ -41,11 +42,14 @@ async def async_setup_entry(
config_entry.entry_id
][COORDINATOR]

# pw-beta homekit emulation
homekit_enabled: bool = config_entry.options.get(CONF_HOMEKIT_EMULATION, False)
homekit_enabled: bool = config_entry.options.get(
CONF_HOMEKIT_EMULATION, False
) # pw-beta homekit emulation

async_add_entities(
PlugwiseClimateEntity(coordinator, device_id, homekit_enabled)
PlugwiseClimateEntity(
coordinator, device_id, homekit_enabled
) # pw-beta homekit emulation
for device_id, device in coordinator.data.devices.items()
if device["dev_class"] in MASTER_THERMOSTATS
)
Expand Down Expand Up @@ -91,9 +95,10 @@ def __init__(

self._attr_min_temp = self.device["thermostat"]["lower_bound"]
self._attr_max_temp = self.device["thermostat"]["upper_bound"]
if resolution := self.device["thermostat"]["resolution"]:
# Ensure we don't drop below 0.1
self._attr_target_temperature_step = max(resolution, 0.1)
# Ensure we don't drop below 0.1
self._attr_target_temperature_step = max(
self.device["thermostat"]["resolution"], 0.1
)

@property
def current_temperature(self) -> float:
Expand Down Expand Up @@ -128,7 +133,9 @@ def target_temperature_low(self) -> float:
@property
def hvac_mode(self) -> HVACMode:
"""Return HVAC operation ie. auto, heat, heat_cool, or off mode."""
if (mode := self.device["mode"]) is None or mode not in self.hvac_modes:
if (
mode := self.device["mode"]
) is None or mode not in self.hvac_modes: # pw-beta add to Core
return HVACMode.HEAT # pragma: no cover
# pw-beta homekit emulation
if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
Expand All @@ -137,7 +144,7 @@ def hvac_mode(self) -> HVACMode:
return HVACMode(mode)

@property
def hvac_action(self) -> HVACAction:
def hvac_action(self) -> HVACAction: # pw-beta add to Core
"""Return the current running hvac operation if supported."""
# When control_state is present, prefer this data
if (control_state := self.device.get("control_state")) == "cooling":
Expand All @@ -154,7 +161,7 @@ def hvac_action(self) -> HVACAction:
]
if hc_data["binary_sensors"]["heating_state"]:
return HVACAction.HEATING
if hc_data["binary_sensors"].get("cooling_state", False):
if hc_data["binary_sensors"].get("cooling_state", False): # pw-beta adds False
return HVACAction.COOLING

return HVACAction.IDLE
Expand All @@ -167,8 +174,10 @@ def preset_mode(self) -> str | None:
@plugwise_command
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if ATTR_HVAC_MODE in kwargs:
await self.async_set_hvac_mode(kwargs[ATTR_HVAC_MODE])
if ATTR_HVAC_MODE in kwargs: # pw-beta add to Core
await self.async_set_hvac_mode(
kwargs[ATTR_HVAC_MODE]
) # pw-beta add to Core

data: dict[str, Any] = {}
if ATTR_TEMPERATURE in kwargs:
Expand All @@ -187,7 +196,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None:
await self.coordinator.api.set_temperature(self.device["location"], data)

@plugwise_command
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the hvac mode."""
if hvac_mode not in self.hvac_modes:
raise HomeAssistantError("Unsupported hvac_mode")
Expand Down
53 changes: 30 additions & 23 deletions custom_components/plugwise/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Config flow for Plugwise integration."""
from __future__ import annotations

import datetime as dt # pw-beta
import datetime as dt # pw-beta options
from typing import Any

import serial.tools.list_ports
import serial.tools.list_ports # pw-beta usb
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.components import usb
from homeassistant.components import usb # pw-beta usb
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import (
Expand All @@ -29,47 +29,51 @@
InvalidAuthentication,
InvalidSetupError,
InvalidXMLError,
NetworkDown,
PortError,
ResponseError,
StickInitError,
TimeoutException,
UnsupportedDeviceError,
)

# pw-beta Note; the below are explicit through isort
from plugwise.exceptions import NetworkDown # pw-beta usb
from plugwise.exceptions import PortError # pw-beta usb
from plugwise.exceptions import StickInitError # pw-beta usb
from plugwise.exceptions import TimeoutException # pw-beta usb
from plugwise.smile import Smile
from plugwise.stick import Stick
from plugwise.stick import Stick # pw-beta usb

from .const import (
API,
CONF_MANUAL_PATH,
CONF_USB_PATH,
COORDINATOR,
DEFAULT_PORT,
DEFAULT_USERNAME,
DOMAIN,
FLOW_NET,
FLOW_SMILE,
FLOW_STRETCH,
FLOW_TYPE,
FLOW_USB,
PW_TYPE,
SMILE,
STICK,
STRETCH,
STRETCH_USERNAME,
ZEROCONF_MAP,
)

# pw-beta Note; the below are explicit through isort
from .const import CONF_HOMEKIT_EMULATION # pw-beta option
from .const import CONF_MANUAL_PATH # pw-beta usb
from .const import CONF_REFRESH_INTERVAL # pw-beta option
from .const import CONF_USB_PATH # pw-beta usb
from .const import DEFAULT_SCAN_INTERVAL # pw-beta option
from .const import FLOW_NET # pw-beta usb
from .const import FLOW_TYPE # pw-beta usb
from .const import FLOW_USB # pw-beta usb
from .const import STICK # pw-beta usb

CONNECTION_SCHEMA = vol.Schema(
{vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In([FLOW_NET, FLOW_USB])}
)
) # pw-beta usb


@callback
def plugwise_stick_entries(hass):
def plugwise_stick_entries(hass): # pw-beta usb
"""Return existing connections for Plugwise USB-stick domain."""
sticks = []
for entry in hass.config_entries.async_entries(DOMAIN):
Expand All @@ -81,7 +85,9 @@ def plugwise_stick_entries(hass):
# Github issue: #265
# Might be a `tuple[dict[str, str], Stick | None]` for typing, but that throws
# Item None of Optional[Any] not having attribute mac [union-attr]
async def validate_usb_connection(self, device_path=None) -> tuple[dict[str, str], Any]:
async def validate_usb_connection(
self, device_path=None
) -> tuple[dict[str, str], Any]: # pw-beta usb
"""Test if device_path is a real Plugwise USB-Stick."""
errors = {}

Expand Down Expand Up @@ -240,7 +246,7 @@ async def async_step_zeroconf(

async def async_step_user_usb(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
) -> FlowResult: # pw-beta usb
"""Step when user initializes a integration."""
errors: dict[str, str] = {}
ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports)
Expand Down Expand Up @@ -278,7 +284,7 @@ async def async_step_user_usb(

async def async_step_manual_path(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
) -> FlowResult: # pw-beta usb
"""Step when manual path to device."""
errors: dict[str, str] = {}
if user_input is not None:
Expand Down Expand Up @@ -369,18 +375,19 @@ async def async_step_user(
errors=errors,
)

# pw-beta
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> config_entries.OptionsFlow:
def async_get_options_flow(
config_entry: ConfigEntry,
) -> config_entries.OptionsFlow: # pw-beta options
"""Get the options flow for this handler."""
return PlugwiseOptionsFlowHandler(config_entry)


# pw-beta - change the scan-interval via CONFIGURE
# pw-beta - add homekit emulation via CONFIGURE
# pw-beta - change the frontend refresh interval via CONFIGURE
class PlugwiseOptionsFlowHandler(config_entries.OptionsFlow):
class PlugwiseOptionsFlowHandler(config_entries.OptionsFlow): # pw-beta options
"""Plugwise option flow."""

def __init__(self, config_entry: ConfigEntry) -> None: # pragma: no cover
Expand Down Expand Up @@ -410,7 +417,7 @@ async def async_step_init(
coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id][COORDINATOR]
interval: dt.timedelta = DEFAULT_SCAN_INTERVAL[
coordinator.api.smile_type
] # pw-beta
] # pw-beta options

data = {
vol.Optional(
Expand Down
Loading