Skip to content

Commit

Permalink
Merge pull request #24636 from home-assistant/rc
Browse files Browse the repository at this point in the history
0.94.4
  • Loading branch information
balloob authored Jun 19, 2019
2 parents aa91211 + 4e06835 commit d85ae5d
Show file tree
Hide file tree
Showing 11 changed files with 578 additions and 145 deletions.
9 changes: 6 additions & 3 deletions homeassistant/components/esphome/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ async def _async_authenticate_or_add(self, user_input,
self.context['title_placeholders'] = {
'name': self._name
}
self.context['name'] = self._name

# Only show authentication step if device uses password
if device_info.uses_password:
Expand Down Expand Up @@ -98,9 +99,11 @@ async def async_step_zeroconf(self, user_input: ConfigType):
already_configured = data.device_info.name == node_name

if already_configured:
return self.async_abort(
reason='already_configured'
)
return self.async_abort(reason='already_configured')

for flow in self._async_in_progress():
if flow['context']['name'] == node_name:
return self.async_abort(reason='already_configured')

return await self._async_authenticate_or_add(user_input={
'host': address,
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/sun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def update_events(self, utc_point_in_time):
utc_point_in_time, 'dusk', PHASE_ASTRONOMICAL_TWILIGHT)
self.next_midnight = self._check_event(
utc_point_in_time, 'solar_midnight', None)
self.location.solar_depression = 'civil'

# if the event was solar midday or midnight, phase will now
# be None. Solar noon doesn't always happen when the sun is
Expand Down
118 changes: 51 additions & 67 deletions homeassistant/components/tplink/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,43 @@
from homeassistant.const import CONF_HOST
from homeassistant import config_entries
import homeassistant.helpers.config_validation as cv
from .config_flow import async_get_devices
from .const import DOMAIN
from homeassistant.helpers.typing import ConfigType, HomeAssistantType

from .common import (
async_discover_devices,
get_static_devices,
ATTR_CONFIG,
CONF_DIMMER,
CONF_DISCOVERY,
CONF_LIGHT,
CONF_SWITCH,
SmartDevices
)

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'tplink'

TPLINK_HOST_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string
})

CONF_LIGHT = 'light'
CONF_SWITCH = 'switch'
CONF_DISCOVERY = 'discovery'

ATTR_CONFIG = 'config'

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional('light', default=[]): vol.All(cv.ensure_list,
[TPLINK_HOST_SCHEMA]),
vol.Optional('switch', default=[]): vol.All(cv.ensure_list,
[TPLINK_HOST_SCHEMA]),
vol.Optional('discovery', default=True): cv.boolean,
vol.Optional(CONF_LIGHT, default=[]): vol.All(
cv.ensure_list,
[TPLINK_HOST_SCHEMA]
),
vol.Optional(CONF_SWITCH, default=[]): vol.All(
cv.ensure_list,
[TPLINK_HOST_SCHEMA]
),
vol.Optional(CONF_DIMMER, default=[]): vol.All(
cv.ensure_list,
[TPLINK_HOST_SCHEMA]
),
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
}),
}, extra=vol.ALLOW_EXTRA)

Expand All @@ -46,76 +61,45 @@ async def async_setup(hass, config):
return True


async def async_setup_entry(hass, config_entry):
async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigType):
"""Set up TPLink from a config entry."""
from pyHS100 import SmartBulb, SmartPlug, SmartDeviceException

devices = {}

config_data = hass.data[DOMAIN].get(ATTR_CONFIG)

# These will contain the initialized devices
lights = hass.data[DOMAIN][CONF_LIGHT] = []
switches = hass.data[DOMAIN][CONF_SWITCH] = []

# If discovery is defined and not disabled, discover devices
# If initialized from configure integrations, there's no config
# so we default here to True
if config_data is None or config_data[CONF_DISCOVERY]:
devs = await async_get_devices(hass)
_LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devs))
devices.update(devs)
# Add static devices
static_devices = SmartDevices()
if config_data is not None:
static_devices = get_static_devices(
config_data,
)

def _device_for_type(host, type_):
dev = None
if type_ == CONF_LIGHT:
dev = SmartBulb(host)
elif type_ == CONF_SWITCH:
dev = SmartPlug(host)
lights.extend(static_devices.lights)
switches.extend(static_devices.switches)

return dev
# Add discovered devices
if config_data is None or config_data[CONF_DISCOVERY]:
discovered_devices = await async_discover_devices(hass, static_devices)

# When arriving from configure integrations, we have no config data.
if config_data is not None:
for type_ in [CONF_LIGHT, CONF_SWITCH]:
for entry in config_data[type_]:
try:
host = entry['host']
dev = _device_for_type(host, type_)
devices[host] = dev
_LOGGER.debug("Succesfully added %s %s: %s",
type_, host, dev)
except SmartDeviceException as ex:
_LOGGER.error("Unable to initialize %s %s: %s",
type_, host, ex)

# This is necessary to avoid I/O blocking on is_dimmable
def _fill_device_lists():
for dev in devices.values():
if isinstance(dev, SmartPlug):
try:
if dev.is_dimmable: # Dimmers act as lights
lights.append(dev)
else:
switches.append(dev)
except SmartDeviceException as ex:
_LOGGER.error("Unable to connect to device %s: %s",
dev.host, ex)

elif isinstance(dev, SmartBulb):
lights.append(dev)
else:
_LOGGER.error("Unknown smart device type: %s", type(dev))

