Skip to content
Closed
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
8 changes: 6 additions & 2 deletions homeassistant/components/plugwise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant

from .const import CONF_USB_PATH
from .gateway import async_setup_entry_gw, async_unload_entry_gw
from .usb import async_setup_entry_usb, async_unload_entry_usb


async def async_setup(hass: HomeAssistant, config: dict):
Expand All @@ -16,13 +18,15 @@ 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)
# PLACEHOLDER USB entry setup
if entry.data.get(CONF_USB_PATH):
return await async_setup_entry_usb(hass, entry)
return False


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload the Plugwise components."""
if entry.data.get(CONF_HOST):
return await async_unload_entry_gw(hass, entry)
# PLACEHOLDER USB entry setup
if entry.data.get(CONF_USB_PATH):
return await async_unload_entry_usb(hass, entry)
return False
154 changes: 147 additions & 7 deletions homeassistant/components/plugwise/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
"""Config flow for Plugwise integration."""
import logging

from plugwise.exceptions import InvalidAuthentication, PlugwiseException
import os
from typing import Dict

import plugwise
from plugwise.exceptions import (
InvalidAuthentication,
NetworkDown,
PlugwiseException,
PortError,
StickInitError,
TimeoutException,
)
from plugwise.smile import Smile
import serial.tools.list_ports
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
Expand All @@ -20,14 +31,77 @@
from homeassistant.helpers.typing import DiscoveryInfoType

from .const import ( # pylint:disable=unused-import
CONF_USB_PATH,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
FLOW_NET,
FLOW_TYPE,
FLOW_USB,
PW_TYPE,
SMILE,
STICK,
STRETCH,
ZEROCONF_MAP,
)

_LOGGER = logging.getLogger(__name__)

CONF_MANUAL_PATH = "Enter Manually"

CONNECTION_SCHEMA = vol.Schema(
{
vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In(
{FLOW_NET: f"Network: {SMILE} / {STRETCH}", FLOW_USB: "USB: Stick"}
),
},
)


@callback
def plugwise_stick_entries(hass):
"""Return existing connections for Plugwise USB-stick domain."""
sticks = []
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.data.get(PW_TYPE) == STICK:
sticks.append(entry.data.get(CONF_USB_PATH))
return sticks


async def validate_usb_connection(self, device_path=None) -> Dict[str, str]:
"""Test if device_path is a real Plugwise USB-Stick."""
errors = {}
# Avoid creating a 2nd connection to an already configured stick
if device_path in plugwise_stick_entries(self):
errors[CONF_BASE] = "already_configured"
return errors, None

stick = await self.async_add_executor_job(plugwise.stick, device_path)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really necessary to call a class using async_add_executor_job()?

Maybe better to use from plugwise import stick
and then use something like api_stick = stick(device_path)?

try:
await self.async_add_executor_job(stick.connect)
await self.async_add_executor_job(stick.initialize_stick)
await self.async_add_executor_job(stick.disconnect)
except PortError:
errors[CONF_BASE] = "cannot_connect"
except StickInitError:
errors[CONF_BASE] = "stick_init"
except NetworkDown:
errors[CONF_BASE] = "network_down"
except TimeoutException:
errors[CONF_BASE] = "network_timeout"
return errors, stick
Comment on lines +79 to +92
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid all the jumps into the executor by wrapping them up in one function.

Copy link
Copy Markdown
Contributor

@bouwew bouwew Jan 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bdraco, first a question: why should this be avoided?

And, combining them in one function is not practical: there are 4 functions, we call use 3 of them here, and in usb.py we use all four of them but in a different combination.
Also, this happens in config_flow.py and usb.py, the functions in these 2 files are only called during init and when removing the integration, how much harm can that cause?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching from async to sync context is expensive. We should try to avoid doing it multiple times when we can do one call to the executor.



def get_serial_by_id(dev_path: str) -> str:
"""Return a /dev/serial/by-id match for given device if available."""
by_id = "/dev/serial/by-id"
if not os.path.isdir(by_id):
return dev_path
for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()):
if os.path.realpath(path) == dev_path:
return path
return dev_path


def _base_gw_schema(discovery_info):
"""Generate base schema for gateways."""
Expand Down Expand Up @@ -108,7 +182,7 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT),
CONF_NAME: _name,
}
return await self.async_step_user()
return await self.async_step_user({FLOW_TYPE: FLOW_NET})

async def async_step_user_gateway(self, user_input=None):
"""Handle the initial step for gateways."""
Expand Down Expand Up @@ -148,13 +222,79 @@ async def async_step_user_gateway(self, user_input=None):
errors=errors or {},
)

# PLACEHOLDER USB async_step_user_usb and async_step_user_usb_manual_paht
async def async_step_user_usb(self, user_input=None):
"""Step when user initializes a integration."""
errors = {}
ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports)
list_of_ports = [
f"{p}, s/n: {p.serial_number or 'n/a'}"
+ (f" - {p.manufacturer}" if p.manufacturer else "")
for p in ports
]
list_of_ports.append(CONF_MANUAL_PATH)

if user_input is not None:
user_selection = user_input[CONF_USB_PATH]
if user_selection == CONF_MANUAL_PATH:
return await self.async_step_manual_path()
port = ports[list_of_ports.index(user_selection)]
device_path = await self.hass.async_add_executor_job(
get_serial_by_id, port.device
)
errors, stick = await validate_usb_connection(self.hass, device_path)
if not errors:
await self.async_set_unique_id(stick.get_mac_stick())
return self.async_create_entry(
title="Stick", data={CONF_USB_PATH: device_path, PW_TYPE: STICK}
)
return self.async_show_form(
step_id="user_usb",
data_schema=vol.Schema(
{vol.Required(CONF_USB_PATH): vol.In(list_of_ports)}
),
errors=errors,
)

async def async_step_manual_path(self, user_input=None):
"""Step when manual path to device."""
errors = {}
if user_input is not None:
device_path = await self.hass.async_add_executor_job(
get_serial_by_id, user_input.get(CONF_USB_PATH)
)
errors, stick = await validate_usb_connection(self.hass, device_path)
if not errors:
await self.async_set_unique_id(stick.get_mac_stick())
return self.async_create_entry(
title="Stick", data={CONF_USB_PATH: device_path}
)
return self.async_show_form(
step_id="manual_path",
data_schema=vol.Schema(
{
vol.Required(
CONF_USB_PATH, default="/dev/ttyUSB0" or vol.UNDEFINED
): str
}
),
errors=errors,
)

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
"""Handle the initial step when using network/gateway setups."""
errors = {}
if user_input is not None:
if user_input[FLOW_TYPE] == FLOW_NET:
return await self.async_step_user_gateway()

# PLACEHOLDER USB vs Gateway Logic
return await self.async_step_user_gateway()
if user_input[FLOW_TYPE] == FLOW_USB:
return await self.async_step_user_usb()

return self.async_show_form(
step_id="user",
data_schema=CONNECTION_SCHEMA,
errors=errors,
)

@staticmethod
@callback
Expand Down
82 changes: 80 additions & 2 deletions homeassistant/components/plugwise/const.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
"""Constant for Plugwise component."""

from homeassistant.components.switch import DEVICE_CLASS_OUTLET
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
ATTR_NAME,
ATTR_STATE,
ATTR_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_POWER,
ENERGY_KILO_WATT_HOUR,
POWER_WATT,
)

ATTR_ENABLED_DEFAULT = "enabled_default"
DOMAIN = "plugwise"
GATEWAY = "gateway"
PW_TYPE = "plugwise_type"
SMILE = "smile"
STICK = "stick"
STRETCH = "stretch"
USB = "usb"

SENSOR_PLATFORMS = ["sensor", "switch"]
PLATFORMS_GATEWAY = ["binary_sensor", "climate", "sensor", "switch"]
PLATFORMS_USB = ["switch"]
SENSOR_PLATFORMS = ["sensor", "switch"]
PW_TYPE = "plugwise_type"
GATEWAY = "gateway"

FLOW_NET = "flow_network"
FLOW_TYPE = "flow_type"
FLOW_USB = "flow_usb"
FLOW_SMILE = "smile (Adam/Anna/P1)"
FLOW_STRETCH = "stretch (Stretch)"

# Sensor mapping
SENSOR_MAP_DEVICE_CLASS = 2
Expand All @@ -28,6 +54,7 @@
CONF_MIN_TEMP = "min_temp"
CONF_POWER = "power"
CONF_THERMOSTAT = "thermostat"
CONF_USB_PATH = "usb_path"

ATTR_ILLUMINANCE = "illuminance"

Expand Down Expand Up @@ -55,3 +82,54 @@
"smile_open_therm": "Adam",
"stretch": "Stretch",
}

# Callback types
CB_NEW_NODE = "NEW_NODE"

# Sensor IDs
AVAILABLE_SENSOR_ID = "available"
CURRENT_POWER_SENSOR_ID = "power_1s"
TODAY_ENERGY_SENSOR_ID = "power_con_today"

ATTR_MAC_ADDRESS = "mac"

# Sensor types
USB_SENSORS = {
AVAILABLE_SENSOR_ID: {
ATTR_DEVICE_CLASS: None,
ATTR_ENABLED_DEFAULT: False,
ATTR_ICON: "mdi:signal-off",
ATTR_NAME: "Available",
ATTR_STATE: "get_available",
ATTR_UNIT_OF_MEASUREMENT: None,
},
CURRENT_POWER_SENSOR_ID: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
ATTR_ENABLED_DEFAULT: True,
ATTR_ICON: None,
ATTR_NAME: "Power usage",
ATTR_STATE: "get_power_usage",
ATTR_UNIT_OF_MEASUREMENT: POWER_WATT,
},
TODAY_ENERGY_SENSOR_ID: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER,
ATTR_ENABLED_DEFAULT: True,
ATTR_ICON: None,
ATTR_NAME: "Power consumption today",
ATTR_STATE: "get_power_consumption_today",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
},
}

# Switch types
USB_SWITCHES = {
"relay": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_OUTLET,
ATTR_ENABLED_DEFAULT: True,
ATTR_ICON: None,
ATTR_NAME: "Relay state",
ATTR_STATE: "get_relay_state",
"switch": "set_relay_state",
ATTR_UNIT_OF_MEASUREMENT: "state",
}
}
32 changes: 26 additions & 6 deletions homeassistant/components/plugwise/strings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"options": {
"step": {
"none": {
"title": "No Options available",
"description": "This Integration does not provide any Options"
},
"init": {
"description": "Adjust Plugwise Options",
"data": {
Expand All @@ -10,6 +14,7 @@
}
},
"config": {
"flow_title": "Smile: {name}",
"step": {
"user": {
"title": "Plugwise type",
Expand All @@ -20,23 +25,38 @@
},
"user_gateway": {
"title": "Connect to the Smile",
"description": "Please enter",
"description": "Please enter:",
"data": {
"password": "Smile ID",
"username" : "Smile Username",
"host": "[%key:common::config_flow::data::ip%]",
"port": "[%key:common::config_flow::data::port%]",
"username" : "Smile Username"
"port": "[%key:common::config_flow::data::port%]"
}
},
"user_usb": {
"title": "Setup direct connection to legacy Plugwise USB-stick",
"description": "Please enter:",
"data": {
"usb_path": "[%key:common::config_flow::data::usb_path%]"
}
},
"manual_path": {
"data": {
"usb_path": "[%key:common::config_flow::data::usb_path%]"
}
}
},
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_auth": "Invalid authentication, check the 8 characters of your Smile ID",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"network_down": "Plugwise Zigbee network is down",
"network_timeout": "Network communication timeout",
"stick_init": "Initialization of Plugwise USB-stick failed",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"flow_title": "Smile: {name}"
}
}
}
Loading