# Avoid blocking on is_dimmable
await hass.async_add_executor_job(_fill_device_lists)
lights.extend(discovered_devices.lights)
switches.extend(discovered_devices.switches)

forward_setup = hass.config_entries.async_forward_entry_setup
if lights:
_LOGGER.debug("Got %s lights: %s", len(lights), lights)
_LOGGER.debug(
"Got %s lights: %s",
len(lights),
", ".join([d.host for d in lights])
)
hass.async_create_task(forward_setup(config_entry, 'light'))
if switches:
_LOGGER.debug("Got %s switches: %s", len(switches), switches)
_LOGGER.debug(
"Got %s switches: %s",
len(switches),
", ".join([d.host for d in switches])
)
hass.async_create_task(forward_setup(config_entry, 'switch'))

return True
Expand Down
202 changes: 202 additions & 0 deletions homeassistant/components/tplink/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"""Common code for tplink."""
import asyncio
import logging
from datetime import timedelta
from typing import Any, Callable, List

from pyHS100 import (
SmartBulb,
SmartDevice,
SmartPlug,
SmartDeviceException
)

from homeassistant.helpers.typing import HomeAssistantType

_LOGGER = logging.getLogger(__name__)


ATTR_CONFIG = 'config'
CONF_DIMMER = 'dimmer'
CONF_DISCOVERY = 'discovery'
CONF_LIGHT = 'light'
CONF_SWITCH = 'switch'


class SmartDevices:
"""Hold different kinds of devices."""

def __init__(
self,
lights: List[SmartDevice] = None,
switches: List[SmartDevice] = None
):
"""Constructor."""
self._lights = lights or []
self._switches = switches or []

@property
def lights(self):
"""Get the lights."""
return self._lights

@property
def switches(self):
"""Get the switches."""
return self._switches

def has_device_with_host(self, host):
"""Check if a devices exists with a specific host."""
for device in self.lights + self.switches:
if device.host == host:
return True

return False


async def async_get_discoverable_devices(hass):
"""Return if there are devices that can be discovered."""
from pyHS100 import Discover

def discover():
devs = Discover.discover()
return devs
return await hass.async_add_executor_job(discover)


async def async_discover_devices(
hass: HomeAssistantType,
existing_devices: SmartDevices
) -> SmartDevices:
"""Get devices through discovery."""
_LOGGER.debug("Discovering devices")
devices = await async_get_discoverable_devices(hass)
_LOGGER.info(
"Discovered %s TP-Link smart home device(s)",
len(devices)
)

lights = []
switches = []

def process_devices():
for dev in devices.values():
# If this device already exists, ignore dynamic setup.
if existing_devices.has_device_with_host(dev.host):
continue

if isinstance(dev, SmartPlug):
try:
if dev.is_dimmable: # Dimmers act as lights
lights.append(dev)
else:
switches.append(dev)
except SmartDeviceException as ex:
_LOGGER.error("Unable to connect to device %s: %s",
dev.host, ex)

elif isinstance(dev, SmartBulb):
lights.append(dev)
else:
_LOGGER.error("Unknown smart device type: %s", type(dev))

await hass.async_add_executor_job(process_devices)

return SmartDevices(lights, switches)


def get_static_devices(config_data) -> SmartDevices:
"""Get statically defined devices in the config."""
_LOGGER.debug("Getting static devices")
lights = []
switches = []

for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_DIMMER]:
for entry in config_data[type_]:
host = entry['host']

if type_ == CONF_LIGHT:
lights.append(SmartBulb(host))
elif type_ == CONF_SWITCH:
switches.append(SmartPlug(host))
# Dimmers need to be defined as smart plugs to work correctly.
elif type_ == CONF_DIMMER:
lights.append(SmartPlug(host))

return SmartDevices(
lights,
switches
)


async def async_add_entities_retry(
hass: HomeAssistantType,
async_add_entities: Callable[[List[Any], bool], None],
objects: List[Any],
callback: Callable[[Any, Callable], None],
interval: timedelta = timedelta(seconds=60)
):
"""
Add entities now and retry later if issues are encountered.
If the callback throws an exception or returns false, that
object will try again a while later.
This is useful for devices that are not online when hass starts.
:param hass:
:param async_add_entities: The callback provided to a
platform's async_setup.
:param objects: The objects to create as entities.
:param callback: The callback that will perform the add.
:param interval: THe time between attempts to add.
:return: A callback to cancel the retries.
"""
add_objects = objects.copy()

is_cancelled = False

def cancel_interval_callback():
nonlocal is_cancelled
is_cancelled = True

async def process_objects_loop(delay: int):
if is_cancelled:
return

await process_objects()

if not add_objects:
return

await asyncio.sleep(delay)

hass.async_create_task(process_objects_loop(delay))

async def process_objects(*args):
# Process each object.
for add_object in list(add_objects):
# Call the individual item callback.
try:
_LOGGER.debug(
"Attempting to add object of type %s",
type(add_object)
)
result = await hass.async_add_job(
callback,
add_object,
async_add_entities
)
except SmartDeviceException as ex:
_LOGGER.debug(
str(ex)
)
result = False

if result is True or result is None:
_LOGGER.debug("Added object.")
add_objects.remove(add_object)
else:
_LOGGER.debug("Failed to add object, will try again later")

await process_objects_loop(interval.seconds)

return cancel_interval_callback
Loading

0 comments on commit d85ae5d

Please sign in to comment.