Merge pull request #8270 from home-assistant/release-0-48

0.48
This commit is contained in:
Paulus Schoutsen 2017-07-01 16:58:10 -07:00 committed by GitHub
commit 7461c57542
173 changed files with 6458 additions and 3464 deletions

View File

@ -35,23 +35,35 @@ omit =
homeassistant/components/bloomsky.py
homeassistant/components/*/bloomsky.py
homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py
homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py
homeassistant/components/dweet.py
homeassistant/components/*/dweet.py
homeassistant/components/eight_sleep.py
homeassistant/components/*/eight_sleep.py
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py
homeassistant/components/google.py
homeassistant/components/*/google.py
homeassistant/components/insteon_hub.py
homeassistant/components/*/insteon_hub.py
homeassistant/components/hdmi_cec.py
homeassistant/components/*/hdmi_cec.py
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_local.py
@ -65,12 +77,18 @@ omit =
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/joaoapps_join.py
homeassistant/components/*/joaoapps_join.py
homeassistant/components/juicenet.py
homeassistant/components/*/juicenet.py
homeassistant/components/kira.py
homeassistant/components/*/kira.py
homeassistant/components/knx.py
homeassistant/components/*/knx.py
homeassistant/components/lutron.py
homeassistant/components/*/lutron.py
@ -80,15 +98,27 @@ omit =
homeassistant/components/mailgun.py
homeassistant/components/*/mailgun.py
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
homeassistant/components/modbus.py
homeassistant/components/*/modbus.py
homeassistant/components/mysensors.py
homeassistant/components/*/mysensors.py
homeassistant/components/neato.py
homeassistant/components/*/neato.py
homeassistant/components/nest.py
homeassistant/components/*/nest.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/octoprint.py
homeassistant/components/*/octoprint.py
@ -116,6 +146,9 @@ omit =
homeassistant/components/scsgate.py
homeassistant/components/*/scsgate.py
homeassistant/components/tado.py
homeassistant/components/*/tado.py
homeassistant/components/tellduslive.py
homeassistant/components/*/tellduslive.py
@ -148,45 +181,18 @@ omit =
homeassistant/components/wink.py
homeassistant/components/*/wink.py
homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/neato.py
homeassistant/components/*/neato.py
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/knx.py
homeassistant/components/*/knx.py
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/tado.py
homeassistant/components/*/tado.py
homeassistant/components/zha/__init__.py
homeassistant/components/zha/const.py
homeassistant/components/*/zha.py
homeassistant/components/eight_sleep.py
homeassistant/components/*/eight_sleep.py
homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py
homeassistant/components/zoneminder.py
homeassistant/components/*/zoneminder.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py
@ -224,11 +230,11 @@ omit =
homeassistant/components/climate/sensibo.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py
homeassistant/components/cover/myq.py
homeassistant/components/cover/opengarage.py
homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py
homeassistant/components/cover/wink.py
homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
@ -242,6 +248,7 @@ omit =
homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/linksys_ap.py
homeassistant/components/device_tracker/linksys_smart.py
homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/mikrotik.py
homeassistant/components/device_tracker/netgear.py
@ -263,12 +270,10 @@ omit =
homeassistant/components/fan/mqtt.py
homeassistant/components/feedreader.py
homeassistant/components/foursquare.py
homeassistant/components/hdmi_cec.py
homeassistant/components/ifttt.py
homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py
homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py
homeassistant/components/keyboard_remote.py
homeassistant/components/light/avion.py
@ -278,7 +283,7 @@ omit =
homeassistant/components/light/flux_led.py
homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx/*.py
homeassistant/components/light/lifx.py
homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py
@ -312,7 +317,6 @@ omit =
homeassistant/components/media_player/frontier_silicon.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/gstreamer.py
homeassistant/components/media_player/hdmi_cec.py
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
@ -342,13 +346,13 @@ omit =
homeassistant/components/notify/aws_sns.py
homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/ciscospark.py
homeassistant/components/notify/clicksend.py
homeassistant/components/notify/discord.py
homeassistant/components/notify/facebook.py
homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py
@ -379,8 +383,10 @@ omit =
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py
homeassistant/components/sensor/bh1750.py
homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/blockchain.py
homeassistant/components/sensor/bme280.py
homeassistant/components/sensor/bom.py
homeassistant/components/sensor/broadlink.py
homeassistant/components/sensor/buienradar.py
@ -419,6 +425,7 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hddtemp.py
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/hydroquebec.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py
@ -473,6 +480,7 @@ omit =
homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py
homeassistant/components/sensor/usps.py
homeassistant/components/sensor/vasttrafik.py
@ -480,6 +488,7 @@ omit =
homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py
homeassistant/components/sensor/zamg.py
homeassistant/components/shiftr.py
homeassistant/components/spc.py
homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/anel_pwrctrl.py
@ -489,7 +498,6 @@ omit =
homeassistant/components/switch/dlink.py
homeassistant/components/switch/edimax.py
homeassistant/components/switch/fritzdect.py
homeassistant/components/switch/hdmi_cec.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/hook.py
homeassistant/components/switch/kankun.py

View File

@ -1,2 +1,14 @@
.tox
# General files
.git
.github
config
# Test related files
.tox
# Other virtualization methods
venv
.vagrant
# Temporary files
**/__pycache__

View File

@ -1,3 +1,7 @@
# Notice:
# When updating this file, please also update virtualization/Docker/Dockerfile.dev
# This way, the development image and the production image are kept in sync.
FROM python:3.6
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
@ -21,8 +25,12 @@ RUN virtualization/Docker/setup_docker_prereqs
# Install hass component dependencies
COPY requirements_all.txt requirements_all.txt
# Uninstall enum34 because some depenndecies install it but breaks Python 3.4+.
# See PR #8103 for more info.
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet && \
pip3 uninstall -y enum34
# Copy source
COPY . .

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.verisure/
"""
import logging
from time import sleep
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.verisure import HUB as hub
@ -20,20 +21,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Verisure platform."""
alarms = []
if int(hub.config.get(CONF_ALARM, 1)):
hub.update_alarms()
alarms.extend([
VerisureAlarm(value.id)
for value in hub.alarm_status.values()
])
hub.update_overview()
alarms.append(VerisureAlarm())
add_devices(alarms)
def set_arm_state(state, code=None):
"""Send set arm state command."""
transaction_id = hub.session.set_arm_state(code, state)[
'armStateChangeTransactionId']
_LOGGER.info('verisure set arm state %s', state)
transaction = {}
while 'result' not in transaction:
sleep(0.5)
transaction = hub.session.get_arm_state_transaction(transaction_id)
# pylint: disable=unexpected-keyword-arg
hub.update_overview(no_throttle=True)
class VerisureAlarm(alarm.AlarmControlPanel):
"""Representation of a Verisure alarm status."""
def __init__(self, device_id):
"""Initialize the Verisure alarm panel."""
self._id = device_id
def __init__(self):
"""Initalize the Verisure alarm panel."""
self._state = STATE_UNKNOWN
self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None
@ -41,18 +51,13 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property
def name(self):
"""Return the name of the device."""
return 'Alarm {}'.format(self._id)
return '{} alarm'.format(hub.session.installations[0]['alias'])
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def available(self):
"""Return True if entity is available."""
return hub.available
@property
def code_format(self):
"""Return the code format as regex."""
@ -65,33 +70,26 @@ class VerisureAlarm(alarm.AlarmControlPanel):
def update(self):
"""Update alarm status."""
hub.update_alarms()
if hub.alarm_status[self._id].status == 'unarmed':
hub.update_overview()
status = hub.get_first("$.armState.statusType")
if status == 'DISARMED':
self._state = STATE_ALARM_DISARMED
elif hub.alarm_status[self._id].status == 'armedhome':
elif status == 'ARMED_HOME':
self._state = STATE_ALARM_ARMED_HOME
elif hub.alarm_status[self._id].status == 'armed':
elif status == 'ARMED_AWAY':
self._state = STATE_ALARM_ARMED_AWAY
elif hub.alarm_status[self._id].status != 'pending':
_LOGGER.error(
"Unknown alarm state %s", hub.alarm_status[self._id].status)
self._changed_by = hub.alarm_status[self._id].name
elif status != 'PENDING':
_LOGGER.error('Unknown alarm state %s', status)
self._changed_by = hub.get_first("$.armState.name")
def alarm_disarm(self, code=None):
"""Send disarm command."""
hub.my_pages.alarm.set(code, 'DISARMED')
_LOGGER.info("Verisure alarm disarming")
hub.my_pages.alarm.wait_while_pending()
set_arm_state('DISARMED', code)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
hub.my_pages.alarm.set(code, 'ARMED_HOME')
_LOGGER.info("Verisure alarm arming home")
hub.my_pages.alarm.wait_while_pending()
set_arm_state('ARMED_HOME', code)
def alarm_arm_away(self, code=None):
"""Send arm away command."""
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
_LOGGER.info("Verisure alarm arming away")
hub.my_pages.alarm.wait_while_pending()
set_arm_state('ARMED_AWAY', code)

View File

@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'alert'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
CONF_DONE_MESSAGE = 'done_message'
CONF_CAN_ACK = 'can_acknowledge'
CONF_NOTIFIERS = 'notifiers'
CONF_REPEAT = 'repeat'
@ -35,6 +36,7 @@ DEFAULT_SKIP_FIRST = False
ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_DONE_MESSAGE, default=None): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
@ -121,10 +123,10 @@ def async_setup(hass, config):
# Setup alerts
for entity_id, alert in alerts.items():
entity = Alert(hass, entity_id,
alert[CONF_NAME], alert[CONF_ENTITY_ID],
alert[CONF_STATE], alert[CONF_REPEAT],
alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS],
alert[CONF_CAN_ACK])
alert[CONF_NAME], alert[CONF_DONE_MESSAGE],
alert[CONF_ENTITY_ID], alert[CONF_STATE],
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
all_alerts[entity.entity_id] = entity
# Read descriptions
@ -154,8 +156,8 @@ def async_setup(hass, config):
class Alert(ToggleEntity):
"""Representation of an alert."""
def __init__(self, hass, entity_id, name, watched_entity_id, state,
repeat, skip_first, notifiers, can_ack):
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
state, repeat, skip_first, notifiers, can_ack):
"""Initialize the alert."""
self.hass = hass
self._name = name
@ -163,6 +165,7 @@ class Alert(ToggleEntity):
self._skip_first = skip_first
self._notifiers = notifiers
self._can_ack = can_ack
self._done_message = done_message
self._delay = [timedelta(minutes=val) for val in repeat]
self._next_delay = 0
@ -170,6 +173,7 @@ class Alert(ToggleEntity):
self._firing = False
self._ack = False
self._cancel = None
self._send_done_message = False
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
event.async_track_state_change(
@ -230,6 +234,8 @@ class Alert(ToggleEntity):
self._cancel()
self._ack = False
self._firing = False
if self._done_message and self._send_done_message:
yield from self._notify_done_message()
self.hass.async_add_job(self.async_update_ha_state)
@asyncio.coroutine
@ -249,11 +255,21 @@ class Alert(ToggleEntity):
if not self._ack:
_LOGGER.info("Alerting: %s", self._name)
self._send_done_message = True
for target in self._notifiers:
yield from self.hass.services.async_call(
'notify', target, {'message': self._name})
yield from self._schedule_notify()
@asyncio.coroutine
def _notify_done_message(self, *args):
"""Send notification of complete alert."""
_LOGGER.info("Alerting: %s", self._done_message)
self._send_done_message = False
for target in self._notifiers:
yield from self.hass.services.async_call(
'notify', target, {'message': self._done_message})
@asyncio.coroutine
def async_turn_on(self):
"""Async Unacknowledge alert."""

View File

@ -1,27 +1,27 @@
"""
This component provides basic support for Netgear Arlo IP cameras.
For more details about this platform, please refer to the documentation at
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/arlo/
"""
import logging
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.loader as loader
from requests.exceptions import HTTPError, ConnectTimeout
import homeassistant.loader as loader
from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
REQUIREMENTS = ['pyarlo==0.0.4']
_LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com'
DOMAIN = 'arlo'
CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
DATA_ARLO = 'data_arlo'
DEFAULT_BRAND = 'Netgear Arlo'
DOMAIN = 'arlo'
NOTIFICATION_ID = 'arlo_notification'
NOTIFICATION_TITLE = 'Arlo Camera Setup'
@ -47,7 +47,7 @@ def setup(hass, config):
arlo = PyArlo(username, password, preload=False)
if not arlo.is_connected:
return False
hass.data['arlo'] = arlo
hass.data[DATA_ARLO] = arlo
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
persistent_notification.create(

View File

@ -11,6 +11,7 @@ import os
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
CONF_HOST, CONF_INCLUDE, CONF_NAME,
CONF_PASSWORD, CONF_TRIGGER_TIME,
@ -18,11 +19,12 @@ from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
from homeassistant.components.discovery import SERVICE_AXIS
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['axis==7']
REQUIREMENTS = ['axis==8']
_LOGGER = logging.getLogger(__name__)
@ -59,6 +61,21 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
SERVICE_VAPIX_CALL = 'vapix_call'
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response'
SERVICE_CGI = 'cgi'
SERVICE_ACTION = 'action'
SERVICE_PARAM = 'param'
SERVICE_DEFAULT_CGI = 'param.cgi'
SERVICE_DEFAULT_ACTION = 'update'
SERVICE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(SERVICE_PARAM): cv.string,
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
})
def request_configuration(hass, name, host, serialnumber):
"""Request configuration steps from the user."""
@ -135,23 +152,34 @@ def setup(hass, base_config):
def axis_device_discovered(service, discovery_info):
"""Called when axis devices has been found."""
host = discovery_info['host']
host = discovery_info[CONF_HOST]
name = discovery_info['hostname']
serialnumber = discovery_info['properties']['macaddress']
if serialnumber not in AXIS_DEVICES:
config_file = _read_config(hass)
if serialnumber in config_file:
# Device config saved to file
try:
config = DEVICE_SCHEMA(config_file[serialnumber])
config[CONF_HOST] = host
except vol.Invalid as err:
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
return False
if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name'])
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
else:
# New device, create configuration request for UI
request_configuration(hass, name, host, serialnumber)
else:
# Device already registered, but on a different IP
device = AXIS_DEVICES[serialnumber]
device.url = host
async_dispatcher_send(hass,
DOMAIN + '_' + device.name + '_new_ip',
host)
# Register discovery service
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
if DOMAIN in base_config:
@ -160,7 +188,30 @@ def setup(hass, base_config):
if CONF_NAME not in config:
config[CONF_NAME] = device
if not setup_device(hass, config):
_LOGGER.error("Couldn\'t set up %s", config['name'])
_LOGGER.error("Couldn\'t set up %s", config[CONF_NAME])
# Services to communicate with device.
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def vapix_service(call):
"""Service to send a message."""
for _, device in AXIS_DEVICES.items():
if device.name == call.data[CONF_NAME]:
response = device.do_request(call.data[SERVICE_CGI],
call.data[SERVICE_ACTION],
call.data[SERVICE_PARAM])
hass.bus.async_fire(SERVICE_VAPIX_CALL_RESPONSE, response)
return True
_LOGGER.info("Couldn\'t find device %s", call.data[CONF_NAME])
return False
# Register service with Home Assistant.
hass.services.register(DOMAIN,
SERVICE_VAPIX_CALL,
vapix_service,
descriptions[DOMAIN][SERVICE_VAPIX_CALL],
schema=SERVICE_SCHEMA)
return True
@ -190,8 +241,16 @@ def setup_device(hass, config):
if enable_metadatastream:
device.initialize_new_event = event_initialized
device.initiate_metadatastream()
if not device.initiate_metadatastream():
notification = get_component('persistent_notification')
notification.create(hass,
'Dependency missing for sensors, '
'please check documentation',
title=DOMAIN,
notification_id='axis_notification')
AXIS_DEVICES[device.serial_number] = device
return True
@ -311,4 +370,4 @@ REMAP = [{'type': 'motion',
'class': 'input',
'topic': 'tns1:Device/tnsaxis:IO/Port',
'subscribe': 'onvif:Device/axis:IO/Port',
'platform': 'sensor'}, ]
'platform': 'binary_sensor'}, ]

View File

@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ArestBinarySensor(
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
device_class, pin)])
device_class, pin)], True)
class ArestBinarySensor(BinarySensorDevice):
@ -64,7 +64,6 @@ class ArestBinarySensor(BinarySensorDevice):
self._name = name
self._device_class = device_class
self._pin = pin
self.update()
if self._pin is not None:
request = requests.get(

View File

@ -8,19 +8,18 @@ import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.digital_ocean import (
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
ATTR_REGION, ATTR_VCPUS)
from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv
ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Droplet'
DEFAULT_SENSOR_CLASS = 'motion'
DEFAULT_SENSOR_CLASS = 'moving'
DEPENDENCIES = ['digital_ocean']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -30,19 +29,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Digital Ocean droplet sensor."""
digital_ocean = get_component('digital_ocean')
digital = hass.data.get(DATA_DIGITAL_OCEAN)
if not digital:
return False
droplets = config.get(CONF_DROPLETS)
dev = []
for droplet in droplets:
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
droplet_id = digital.get_droplet_id(droplet)
if droplet_id is None:
_LOGGER.error("Droplet %s is not available", droplet)
return False
dev.append(DigitalOceanBinarySensor(
digital_ocean.DIGITAL_OCEAN, droplet_id))
dev.append(DigitalOceanBinarySensor(digital, droplet_id))
add_devices(dev)
add_devices(dev, True)
class DigitalOceanBinarySensor(BinarySensorDevice):
@ -53,7 +54,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
self._digital_ocean = do
self._droplet_id = droplet_id
self._state = None
self.update()
self.data = None
@property
def name(self):

View File

@ -50,6 +50,10 @@ class ModbusCoilSensor(BinarySensorDevice):
self._coil = int(coil)
self._value = None
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return the state of the sensor."""

View File

@ -0,0 +1,233 @@
"""
Support for RFXtrx binary sensors.
Lighting4 devices (sensors based on PT2262 encoder) are supported and
tested. Other types may need some work.
"""
import logging
import voluptuous as vol
from homeassistant.components import rfxtrx
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import event as evt
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rfxtrx import (
ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_OFF_DELAY, ATTR_FIREEVENT,
ATTR_DATA_BITS, CONF_DEVICES
)
from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
)
DEPENDENCIES = ["rfxtrx"]
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required("platform"): rfxtrx.DOMAIN,
vol.Optional(CONF_DEVICES, default={}): vol.All(
dict, rfxtrx.valid_binary_sensor),
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
}, extra=vol.ALLOW_EXTRA)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the Binary Sensor platform to rfxtrx."""
import RFXtrx as rfxtrxmod
sensors = []
for packet_id, entity in config['devices'].items():
event = rfxtrx.get_rfx_object(packet_id)
device_id = slugify(event.device.id_string.lower())
if device_id in rfxtrx.RFX_DEVICES:
continue
if entity[ATTR_DATA_BITS] is not None:
_LOGGER.info("Masked device id: %s",
rfxtrx.get_pt2262_deviceid(device_id,
entity[ATTR_DATA_BITS]))
_LOGGER.info("Add %s rfxtrx.binary_sensor (class %s)",
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
device = RfxtrxBinarySensor(event, entity[ATTR_NAME],
entity[CONF_DEVICE_CLASS],
entity[ATTR_FIREEVENT],
entity[ATTR_OFF_DELAY],
entity[ATTR_DATA_BITS],
entity[CONF_COMMAND_ON],
entity[CONF_COMMAND_OFF])
device.hass = hass
sensors.append(device)
rfxtrx.RFX_DEVICES[device_id] = device
add_devices_callback(sensors)
# pylint: disable=too-many-branches
def binary_sensor_update(event):
"""Callback for control updates from the RFXtrx gateway."""
if not isinstance(event, rfxtrxmod.ControlEvent):
return
device_id = slugify(event.device.id_string.lower())
if device_id in rfxtrx.RFX_DEVICES:
sensor = rfxtrx.RFX_DEVICES[device_id]
else:
sensor = rfxtrx.get_pt2262_device(device_id)
if sensor is None:
# Add the entity if not exists and automatic_add is True
if not config[ATTR_AUTOMATIC_ADD]:
return
poss_dev = rfxtrx.find_possible_pt2262_device(device_id)
if poss_dev is not None:
poss_id = slugify(poss_dev.event.device.id_string.lower())
_LOGGER.info("Found possible matching deviceid %s.",
poss_id)
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
sensor = RfxtrxBinarySensor(event, pkt_id)
rfxtrx.RFX_DEVICES[device_id] = sensor
add_devices_callback([sensor])
_LOGGER.info("Added binary sensor %s "
"(Device_id: %s Class: %s Sub: %s)",
pkt_id,
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype)
elif not isinstance(sensor, RfxtrxBinarySensor):
return
else:
_LOGGER.info("Binary sensor update "
"(Device_id: %s Class: %s Sub: %s)",
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype)
if sensor.is_pt2262:
cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits)
_LOGGER.info("applying cmd %s to device_id: %s)",
cmd, sensor.masked_id)
sensor.apply_cmd(int(cmd, 16))
else:
rfxtrx.apply_received_command(event)
if (sensor.is_on and sensor.off_delay is not None and
sensor.delay_listener is None):
def off_delay_listener(now):
"""Switch device off after a delay."""
sensor.delay_listener = None
sensor.update_state(False)
sensor.delay_listener = evt.track_point_in_time(
hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay
)
# Subscribe to main rfxtrx events
if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update)
# pylint: disable=too-many-instance-attributes,too-many-arguments
class RfxtrxBinarySensor(BinarySensorDevice):
"""An Rfxtrx binary sensor."""
def __init__(self, event, name, device_class=None,
should_fire=False, off_delay=None, data_bits=None,
cmd_on=None, cmd_off=None):
"""Initialize the sensor."""
self.event = event
self._name = name
self._should_fire_event = should_fire
self._device_class = device_class
self._off_delay = off_delay
self._state = False
self.delay_listener = None
self._data_bits = data_bits
self._cmd_on = cmd_on
self._cmd_off = cmd_off
if data_bits is not None:
self._masked_id = rfxtrx.get_pt2262_deviceid(
event.device.id_string.lower(),
data_bits)
def __str__(self):
"""Return the name of the sensor."""
return self._name
@property
def name(self):
"""Return the device name."""
return self._name
@property
def is_pt2262(self):
"""Return true if the device is PT2262-based."""
return self._data_bits is not None
@property
def masked_id(self):
"""Return the masked device id (isolated address bits)."""
return self._masked_id
@property
def data_bits(self):
"""Return the number of data bits."""
return self._data_bits
@property
def cmd_on(self):
"""Return the value of the 'On' command."""
return self._cmd_on
@property
def cmd_off(self):
"""Return the value of the 'Off' command."""
return self._cmd_off
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def should_fire_event(self):
"""Return is the device must fire event."""
return self._should_fire_event
@property
def device_class(self):
"""Return the sensor class."""
return self._device_class
@property
def off_delay(self):
"""Return the off_delay attribute value."""
return self._off_delay
@property
def is_on(self):
"""Return true if the sensor state is True."""
return self._state
def apply_cmd(self, cmd):
"""Apply a command for updating the state."""
if cmd == self.cmd_on:
self.update_state(True)
elif cmd == self.cmd_off:
self.update_state(False)
def update_state(self, state):
"""Update the state of the device."""
self._state = state
self.schedule_update_ha_state()

View File

@ -0,0 +1,59 @@
"""
Interfaces with Verisure sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.verisure/
"""
import logging
from homeassistant.components.verisure import HUB as hub
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.verisure import CONF_DOOR_WINDOW
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Verisure binary sensors."""
sensors = []
hub.update_overview()
if int(hub.config.get(CONF_DOOR_WINDOW, 1)):
sensors.extend([
VerisureDoorWindowSensor(device_label)
for device_label in hub.get(
"$.doorWindow.doorWindowDevice[*].deviceLabel")])
add_devices(sensors)
class VerisureDoorWindowSensor(BinarySensorDevice):
"""Verisure door window sensor."""
def __init__(self, device_label):
"""Initialize the modbus coil sensor."""
self._device_label = device_label
@property
def name(self):
"""Return the name of the binary sensor."""
return hub.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].area",
self._device_label)
@property
def is_on(self):
"""Return the state of the sensor."""
return hub.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')].state",
self._device_label) == "OPEN"
@property
def available(self):
"""Return True if entity is available."""
return hub.get_first(
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
self._device_label) is not None
def update(self):
"""Update the state of the sensor."""
hub.update_overview()

View File

@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
if disc_info is None:
return
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
if not any(data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]):
return
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))

View File

@ -12,13 +12,16 @@ from datetime import timedelta
import logging
import hashlib
from random import SystemRandom
import os
import aiohttp
from aiohttp import web
import async_timeout
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE)
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
@ -26,9 +29,12 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.helpers.event import async_track_time_interval
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
SERVICE_EN_MOTION = 'enable_motion_detection'
SERVICE_DISEN_MOTION = 'disable_motion_detection'
DOMAIN = 'camera'
DEPENDENCIES = ['http']
SCAN_INTERVAL = timedelta(seconds=30)
@ -38,11 +44,30 @@ STATE_RECORDING = 'recording'
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'
DEFAULT_CONTENT_TYPE = 'image/jpeg'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
_RND = SystemRandom()
CAMERA_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
def enable_motion_detection(hass, entity_id=None):
"""Enable Motion Detection."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_EN_MOTION, data))
def disable_motion_detection(hass, entity_id=None):
"""Disable Motion Detection."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.async_add_job(hass.services.async_call(
DOMAIN, SERVICE_DISEN_MOTION, data))
@asyncio.coroutine
def async_get_image(hass, entity_id, timeout=10):
@ -92,6 +117,44 @@ def async_setup(hass, config):
hass.async_add_job(entity.async_update_ha_state())
async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL)
@asyncio.coroutine
def async_handle_camera_service(service):
"""Handle calls to the camera services."""
target_cameras = component.async_extract_from_service(service)
for camera in target_cameras:
if service.service == SERVICE_EN_MOTION:
yield from camera.async_enable_motion_detection()
elif service.service == SERVICE_DISEN_MOTION:
yield from camera.async_disable_motion_detection()
update_tasks = []
for camera in target_cameras:
if not camera.should_poll:
continue
update_coro = hass.async_add_job(
camera.async_update_ha_state(True))
if hasattr(camera, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
descriptions = yield from hass.async_add_job(
load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
DOMAIN, SERVICE_EN_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_EN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_DISEN_MOTION, async_handle_camera_service,
descriptions.get(SERVICE_DISEN_MOTION), schema=CAMERA_SERVICE_SCHEMA)
return True
@ -101,6 +164,7 @@ class Camera(Entity):
def __init__(self):
"""Initialize a camera."""
self.is_streaming = False
self.content_type = DEFAULT_CONTENT_TYPE
self.access_tokens = collections.deque([], 2)
self.async_update_token()
@ -124,6 +188,11 @@ class Camera(Entity):
"""Return the camera brand."""
return None
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return None
@property
def model(self):
"""Return the camera model."""
@ -149,16 +218,17 @@ class Camera(Entity):
response = web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
'boundary=--frameboundary')
yield from response.prepare(request)
def write(img_bytes):
"""Write image to stream."""
response.write(bytes(
'--jpegboundary\r\n'
'Content-Type: image/jpeg\r\n'
'--frameboundary\r\n'
'Content-Type: {}\r\n'
'Content-Length: {}\r\n\r\n'.format(
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
self.content_type, len(img_bytes)),
'utf-8') + img_bytes + b'\r\n')
last_image = None
@ -199,6 +269,22 @@ class Camera(Entity):
else:
return STATE_IDLE
def enable_motion_detection(self):
"""Enable motion detection in the camera."""
raise NotImplementedError()
def async_enable_motion_detection(self):
"""Call the job and enable motion detection."""
return self.hass.async_add_job(self.enable_motion_detection)
def disable_motion_detection(self):
"""Disable motion detection in camera."""
raise NotImplementedError()
def async_disable_motion_detection(self):
"""Call the job and disable motion detection."""
return self.hass.async_add_job(self.disable_motion_detection)
@property
def state_attributes(self):
"""Return the camera state attributes."""
@ -212,6 +298,9 @@ class Camera(Entity):
if self.brand:
attr['brand'] = self.brand
if self.motion_detection_enabled:
attr['motion_detection'] = self.motion_detection_enabled
return attr
@callback
@ -269,7 +358,8 @@ class CameraImageView(CameraView):
image = yield from camera.async_camera_image()
if image:
return web.Response(body=image, content_type='image/jpeg')
return web.Response(body=image,
content_type=camera.content_type)
return web.Response(status=500)

View File

@ -6,32 +6,32 @@ https://home-assistant.io/components/camera.arlo/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.components.arlo import DEFAULT_BRAND
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream)
DEPENDENCIES = ['arlo', 'ffmpeg']
_LOGGER = logging.getLogger(__name__)
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FFMPEG_ARGUMENTS):
cv.string,
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up an Arlo IP Camera."""
arlo = hass.data.get('arlo')
arlo = hass.data.get(DATA_ARLO)
if not arlo:
return False
@ -40,7 +40,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
cameras.append(ArloCam(hass, camera, config))
async_add_devices(cameras, True)
return True
class ArloCam(Camera):
@ -49,14 +48,15 @@ class ArloCam(Camera):
def __init__(self, hass, camera, device_info):
"""Initialize an Arlo camera."""
super().__init__()
self._camera = camera
self._base_stn = hass.data['arlo'].base_stations[0]
self._name = self._camera.name
self._motion_status = False
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
def camera_image(self):
"""Return a still image reponse from the camera."""
"""Return a still image response from the camera."""
return self._camera.last_image
@asyncio.coroutine
@ -90,3 +90,27 @@ class ArloCam(Camera):
def brand(self):
"""Camera brand."""
return DEFAULT_BRAND
@property
def should_poll(self):
"""Camera should poll periodically."""
return True
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
return self._motion_status
def set_base_station_mode(self, mode):
"""Set the mode in the base station."""
self._base_stn.mode = mode
def enable_motion_detection(self):
"""Enable the Motion detection in base station (Arm)."""
self._motion_status = True
self.set_base_station_mode(ARLO_MODE_ARMED)
def disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm)."""
self._motion_status = False
self.set_base_station_mode(ARLO_MODE_DISARMED)

View File

@ -7,15 +7,16 @@ https://home-assistant.io/components/camera.axis/
import logging
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['axis']
DOMAIN = 'axis'
DEPENDENCIES = [DOMAIN]
def _get_image_url(host, mode):
@ -27,12 +28,29 @@ def _get_image_url(host, mode):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Axis camera."""
device_info = {
CONF_NAME: discovery_info['name'],
CONF_USERNAME: discovery_info['username'],
CONF_PASSWORD: discovery_info['password'],
CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'),
config = {
CONF_NAME: discovery_info[CONF_NAME],
CONF_USERNAME: discovery_info[CONF_USERNAME],
CONF_PASSWORD: discovery_info[CONF_PASSWORD],
CONF_MJPEG_URL: _get_image_url(discovery_info[CONF_HOST], 'mjpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info[CONF_HOST],
'single'),
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
}
add_devices([MjpegCamera(hass, device_info)])
add_devices([AxisCamera(hass, config)])
class AxisCamera(MjpegCamera):
"""AxisCamera class."""
def __init__(self, hass, config):
"""Initialize Axis Communications camera component."""
super().__init__(hass, config)
async_dispatcher_connect(hass,
DOMAIN + '_' + config[CONF_NAME] + '_new_ip',
self._new_ip)
def _new_ip(self, host):
"""Set new IP for video stream."""
self._mjpeg_url = _get_image_url(host, 'mjpeg')
self._still_image_url = _get_image_url(host, 'mjpeg')

View File

@ -5,25 +5,29 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
import os
import logging
import homeassistant.util.dt as dt_util
from homeassistant.components.camera import Camera
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo camera platform."""
add_devices([
DemoCamera('Demo camera')
DemoCamera(hass, config, 'Demo camera')
])
class DemoCamera(Camera):
"""The representation of a Demo camera."""
def __init__(self, name):
def __init__(self, hass, config, name):
"""Initialize demo camera component."""
super().__init__()
self._parent = hass
self._name = name
self._motion_status = False
def camera_image(self):
"""Return a faked still image response."""
@ -38,3 +42,21 @@ class DemoCamera(Camera):
def name(self):
"""Return the name of this camera."""
return self._name
@property
def should_poll(self):
"""Camera should poll periodically."""
return True
@property
def motion_detection_enabled(self):
"""Camera Motion Detection Status."""
return self._motion_status
def enable_motion_detection(self):
"""Enable the Motion detection in base station (Arm)."""
self._motion_status = True
def disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm)."""
self._motion_status = False

View File

@ -17,13 +17,15 @@ from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.components.camera import (
PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, Camera)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
CONF_CONTENT_TYPE = 'content_type'
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
CONF_STILL_IMAGE_URL = 'still_image_url'
@ -37,6 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
})
@ -59,6 +62,7 @@ class GenericCamera(Camera):
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url.hass = hass
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self.content_type = device_info[CONF_CONTENT_TYPE]
username = device_info.get(CONF_USERNAME)
password = device_info.get(CONF_PASSWORD)

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.local_file/
"""
import logging
import mimetypes
import os
import voluptuous as vol
@ -46,6 +47,10 @@ class LocalFile(Camera):
self._name = name
self._file_path = file_path
# Set content type of local file
content, _ = mimetypes.guess_type(file_path)
if content is not None:
self.content_type = content
def camera_image(self):
"""Return image response."""

View File

@ -0,0 +1,17 @@
# Describes the format for available camera services
enable_motion_detection:
description: Enable the motion detection in a camera
fields:
entity_id:
description: Name(s) of entities to enable motion detection
example: 'camera.living_room_camera'
disable_motion_detection:
description: Disable the motion detection in a camera
fields:
entity_id:
description: Name(s) of entities to disable motion detection
example: 'camera.living_room_camera'

View File

@ -24,22 +24,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if not os.access(directory_path, os.R_OK):
_LOGGER.error("file path %s is not readable", directory_path)
return False
hub.update_smartcam()
hub.update_overview()
smartcams = []
smartcams.extend([
VerisureSmartcam(hass, value.deviceLabel, directory_path)
for value in hub.smartcam_status.values()])
VerisureSmartcam(hass, device_label, directory_path)
for device_label in hub.get(
"$.customerImageCameras[*].deviceLabel")])
add_devices(smartcams)
class VerisureSmartcam(Camera):
"""Representation of a Verisure camera."""
def __init__(self, hass, device_id, directory_path):
def __init__(self, hass, device_label, directory_path):
"""Initialize Verisure File Camera component."""
super().__init__()
self._device_id = device_id
self._device_label = device_label
self._directory_path = directory_path
self._image = None
self._image_id = None
@ -58,28 +59,27 @@ class VerisureSmartcam(Camera):
def check_imagelist(self):
"""Check the contents of the image list."""
hub.update_smartcam_imagelist()
if (self._device_id not in hub.smartcam_dict or
not hub.smartcam_dict[self._device_id]):
hub.update_smartcam_imageseries()
image_ids = hub.get_image_info(
"$.imageSeries[?(@.deviceLabel=='%s')].image[0].imageId",
self._device_label)
if not image_ids:
return
images = hub.smartcam_dict[self._device_id]
new_image_id = images[0]
_LOGGER.debug("self._device_id=%s, self._images=%s, "
"self._new_image_id=%s", self._device_id,
images, new_image_id)
new_image_id = image_ids[0]
if (new_image_id == '-1' or
self._image_id == new_image_id):
_LOGGER.debug("The image is the same, or loading image_id")
return
_LOGGER.debug("Download new image %s", new_image_id)
hub.my_pages.smartcam.download_image(
self._device_id, new_image_id, self._directory_path)
new_image_path = os.path.join(
self._directory_path, '{}{}'.format(new_image_id, '.jpg'))
hub.session.download_image(
self._device_label, new_image_id, new_image_path)
_LOGGER.debug("Old image_id=%s", self._image_id)
self.delete_image(self)
self._image_id = new_image_id
self._image = os.path.join(
self._directory_path, '{}{}'.format(self._image_id, '.jpg'))
self._image = new_image_path
def delete_image(self, event):
"""Delete an old image."""
@ -95,4 +95,6 @@ class VerisureSmartcam(Camera):
@property
def name(self):
"""Return the name of this camera."""
return hub.smartcam_status[self._device_id].location
return hub.get_first(
"$.customerImageCameras[?(@.deviceLabel=='%s')].area",
self._device_label)

View File

@ -693,8 +693,14 @@ class ClimateDevice(Entity):
def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes."""
if temp is None or not isinstance(temp, Number):
if temp is None:
return temp
# if the temperature is not a number this can cause issues
# with polymer components, so bail early there.
if not isinstance(temp, Number):
raise TypeError("Temperature is not a number: %s" % temp)
if self.temperature_unit != self.unit_of_measurement:
temp = convert_temperature(
temp, self.temperature_unit, self.unit_of_measurement)

View File

@ -67,7 +67,12 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
return self._values.get(self.gateway.const.SetReq.V_TEMP)
value = self._values.get(self.gateway.const.SetReq.V_TEMP)
if value is not None:
value = float(value)
return value
@property
def target_temperature(self):
@ -79,21 +84,21 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
if temp is None:
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
return temp
return float(temp)
@property
def target_temperature_high(self):
"""Return the highbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_HEAT in self._values:
return self._values.get(set_req.V_HVAC_SETPOINT_COOL)
return float(self._values.get(set_req.V_HVAC_SETPOINT_COOL))
@property
def target_temperature_low(self):
"""Return the lowbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_COOL in self._values:
return self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
return float(self._values.get(set_req.V_HVAC_SETPOINT_HEAT))
@property
def current_operation(self):

View File

@ -15,7 +15,7 @@ from homeassistant.components.climate import (
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['radiotherm==1.2']
REQUIREMENTS = ['radiotherm==1.3']
_LOGGER = logging.getLogger(__name__)
@ -84,6 +84,7 @@ class RadioThermostat(ClimateDevice):
self._name = None
self._fmode = None
self._tmode = None
self._tstate = None
self._hold_temp = hold_temp
self._away = False
self._away_temps = away_temps
@ -140,6 +141,7 @@ class RadioThermostat(ClimateDevice):
self._name = self.device.name['raw']
self._fmode = self.device.fmode['human']
self._tmode = self.device.tmode['human']
self._tstate = self.device.tstate['human']
if self._tmode == 'Cool':
self._target_temperature = self.device.t_cool['raw']
@ -147,6 +149,12 @@ class RadioThermostat(ClimateDevice):
elif self._tmode == 'Heat':
self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_HEAT
elif self._tmode == 'Auto':
if self._tstate == 'Cool':
self._target_temperature = self.device.t_cool['raw']
elif self._tstate == 'Heat':
self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_AUTO
else:
self._current_operation = STATE_IDLE
@ -159,6 +167,12 @@ class RadioThermostat(ClimateDevice):
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_HEAT:
self.device.t_heat = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_AUTO:
if self._tstate == 'Cool':
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._tstate == 'Heat':
self.device.t_heat = round(temperature * 2.0) / 2.0
if self._hold_temp or self._away:
self.device.hold = 1
else:

View File

@ -16,6 +16,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.temperature import convert as convert_temperature
@ -52,9 +53,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)):
if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]:
devices.append(SensiboClimate(client, dev))
except aiohttp.client_exceptions.ClientConnectorError:
except (aiohttp.client_exceptions.ClientConnectorError,
asyncio.TimeoutError):
_LOGGER.exception('Failed to connct to Sensibo servers.')
return False
raise PlatformNotReady
if devices:
async_add_devices(devices)

View File

@ -24,6 +24,17 @@ CONST_OVERLAY_MANUAL = 'MANUAL'
# the temperature will be reset after a timespan
CONST_OVERLAY_TIMER = 'TIMER'
CONST_MODE_FAN_HIGH = 'HIGH'
CONST_MODE_FAN_MIDDLE = 'MIDDLE'
CONST_MODE_FAN_LOW = 'LOW'
FAN_MODES_LIST = {
CONST_MODE_FAN_HIGH: 'High',
CONST_MODE_FAN_MIDDLE: 'Middle',
CONST_MODE_FAN_LOW: 'Low',
CONST_MODE_OFF: 'Off',
}
OPERATION_LIST = {
CONST_OVERLAY_MANUAL: 'Manual',
CONST_OVERLAY_TIMER: 'Timer',
@ -60,9 +71,15 @@ def create_climate_device(tado, hass, zone, name, zone_id):
capabilities = tado.get_capabilities(zone_id)
unit = TEMP_CELSIUS
min_temp = float(capabilities['temperatures']['celsius']['min'])
max_temp = float(capabilities['temperatures']['celsius']['max'])
ac_mode = capabilities['type'] != 'HEATING'
ac_mode = capabilities['type'] == 'AIR_CONDITIONING'
if ac_mode:
temperatures = capabilities['HEAT']['temperatures']
else:
temperatures = capabilities['temperatures']
min_temp = float(temperatures['celsius']['min'])
max_temp = float(temperatures['celsius']['max'])
data_id = 'zone {} {}'.format(name, zone_id)
device = TadoClimate(tado,
@ -107,7 +124,9 @@ class TadoClimate(ClimateDevice):
self._max_temp = max_temp
self._target_temp = None
self._tolerance = tolerance
self._cooling = False
self._current_fan = CONST_MODE_OFF
self._current_operation = CONST_MODE_SMART_SCHEDULE
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
@ -129,13 +148,32 @@ class TadoClimate(ClimateDevice):
@property
def current_operation(self):
"""Return current readable operation mode."""
return OPERATION_LIST.get(self._current_operation)
if self._cooling:
return "Cooling"
else:
return OPERATION_LIST.get(self._current_operation)
@property
def operation_list(self):
"""Return the list of available operation modes (readable)."""
return list(OPERATION_LIST.values())
@property
def current_fan_mode(self):
"""Return the fan setting."""
if self.ac_mode:
return FAN_MODES_LIST.get(self._current_fan)
else:
return None
@property
def fan_list(self):
"""List of available fan modes."""
if self.ac_mode:
return list(FAN_MODES_LIST.values())
else:
return None
@property
def temperature_unit(self):
"""Return the unit of measurement used by the platform."""
@ -205,27 +243,27 @@ class TadoClimate(ClimateDevice):
if 'sensorDataPoints' in data:
sensor_data = data['sensorDataPoints']
temperature = float(
sensor_data['insideTemperature']['celsius'])
humidity = float(
sensor_data['humidity']['percentage'])
setting = 0
unit = TEMP_CELSIUS
if 'insideTemperature' in sensor_data:
temperature = float(
sensor_data['insideTemperature']['celsius'])
self._cur_temp = self.hass.config.units.temperature(
temperature, unit)
if 'humidity' in sensor_data:
humidity = float(
sensor_data['humidity']['percentage'])
self._cur_humidity = humidity
# temperature setting will not exist when device is off
if 'temperature' in data['setting'] and \
data['setting']['temperature'] is not None:
setting = float(
data['setting']['temperature']['celsius'])
unit = TEMP_CELSIUS
self._cur_temp = self.hass.config.units.temperature(
temperature, unit)
self._target_temp = self.hass.config.units.temperature(
setting, unit)
self._cur_humidity = humidity
self._target_temp = self.hass.config.units.temperature(
setting, unit)
if 'tadoMode' in data:
mode = data['tadoMode']
@ -235,29 +273,39 @@ class TadoClimate(ClimateDevice):
power = data['setting']['power']
if power == 'OFF':
self._current_operation = CONST_MODE_OFF
self._current_fan = CONST_MODE_OFF
# There is no overlay, the mode will always be
# "SMART_SCHEDULE"
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._device_is_active = False
else:
self._device_is_active = True
if 'overlay' in data and data['overlay'] is not None:
overlay = True
termination = data['overlay']['termination']['type']
else:
if self._device_is_active:
overlay = False
termination = ""
overlay_data = None
termination = self._current_operation
cooling = False
fan_speed = CONST_MODE_OFF
# If you set mode manualy to off, there will be an overlay
# and a termination, but we want to see the mode "OFF"
if 'overlay' in data:
overlay_data = data['overlay']
overlay = overlay_data is not None
if overlay:
termination = overlay_data['termination']['type']
if 'setting' in overlay_data:
cooling = overlay_data['setting']['mode'] == 'COOL'
fan_speed = overlay_data['setting']['fanSpeed']
# If you set mode manualy to off, there will be an overlay
# and a termination, but we want to see the mode "OFF"
if overlay and self._device_is_active:
# There is an overlay the device is on
self._overlay_mode = termination
self._current_operation = termination
else:
# There is no overlay, the mode will always be
# "SMART_SCHEDULE"
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._current_operation = CONST_MODE_SMART_SCHEDULE
self._cooling = cooling
self._current_fan = fan_speed
def _control_heating(self):
"""Send new target temperature to mytado."""

View File

@ -0,0 +1,134 @@
"""
Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/comfoconnect/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_TOKEN, CONF_PIN, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import (discovery)
from homeassistant.helpers.dispatcher import (dispatcher_send)
REQUIREMENTS = ['pycomfoconnect==0.3']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'comfoconnect'
SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = 'comfoconnect_update_received'
ATTR_CURRENT_TEMPERATURE = 'current_temperature'
ATTR_CURRENT_HUMIDITY = 'current_humidity'
ATTR_OUTSIDE_TEMPERATURE = 'outside_temperature'
ATTR_OUTSIDE_HUMIDITY = 'outside_humidity'
ATTR_AIR_FLOW_SUPPLY = 'air_flow_supply'
ATTR_AIR_FLOW_EXHAUST = 'air_flow_exhaust'
CONF_USER_AGENT = 'user_agent'
DEFAULT_NAME = 'ComfoAirQ'
DEFAULT_PIN = 0
DEFAULT_TOKEN = '00000000000000000000000000000001'
DEFAULT_USER_AGENT = 'Home Assistant'
DEVICE = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TOKEN, default=DEFAULT_TOKEN):
vol.Length(min=32, max=32, msg='invalid token'),
vol.Optional(CONF_USER_AGENT, default=DEFAULT_USER_AGENT): cv.string,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the ComfoConnect bridge."""
from pycomfoconnect import (Bridge)
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
name = conf.get(CONF_NAME)
token = conf.get(CONF_TOKEN)
user_agent = conf.get(CONF_USER_AGENT)
pin = conf.get(CONF_PIN)
# Run discovery on the configured ip
bridges = Bridge.discover(host)
if not bridges:
_LOGGER.error("Could not connect to ComfoConnect bridge on %s", host)
return False
bridge = bridges[0]
_LOGGER.info("Bridge found: %s (%s)", bridge.uuid.hex(), bridge.host)
# Setup ComfoConnect Bridge
ccb = ComfoConnectBridge(hass, bridge, name, token, user_agent, pin)
hass.data[DOMAIN] = ccb
# Start connection with bridge
ccb.connect()
# Schedule disconnect on shutdown
def _shutdown(_event):
ccb.disconnect()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
# Load platforms
discovery.load_platform(hass, 'fan', DOMAIN, {}, config)
return True
class ComfoConnectBridge(object):
"""Representation of a ComfoConnect bridge."""
def __init__(self, hass, bridge, name, token, friendly_name, pin):
"""Initialize the ComfoConnect bridge."""
from pycomfoconnect import (ComfoConnect)
self.data = {}
self.name = name
self.hass = hass
self.comfoconnect = ComfoConnect(
bridge=bridge, local_uuid=bytes.fromhex(token),
local_devicename=friendly_name, pin=pin)
self.comfoconnect.callback_sensor = self.sensor_callback
def connect(self):
"""Connect with the bridge."""
_LOGGER.debug("Connecting with bridge")
self.comfoconnect.connect(True)
def disconnect(self):
"""Disconnect from the bridge."""
_LOGGER.debug("Disconnecting from bridge")
self.comfoconnect.disconnect()
def sensor_callback(self, var, value):
"""Callback function for sensor updates."""
_LOGGER.debug("Got value from bridge: %d = %d", var, value)
from pycomfoconnect import (
SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR)
if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]:
self.data[var] = value / 10
else:
self.data[var] = value
# Notify listeners that we have received an update
dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var)
def subscribe_sensor(self, sensor_id):
"""Subscribe for the specified sensor."""
self.comfoconnect.register_sensor(sensor_id)

View File

@ -40,6 +40,8 @@ DEVICE_CLASSES = [
'garage', # Garage door control
]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
SUPPORT_OPEN = 1
SUPPORT_CLOSE = 2
SUPPORT_SET_POSITION = 4

View File

@ -0,0 +1,185 @@
"""
Support for KNX covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.knx/
"""
import logging
import voluptuous as vol
from homeassistant.components.cover import (
CoverDevice, PLATFORM_SCHEMA, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, SUPPORT_STOP,
SUPPORT_SET_TILT_POSITION
)
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
from homeassistant.const import (CONF_NAME, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_GETPOSITION_ADDRESS = 'getposition_address'
CONF_SETPOSITION_ADDRESS = 'setposition_address'
CONF_GETANGLE_ADDRESS = 'getangle_address'
CONF_SETANGLE_ADDRESS = 'setangle_address'
CONF_STOP = 'stop_address'
CONF_UPDOWN = 'updown_address'
CONF_INVERT_POSITION = 'invert_position'
CONF_INVERT_ANGLE = 'invert_angle'
DEFAULT_NAME = 'KNX Cover'
DEPENDENCIES = ['knx']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_UPDOWN): cv.string,
vol.Required(CONF_STOP): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_GETPOSITION_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SETPOSITION_ADDRESS): cv.string,
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
vol.Inclusive(CONF_GETANGLE_ADDRESS, 'angle'): cv.string,
vol.Inclusive(CONF_SETANGLE_ADDRESS, 'angle'): cv.string,
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Create and add an entity based on the configuration."""
add_devices([KNXCover(hass, KNXConfig(config))])
class KNXCover(KNXMultiAddressDevice, CoverDevice):
"""Representation of a KNX cover. e.g. a rollershutter."""
def __init__(self, hass, config):
"""Initialize the cover."""
KNXMultiAddressDevice.__init__(
self, hass, config,
['updown', 'stop'], # required
optional=['setposition', 'getposition',
'getangle', 'setangle']
)
self._device_class = config.config.get(CONF_DEVICE_CLASS)
self._invert_position = config.config.get(CONF_INVERT_POSITION)
self._invert_angle = config.config.get(CONF_INVERT_ANGLE)
self._hass = hass
self._current_pos = None
self._target_pos = None
self._current_tilt = None
self._target_tilt = None
self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
SUPPORT_SET_POSITION | SUPPORT_STOP
# Tilt is only supported, if there is a angle get and set address
if CONF_SETANGLE_ADDRESS in config.config:
_LOGGER.debug("%s: Tilt supported at addresses %s, %s",
self.name, config.config.get(CONF_SETANGLE_ADDRESS),
config.config.get(CONF_GETANGLE_ADDRESS))
self._supported_features = self._supported_features | \
SUPPORT_SET_TILT_POSITION
@property
def should_poll(self):
"""Polling is needed for the KNX cover."""
return True
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
if self.current_cover_position > 0:
return False
else:
return True
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._current_pos
@property
def target_position(self):
"""Return the position we are trying to reach: 0 - 100."""
return self._target_pos
@property
def current_cover_tilt_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._current_tilt
@property
def target_tilt(self):
"""Return the tilt angle (in %) we are trying to reach: 0 - 100."""
return self._target_tilt
def set_cover_position(self, **kwargs):
"""Set new target position."""
position = kwargs.get(ATTR_POSITION)
if position is None:
return
if self._invert_position:
position = 100-position
self._target_pos = position
self.set_percentage('setposition', position)
_LOGGER.debug("%s: Set target position to %d", self.name, position)
def update(self):
"""Update device state."""
super().update()
value = self.get_percentage('getposition')
if value is not None:
self._current_pos = value
if self._invert_position:
self._current_pos = 100-value
_LOGGER.debug("%s: position = %d", self.name, value)
if self._supported_features & SUPPORT_SET_TILT_POSITION:
value = self.get_percentage('getangle')
if value is not None:
self._current_tilt = value
if self._invert_angle:
self._current_tilt = 100-value
_LOGGER.debug("%s: tilt = %d", self.name, value)
def open_cover(self, **kwargs):
"""Open the cover."""
_LOGGER.debug("%s: open: updown = 0", self.name)
self.set_int_value('updown', 0)
def close_cover(self, **kwargs):
"""Close the cover."""
_LOGGER.debug("%s: open: updown = 1", self.name)
self.set_int_value('updown', 1)
def stop_cover(self, **kwargs):
"""Stop the cover movement."""
_LOGGER.debug("%s: stop: stop = 1", self.name)
self.set_int_value('stop', 1)
def set_cover_tilt_position(self, tilt_position, **kwargs):
"""Move the cover til to a specific position."""
if self._invert_angle:
tilt_position = 100-tilt_position
self._target_tilt = round(tilt_position, -1)
self.set_percentage('setangle', tilt_position)
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return self._device_class

View File

@ -0,0 +1,354 @@
"""
Support for covers which integrate with other components.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.template/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.cover import (
ENTITY_ID_FORMAT, CoverDevice, PLATFORM_SCHEMA,
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT,
SUPPORT_SET_TILT_POSITION, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
SUPPORT_SET_POSITION, ATTR_POSITION, ATTR_TILT_POSITION)
from homeassistant.const import (
CONF_FRIENDLY_NAME, CONF_ENTITY_ID,
EVENT_HOMEASSISTANT_START, MATCH_ALL,
CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE,
STATE_OPEN, STATE_CLOSED)
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.script import Script
_LOGGER = logging.getLogger(__name__)
_VALID_STATES = [STATE_OPEN, STATE_CLOSED, 'true', 'false']
CONF_COVERS = 'covers'
CONF_POSITION_TEMPLATE = 'position_template'
CONF_TILT_TEMPLATE = 'tilt_template'
OPEN_ACTION = 'open_cover'
CLOSE_ACTION = 'close_cover'
STOP_ACTION = 'stop_cover'
POSITION_ACTION = 'set_cover_position'
TILT_ACTION = 'set_cover_tilt_position'
CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position'
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
SUPPORT_SET_TILT_POSITION)
COVER_SCHEMA = vol.Schema({
vol.Required(OPEN_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CLOSE_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(STOP_ACTION): cv.SCRIPT_SCHEMA,
vol.Exclusive(CONF_POSITION_TEMPLATE,
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
vol.Exclusive(CONF_VALUE_TEMPLATE,
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_TILT_TEMPLATE): cv.template,
vol.Optional(CONF_ICON_TEMPLATE): cv.template,
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Template cover."""
covers = []
for device, device_config in config[CONF_COVERS].items():
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
state_template = device_config.get(CONF_VALUE_TEMPLATE)
position_template = device_config.get(CONF_POSITION_TEMPLATE)
tilt_template = device_config.get(CONF_TILT_TEMPLATE)
icon_template = device_config.get(CONF_ICON_TEMPLATE)
open_action = device_config[OPEN_ACTION]
close_action = device_config[CLOSE_ACTION]
stop_action = device_config[STOP_ACTION]
position_action = device_config.get(POSITION_ACTION)
tilt_action = device_config.get(TILT_ACTION)
if position_template is None and state_template is None:
_LOGGER.error('Must specify either %s' or '%s',
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE)
continue
template_entity_ids = set()
if state_template is not None:
temp_ids = state_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if position_template is not None:
temp_ids = position_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if tilt_template is not None:
temp_ids = tilt_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if icon_template is not None:
temp_ids = icon_template.extract_entities()
if str(temp_ids) != MATCH_ALL:
template_entity_ids |= set(temp_ids)
if not template_entity_ids:
template_entity_ids = MATCH_ALL
entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids)
covers.append(
CoverTemplate(
hass,
device, friendly_name, state_template,
position_template, tilt_template, icon_template,
open_action, close_action, stop_action,
position_action, tilt_action, entity_ids
)
)
if not covers:
_LOGGER.error("No covers added")
return False
async_add_devices(covers, True)
return True
class CoverTemplate(CoverDevice):
"""Representation of a Template cover."""
def __init__(self, hass, device_id, friendly_name, state_template,
position_template, tilt_template, icon_template,
open_action, close_action, stop_action,
position_action, tilt_action, entity_ids):
"""Initialize the Template cover."""
self.hass = hass
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, device_id, hass=hass)
self._name = friendly_name
self._template = state_template
self._position_template = position_template
self._tilt_template = tilt_template
self._icon_template = icon_template
self._open_script = Script(hass, open_action)
self._close_script = Script(hass, close_action)
self._stop_script = Script(hass, stop_action)
self._position_script = None
if position_action is not None:
self._position_script = Script(hass, position_action)
self._tilt_script = None
if tilt_action is not None:
self._tilt_script = Script(hass, tilt_action)
self._icon = None
self._position = None
self._tilt_value = None
self._entities = entity_ids
if self._template is not None:
self._template.hass = self.hass
if self._position_template is not None:
self._position_template.hass = self.hass
if self._tilt_template is not None:
self._tilt_template.hass = self.hass
if self._icon_template is not None:
self._icon_template.hass = self.hass
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
self._position = 100 if state.state == STATE_OPEN else 0
@callback
def template_cover_state_listener(entity, old_state, new_state):
"""Handle target device state changes."""
self.hass.async_add_job(self.async_update_ha_state(True))
@callback
def template_cover_startup(event):
"""Update template on startup."""
async_track_state_change(
self.hass, self._entities, template_cover_state_listener)
self.hass.async_add_job(self.async_update_ha_state(True))
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, template_cover_startup)
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._position == 0
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._position
@property
def current_cover_tilt_position(self):
"""Return current position of cover tilt.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._tilt_value
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._icon
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
if self.current_cover_position is not None:
supported_features |= SUPPORT_SET_POSITION
if self.current_cover_tilt_position is not None:
supported_features |= TILT_FEATURES
return supported_features
@property
def should_poll(self):
"""Return the polling state."""
return False
@asyncio.coroutine
def async_open_cover(self, **kwargs):
"""Move the cover up."""
self.hass.async_add_job(self._open_script.async_run())
@asyncio.coroutine
def async_close_cover(self, **kwargs):
"""Move the cover down."""
self.hass.async_add_job(self._close_script.async_run())
@asyncio.coroutine
def async_stop_cover(self, **kwargs):
"""Fire the stop action."""
self.hass.async_add_job(self._stop_script.async_run())
@asyncio.coroutine
def async_set_cover_position(self, **kwargs):
"""Set cover position."""
if ATTR_POSITION not in kwargs:
return
self._position = kwargs[ATTR_POSITION]
self.hass.async_add_job(self._position_script.async_run(
{"position": self._position}))
@asyncio.coroutine
def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open."""
self._tilt_value = 100
self.hass.async_add_job(self._tilt_script.async_run(
{"tilt": self._tilt_value}))
@asyncio.coroutine
def async_close_cover_tilt(self, **kwargs):
"""Tilt the cover closed."""
self._tilt_value = 0
self.hass.async_add_job(self._tilt_script.async_run(
{"tilt": self._tilt_value}))
@asyncio.coroutine
def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
if ATTR_TILT_POSITION not in kwargs:
return
self._tilt_value = kwargs[ATTR_TILT_POSITION]
self.hass.async_add_job(self._tilt_script.async_run(
{"tilt": self._tilt_value}))
@asyncio.coroutine
def async_update(self):
"""Update the state from the template."""
if self._template is not None:
try:
state = self._template.async_render().lower()
if state in _VALID_STATES:
if state in ('true', STATE_OPEN):
self._position = 100
else:
self._position = 0
else:
_LOGGER.error(
'Received invalid cover is_on state: %s. Expected: %s',
state, ', '.join(_VALID_STATES))
self._position = None
except TemplateError as ex:
_LOGGER.error(ex)
self._position = None
if self._position_template is not None:
try:
state = float(self._position_template.async_render())
if state < 0 or state > 100:
self._position = None
_LOGGER.error("Cover position value must be"
" between 0 and 100."
" Value was: %.2f", state)
else:
self._position = state
except TemplateError as ex:
_LOGGER.error(ex)
self._position = None
except ValueError as ex:
_LOGGER.error(ex)
self._position = None
if self._tilt_template is not None:
try:
state = float(self._tilt_template.async_render())
if state < 0 or state > 100:
self._tilt_value = None
_LOGGER.error("Tilt value must be between 0 and 100."
" Value was: %.2f", state)
else:
self._tilt_value = state
except TemplateError as ex:
_LOGGER.error(ex)
self._tilt_value = None
except ValueError as ex:
_LOGGER.error(ex)
self._tilt_value = None
if self._icon_template is not None:
try:
self._icon = self._icon_template.async_render()
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
# Common during HA startup - so just a warning
_LOGGER.warning('Could not render icon template %s,'
' the state is unknown.', self._name)
return
self._icon = super().icon
_LOGGER.error('Could not render icon template %s: %s',
self._name, ex)

View File

@ -210,6 +210,7 @@ def async_setup(hass, config):
description=("Press the button on the bridge to register Philips "
"Hue with Home Assistant."),
description_image="/static/images/config_philips_hue.jpg",
fields=[{'id': 'username', 'name': 'Username'}],
submit_caption="I have pressed the button"
)
configurator_ids.append(request_id)

View File

@ -0,0 +1,110 @@
"""Support for Linksys Smart Wifi routers."""
import logging
import threading
from datetime import timedelta
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
DEFAULT_TIMEOUT = 10
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
})
def get_scanner(hass, config):
"""Validate the configuration and return a Linksys AP scanner."""
try:
return LinksysSmartWifiDeviceScanner(config[DOMAIN])
except ConnectionError:
return None
class LinksysSmartWifiDeviceScanner(DeviceScanner):
"""This class queries a Linksys Access Point."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.lock = threading.Lock()
self.last_results = {}
# Check if the access point is accessible
response = self._make_request()
if not response.status_code == 200:
raise ConnectionError("Cannot connect to Linksys Access Point")
def scan_devices(self):
"""Scan for new devices and return a list with device IDs (MACs)."""
self._update_info()
return self.last_results.keys()
def get_device_name(self, mac):
"""Return the name (if known) of the device."""
return self.last_results.get(mac)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Check for connected devices."""
with self.lock:
_LOGGER.info("Checking Linksys Smart Wifi")
self.last_results = {}
response = self._make_request()
if response.status_code != 200:
_LOGGER.error(
"Got HTTP status code %d when getting device list",
response.status_code)
return False
try:
data = response.json()
result = data["responses"][0]
devices = result["output"]["devices"]
for device in devices:
macs = device["knownMACAddresses"]
if not macs:
_LOGGER.warning(
"Skipping device without known MAC address")
continue
mac = macs[-1]
connections = device["connections"]
if not connections:
_LOGGER.debug("Device %s is not connected", mac)
continue
name = device["friendlyName"]
properties = device["properties"]
for prop in properties:
if prop["name"] == "userDeviceName":
name = prop["value"]
_LOGGER.debug("Device %s is connected", mac)
self.last_results[mac] = name
except (KeyError, IndexError):
_LOGGER.exception("Router returned unexpected response")
return False
return True
def _make_request(self):
# Weirdly enough, this doesn't seem to require authentication
data = [{
"request": {
"sinceRevision": 0
},
"action": "http://linksys.com/jnap/devicelist/GetDevices"
}]
headers = {"X-JNAP-Action": "http://linksys.com/jnap/core/Transaction"}
return requests.post('http://{}/JNAP/'.format(self.host),
timeout=DEFAULT_TIMEOUT,
headers=headers,
json=data)

View File

@ -158,6 +158,11 @@ class MikrotikScanner(DeviceScanner):
for device in devices
}
else:
self.last_results = mac_names
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
for device in device_names
if device.get('active-address')
}
return True

View File

@ -21,7 +21,7 @@ from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
DEPENDENCIES = ['mqtt']
REQUIREMENTS = ['libnacl==1.5.0']
REQUIREMENTS = ['libnacl==1.5.1']
_LOGGER = logging.getLogger(__name__)

32
homeassistant/components/device_tracker/ubus.py Executable file → Normal file
View File

@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
from homeassistant.exceptions import HomeAssistantError
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
@ -38,6 +39,23 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
def _refresh_on_acccess_denied(func):
"""If remove rebooted, it lost our session so rebuld one and try again."""
def decorator(self, *args, **kwargs):
"""Wrapper function to refresh session_id on PermissionError."""
try:
return func(self, *args, **kwargs)
except PermissionError:
_LOGGER.warning("Invalid session detected." +
" Tryign to refresh session_id and re-run the rpc")
self.session_id = _get_session_id(self.url, self.username,
self.password)
return func(self, *args, **kwargs)
return decorator
class UbusDeviceScanner(DeviceScanner):
"""
This class queries a wireless router running OpenWrt firmware.
@ -48,14 +66,16 @@ class UbusDeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialize the scanner."""
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.lock = threading.Lock()
self.last_results = {}
self.url = 'http://{}/ubus'.format(host)
self.session_id = _get_session_id(self.url, username, password)
self.session_id = _get_session_id(self.url, self.username,
self.password)
self.hostapd = []
self.leasefile = None
self.mac2name = None
@ -66,6 +86,7 @@ class UbusDeviceScanner(DeviceScanner):
self._update_info()
return self.last_results
@_refresh_on_acccess_denied
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
@ -95,6 +116,7 @@ class UbusDeviceScanner(DeviceScanner):
return self.mac2name.get(device.upper(), None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
@_refresh_on_acccess_denied
def _update_info(self):
"""Ensure the information from the Luci router is up to date.
@ -142,6 +164,12 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
if res.status_code == 200:
response = res.json()
if 'error' in response:
if 'message' in response['error'] and \
response['error']['message'] == "Access denied":
raise PermissionError(response['error']['message'])
else:
raise HomeAssistantError(response['error']['message'])
if rpcmethod == "call":
try:

View File

@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-digitalocean==1.11']
REQUIREMENTS = ['python-digitalocean==1.12']
_LOGGER = logging.getLogger(__name__)
@ -29,7 +29,7 @@ ATTR_VCPUS = 'vcpus'
CONF_DROPLETS = 'droplets'
DIGITAL_OCEAN = None
DATA_DIGITAL_OCEAN = 'data_do'
DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor']
DOMAIN = 'digital_ocean'
@ -47,13 +47,14 @@ def setup(hass, config):
conf = config[DOMAIN]
access_token = conf.get(CONF_ACCESS_TOKEN)
global DIGITAL_OCEAN
DIGITAL_OCEAN = DigitalOcean(access_token)
digital = DigitalOcean(access_token)
if not DIGITAL_OCEAN.manager.get_account():
if not digital.manager.get_account():
_LOGGER.error("No Digital Ocean account found for the given API Token")
return False
hass.data[DATA_DIGITAL_OCEAN] = digital
return True

View File

@ -55,6 +55,7 @@ SERVICE_HANDLERS = {
'apple_tv': ('media_player', 'apple_tv'),
'frontier_silicon': ('media_player', 'frontier_silicon'),
'openhome': ('media_player', 'openhome'),
'harmony': ('remote', 'harmony'),
'bose_soundtouch': ('media_player', 'soundtouch'),
}

View File

@ -0,0 +1,118 @@
"""
Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.comfoconnect/
"""
import logging
from homeassistant.components.comfoconnect import (
DOMAIN, ComfoConnectBridge, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED)
from homeassistant.components.fan import (
FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.dispatcher import (dispatcher_connect)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['comfoconnect']
SPEED_MAPPING = {
0: SPEED_OFF,
1: SPEED_LOW,
2: SPEED_MEDIUM,
3: SPEED_HIGH
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the ComfoConnect fan platform."""
ccb = hass.data[DOMAIN]
add_devices([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True)
return
class ComfoConnectFan(FanEntity):
"""Representation of the ComfoConnect fan platform."""
def __init__(self, hass, name, ccb: ComfoConnectBridge):
"""Initialize the ComfoConnect fan."""
from pycomfoconnect import SENSOR_FAN_SPEED_MODE
self._ccb = ccb
self._name = name
# Ask the bridge to keep us updated
self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE)
def _handle_update(var):
if var == SENSOR_FAN_SPEED_MODE:
_LOGGER.debug("Dispatcher update for %s", var)
self.schedule_update_ha_state()
# Register for dispatcher updates
dispatcher_connect(
hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update)
@property
def name(self):
"""Return the name of the fan."""
return self._name
@property
def icon(self):
"""Return the icon to use in the frontend."""
return 'mdi:air-conditioner'
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_SET_SPEED
@property
def speed(self):
"""Return the current fan mode."""
from pycomfoconnect import (SENSOR_FAN_SPEED_MODE)
try:
speed = self._ccb.data[SENSOR_FAN_SPEED_MODE]
return SPEED_MAPPING[speed]
except KeyError:
return STATE_UNKNOWN
@property
def speed_list(self):
"""List of available fan modes."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
def turn_on(self, speed: str=None, **kwargs) -> None:
"""Turn on the fan."""
if speed is None:
speed = SPEED_LOW
self.set_speed(speed)
def turn_off(self) -> None:
"""Turn off the fan (to away)."""
self.set_speed(SPEED_OFF)
def set_speed(self, mode):
"""Set fan speed."""
_LOGGER.debug('Changing fan mode to %s.', mode)
from pycomfoconnect import (
CMD_FAN_MODE_AWAY, CMD_FAN_MODE_LOW, CMD_FAN_MODE_MEDIUM,
CMD_FAN_MODE_HIGH)
if mode == SPEED_OFF:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY)
elif mode == SPEED_LOW:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW)
elif mode == SPEED_MEDIUM:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM)
elif mode == SPEED_HIGH:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH)
# Update current mode
self.schedule_update_ha_state()

View File

@ -0,0 +1,195 @@
"""
Support for Insteon fans via local hub control.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/fan.insteon_local/
"""
import json
import logging
import os
from datetime import timedelta
from homeassistant.components.fan import (
ATTR_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED, FanEntity)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
import homeassistant.util as util
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['insteon_local']
DOMAIN = 'fan'
INSTEON_LOCAL_FANS_CONF = 'insteon_local_fans.conf'
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
SUPPORT_INSTEON_LOCAL = SUPPORT_SET_SPEED
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon local fan platform."""
insteonhub = hass.data['insteon_local']
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if len(conf_fans):
for device_id in conf_fans:
setup_fan(device_id, conf_fans[device_id], insteonhub, hass,
add_devices)
else:
linked = insteonhub.get_linked()
for device_id in linked:
if (linked[device_id]['cat_type'] == 'dimmer' and
linked[device_id]['sku'] == '2475F' and
device_id not in conf_fans):
request_configuration(device_id,
insteonhub,
linked[device_id]['model_name'] + ' ' +
linked[device_id]['sku'],
hass, add_devices)
def request_configuration(device_id, insteonhub, model, hass,
add_devices_callback):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
# We got an error if this method is called while we are configuring
if device_id in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[device_id], 'Failed to register, please try again.')
return
def insteon_fan_config_callback(data):
"""The actions to do when our configuration callback is called."""
setup_fan(device_id, data.get('name'), insteonhub, hass,
add_devices_callback)
_CONFIGURING[device_id] = configurator.request_config(
hass, 'Insteon ' + model + ' addr: ' + device_id,
insteon_fan_config_callback,
description=('Enter a name for ' + model + ' Fan addr: ' + device_id),
entity_picture='/static/images/config_insteon.png',
submit_caption='Confirm',
fields=[{'id': 'name', 'name': 'Name', 'type': ''}]
)
def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
"""Set up the fan."""
if device_id in _CONFIGURING:
request_id = _CONFIGURING.pop(device_id)
configurator = get_component('configurator')
configurator.request_done(request_id)
_LOGGER.info("Device configuration done!")
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if device_id not in conf_fans:
conf_fans[device_id] = name
if not config_from_file(
hass.config.path(INSTEON_LOCAL_FANS_CONF),
conf_fans):
_LOGGER.error("Failed to save configuration file")
device = insteonhub.fan(device_id)
add_devices_callback([InsteonLocalFanDevice(device, name)])
def config_from_file(filename, config=None):
"""Small configuration file management function."""
if config:
# We're writing configuration
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error('Saving config file failed: %s', error)
return False
return True
else:
# We're reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except IOError as error:
_LOGGER.error("Reading configuration file failed: %s", error)
# This won't work yet
return False
else:
return {}
class InsteonLocalFanDevice(FanEntity):
"""An abstract Class for an Insteon node."""
def __init__(self, node, name):
"""Initialize the device."""
self.node = node
self.node.deviceName = name
self._speed = SPEED_OFF
@property
def name(self):
"""Return the the name of the node."""
return self.node.deviceName
@property
def unique_id(self):
"""Return the ID of this Insteon node."""
return 'insteon_local_{}_fan'.format(self.node.device_id)
@property
def speed(self) -> str:
"""Return the current speed."""
return self._speed
@property
def speed_list(self: ToggleEntity) -> list:
"""Get the list of available speeds."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
"""Update state of the fan."""
resp = self.node.status()
if 'cmd2' in resp:
if resp['cmd2'] == '00':
self._speed = SPEED_OFF
elif resp['cmd2'] == '55':
self._speed = SPEED_LOW
elif resp['cmd2'] == 'AA':
self._speed = SPEED_MEDIUM
elif resp['cmd2'] == 'FF':
self._speed = SPEED_HIGH
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_INSTEON_LOCAL
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn device on."""
if speed is None:
if ATTR_SPEED in kwargs:
speed = kwargs[ATTR_SPEED]
else:
speed = SPEED_MEDIUM
self.set_speed(speed)
def turn_off(self: ToggleEntity, **kwargs) -> None:
"""Turn device off."""
self.node.off()
def set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan."""
if self.node.on(speed):
self._speed = speed

View File

@ -3,21 +3,21 @@
FINGERPRINTS = {
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
"frontend.html": "cca45decbed803e7f0ec0b4f6e18fe53",
"mdi.html": "1a5ad9654c1f0e57440e30afd92846a5",
"frontend.html": "f170a7221615ca2839cb8fd51a82f50a",
"mdi.html": "c92bd28c434865d6cabb34cd3c0a3e4c",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
"panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1",
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2",
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed",
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750",
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b",
"panels/ha-panel-automation.html": "4f98839bb082885657bbcd0ac04fc680",
"panels/ha-panel-config.html": "76853de505d173e82249bf605eb73505",
"panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61",
"panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc",
"panels/ha-panel-dev-service.html": "92c6be30b1af95791d5a6429df505852",
"panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869",
"panels/ha-panel-dev-template.html": "d33a55b937b50cdfe8b6fae81f70a139",
"panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229",
"panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1",
"panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
"panels/ha-panel-zwave.html": "92edac58dd52c297c761fd9acec7f436",
"panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d",
"panels/ha-panel-map.html": "0ba605729197c4724ecc7310b08f7050",
"panels/ha-panel-zwave.html": "2ea2223339d1d2faff478751c2927d11",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
}

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 81ab4ff8a8ef7cc4b96b60f63c16472b0427adc7
Subproject commit 1ad42592134c290119879e8f8505ef5736a3071e

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hass.config.core.version]]</p><p>Path to configuration.yaml: [[hass.config.core.config_dir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the Apache 2.0 license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a><a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},polymerVersion:{type:String,value:Polymer.version},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.callApi("GET","error_log").then(function(e){this.errorLog=e||"No errors have been reported."}.bind(this))}})</script></body></html>
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header slot="header" fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hass.config.core.version]]</p><p>Path to configuration.yaml: [[hass.config.core.config_dir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the Apache 2.0 license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a><a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},polymerVersion:{type:String,value:Polymer.version},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.callApi("GET","error_log").then(function(e){this.errorLog=e||"No errors have been reported."}.bind(this))}})</script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,55 +0,0 @@
"""
Support for Insteon Hub.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_hub/
"""
import logging
import voluptuous as vol
from homeassistant.const import (CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['insteon_hub==0.4.5']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'insteon_hub'
INSTEON = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Insteon Hub component.
This will automatically import associated lights.
"""
_LOGGER.warning("Component disabled at request from Insteon. "
"For more information: https://goo.gl/zLJaic")
return False
# pylint: disable=unreachable
import insteon
username = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
api_key = config[DOMAIN][CONF_API_KEY]
global INSTEON
INSTEON = insteon.Insteon(username, password, api_key)
if INSTEON is None:
_LOGGER.error("Could not connect to Insteon service")
return False
discovery.load_platform(hass, 'light', DOMAIN, {}, config)
return True

View File

@ -51,7 +51,7 @@ def setup(hass, config):
res = KNXTUNNEL.connect()
_LOGGER.debug("Res = %s", res)
if not res:
_LOGGER.exception("Could not connect to KNX/IP interface %s", host)
_LOGGER.error("Could not connect to KNX/IP interface %s", host)
return False
except KNXException as ex:
@ -127,7 +127,10 @@ class KNXGroupAddress(Entity):
self._config = config
self._state = False
self._data = None
_LOGGER.debug("Initalizing KNX group address %s", self.address)
_LOGGER.debug(
"Initalizing KNX group address for %s (%s)",
self.name, self.address
)
def handle_knx_message(addr, data):
"""Handle an incoming KNX frame.
@ -198,11 +201,15 @@ class KNXGroupAddress(Entity):
self._data = res
else:
_LOGGER.debug(
"Unable to read from KNX address: %s (None)", self.address)
"%s: unable to read from KNX address: %s (None)",
self.name, self.address
)
except KNXException:
_LOGGER.exception(
"Unable to read from KNX address: %s", self.address)
"%s: unable to read from KNX address: %s",
self.name, self.address
)
return False
@ -213,9 +220,6 @@ class KNXMultiAddressDevice(Entity):
to be controlled by multiple group addresses.
"""
names = {}
values = {}
def __init__(self, hass, config, required, optional=None):
"""Initialize the device.
@ -226,33 +230,69 @@ class KNXMultiAddressDevice(Entity):
"""
from knxip.core import parse_group_address, KNXException
self.names = {}
self.values = {}
self._config = config
self._state = False
self._data = None
_LOGGER.debug("Initalizing KNX multi address device")
_LOGGER.debug(
"%s: initalizing KNX multi address device",
self.name
)
settings = self._config.config
if config.address:
_LOGGER.debug(
"%s: base address: address=%s",
self.name, settings.get('address')
)
self.names[config.address] = 'base'
if config.state_address:
_LOGGER.debug(
"%s, state address: state_address=%s",
self.name, settings.get('state_address')
)
self.names[config.state_address] = 'state'
# parse required addresses
for name in required:
_LOGGER.info(name)
paramname = '{}{}'.format(name, '_address')
addr = self._config.config.get(paramname)
addr = settings.get(paramname)
if addr is None:
_LOGGER.exception(
"Required KNX group address %s missing", paramname)
_LOGGER.error(
"%s: Required KNX group address %s missing",
self.name, paramname
)
raise KNXException(
"Group address for %s missing in configuration", paramname)
"%s: Group address for {} missing in "
"configuration for {}".format(
self.name, paramname
)
)
_LOGGER.debug(
"%s: (required parameter) %s=%s",
self.name, paramname, addr
)
addr = parse_group_address(addr)
self.names[addr] = name
# parse optional addresses
for name in optional:
paramname = '{}{}'.format(name, '_address')
addr = self._config.config.get(paramname)
addr = settings.get(paramname)
_LOGGER.debug(
"%s: (optional parameter) %s=%s",
self.name, paramname, addr
)
if addr:
try:
addr = parse_group_address(addr)
except KNXException:
_LOGGER.exception("Cannot parse group address %s", addr)
_LOGGER.exception(
"%s: cannot parse group address %s",
self.name, addr
)
self.names[addr] = name
@property
@ -280,11 +320,53 @@ class KNXMultiAddressDevice(Entity):
This is mostly important for optional addresses.
"""
for attributename, dummy_attribute in self.names.items():
for attributename in self.names.values():
if attributename == name:
return True
return False
def set_percentage(self, name, percentage):
"""Set a percentage in knx for a given attribute.
DPT_Scaling / DPT 5.001 is a single byte scaled percentage
"""
percentage = abs(percentage) # only accept positive values
scaled_value = percentage * 255 / 100
value = min(255, scaled_value)
return self.set_int_value(name, value)
def get_percentage(self, name):
"""Get a percentage from knx for a given attribute.
DPT_Scaling / DPT 5.001 is a single byte scaled percentage
"""
value = self.get_int_value(name)
percentage = round(value * 100 / 255)
return percentage
def set_int_value(self, name, value, num_bytes=1):
"""Set an integer value for a given attribute."""
# KNX packets are big endian
value = round(value) # only accept integers
b_value = value.to_bytes(num_bytes, byteorder='big')
return self.set_value(name, list(b_value))
def get_int_value(self, name):
"""Get an integer value for a given attribute."""
# KNX packets are big endian
summed_value = 0
raw_value = self.value(name)
try:
# convert raw value in bytes
for val in raw_value:
summed_value *= 256
summed_value += val
except TypeError:
# pknx returns a non-iterable type for unsuccessful reads
pass
return summed_value
def value(self, name):
"""Return the value to a given named attribute."""
from knxip.core import KNXException
@ -295,13 +377,21 @@ class KNXMultiAddressDevice(Entity):
addr = attributeaddress
if addr is None:
_LOGGER.exception("Attribute %s undefined", name)
_LOGGER.error("%s: attribute '%s' undefined",
self.name, name)
_LOGGER.debug(
"%s: defined attributes: %s",
self.name, str(self.names)
)
return False
try:
res = KNXTUNNEL.group_read(addr, use_cache=self.cache)
except KNXException:
_LOGGER.exception("Unable to read from KNX address: %s", addr)
_LOGGER.exception(
"%s: unable to read from KNX address: %s",
self.name, addr
)
return False
return res
@ -316,13 +406,21 @@ class KNXMultiAddressDevice(Entity):
addr = attributeaddress
if addr is None:
_LOGGER.exception("Attribute %s undefined", name)
_LOGGER.error("%s: attribute '%s' undefined",
self.name, name)
_LOGGER.debug(
"%s: defined attributes: %s",
self.name, str(self.names)
)
return False
try:
KNXTUNNEL.group_write(addr, value)
except KNXException:
_LOGGER.exception("Unable to write to KNX address: %s", addr)
_LOGGER.exception(
"%s: unable to write to KNX address: %s",
self.name, addr
)
return False
return True

View File

@ -14,7 +14,7 @@ from homeassistant.components.light import (
PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['decora==0.4']
REQUIREMENTS = ['decora==0.6']
_LOGGER = logging.getLogger(__name__)
@ -59,7 +59,7 @@ class DecoraLight(Light):
self._switch = decora.decora(self._address, self._key)
self._switch.connect()
self._state = self._switch.get_on()
self._brightness = self._switch.get_brightness()
self._brightness = self._switch.get_brightness() * 2.55
self.is_valid = True
@property
@ -99,7 +99,7 @@ class DecoraLight(Light):
def set_state(self, brightness):
"""Set the state of this lamp to the provided brightness."""
self._switch.set_brightness(brightness)
self._switch.set_brightness(int(brightness / 2.55))
self._brightness = brightness
return True
@ -120,5 +120,5 @@ class DecoraLight(Light):
def update(self):
"""Synchronise internal state with the actual light state."""
self._brightness = self._switch.get_brightness()
self._brightness = self._switch.get_brightness() * 2.55
self._state = self._switch.get_on()

View File

@ -1,79 +0,0 @@
"""
Support for Insteon Hub lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/insteon_hub/
"""
from homeassistant.components.insteon_hub import INSTEON
from homeassistant.components.light import (ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS, Light)
DEPENDENCIES = ['insteon_hub']
SUPPORT_INSTEON_HUB = SUPPORT_BRIGHTNESS
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon Hub light platform."""
devs = []
for device in INSTEON.devices:
if device.DeviceCategory == "Switched Lighting Control":
devs.append(InsteonToggleDevice(device))
if device.DeviceCategory == "Dimmable Lighting Control":
devs.append(InsteonToggleDevice(device))
add_devices(devs)
class InsteonToggleDevice(Light):
"""An abstract Class for an Insteon node."""
def __init__(self, node):
"""Initialize the device."""
self.node = node
self._value = 0
@property
def name(self):
"""Return the the name of the node."""
return self.node.DeviceName
@property
def unique_id(self):
"""Return the ID of this insteon node."""
return self.node.DeviceID
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._value / 100 * 255
def update(self):
"""Update state of the sensor."""
resp = self.node.send_command('get_status', wait=True)
try:
self._value = resp['response']['level']
except KeyError:
pass
@property
def is_on(self):
"""Return the boolean response if the node is on."""
return self._value != 0
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_INSTEON_HUB
def turn_on(self, **kwargs):
"""Turn device on."""
if ATTR_BRIGHTNESS in kwargs:
self._value = kwargs[ATTR_BRIGHTNESS] / 255 * 100
self.node.send_command('on', self._value)
else:
self._value = 100
self.node.send_command('on')
def turn_off(self, **kwargs):
"""Turn device off."""
self.node.send_command('off')

View File

@ -56,7 +56,7 @@ class ISYLightDevice(isy.ISYDevice, Light):
def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 light device."""
if not self._node.off():
_LOGGER.debug("Unable to turn on light")
_LOGGER.debug("Unable to turn off light")
def turn_on(self, brightness=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 light device."""

View File

@ -11,18 +11,19 @@ import math
from os import path
from functools import partial
from datetime import timedelta
import async_timeout
import voluptuous as vol
from homeassistant.components.light import (
Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA,
ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT,
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_RGB_COLOR,
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_TRANSITION, ATTR_EFFECT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT,
preprocess_turn_on_alternatives)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant import util
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_point_in_utc_time
@ -30,34 +31,79 @@ from homeassistant.helpers.service import extract_entity_ids
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
from . import effects as lifx_effects
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['aiolifx==0.4.8']
REQUIREMENTS = ['aiolifx==0.5.0', 'aiolifx_effects==0.1.0']
UDP_BROADCAST_PORT = 56700
# Delay (in ms) expected for changes to take effect in the physical bulb
BULB_LATENCY = 500
CONF_SERVER = 'server'
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
ATTR_HSBK = 'hsbk'
ATTR_INFRARED = 'infrared'
ATTR_POWER = 'power'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
})
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
ATTR_INFRARED = 'infrared'
ATTR_POWER = 'power'
LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({
ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
ATTR_POWER: cv.boolean,
})
SERVICE_EFFECT_PULSE = 'lifx_effect_pulse'
SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop'
SERVICE_EFFECT_STOP = 'lifx_effect_stop'
ATTR_POWER_ON = 'power_on'
ATTR_MODE = 'mode'
ATTR_PERIOD = 'period'
ATTR_CYCLES = 'cycles'
ATTR_SPREAD = 'spread'
ATTR_CHANGE = 'change'
PULSE_MODE_BLINK = 'blink'
PULSE_MODE_BREATHE = 'breathe'
PULSE_MODE_PING = 'ping'
PULSE_MODE_STROBE = 'strobe'
PULSE_MODE_SOLID = 'solid'
PULSE_MODES = [PULSE_MODE_BLINK, PULSE_MODE_BREATHE, PULSE_MODE_PING,
PULSE_MODE_STROBE, PULSE_MODE_SOLID]
LIFX_EFFECT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_POWER_ON, default=True): cv.boolean,
})
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)),
ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)),
ATTR_MODE: vol.In(PULSE_MODES),
})
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)),
})
LIFX_EFFECT_STOP_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@ -71,27 +117,79 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
server_addr = config.get(CONF_SERVER)
lifx_manager = LIFXManager(hass, async_add_devices)
lifx_discovery = aiolifx.LifxDiscovery(hass.loop, lifx_manager)
coro = hass.loop.create_datagram_endpoint(
partial(aiolifx.LifxDiscovery, hass.loop, lifx_manager),
local_addr=(server_addr, UDP_BROADCAST_PORT))
lambda: lifx_discovery, local_addr=(server_addr, UDP_BROADCAST_PORT))
hass.async_add_job(coro)
lifx_effects.setup(hass, lifx_manager)
@callback
def cleanup(event):
"""Clean up resources."""
lifx_discovery.cleanup()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
return True
def find_hsbk(**kwargs):
"""Find the desired color from a number of possible inputs."""
hue, saturation, brightness, kelvin = [None]*4
preprocess_turn_on_alternatives(kwargs)
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR])
saturation = convert_8_to_16(saturation)
brightness = convert_8_to_16(brightness)
kelvin = 3500
if ATTR_XY_COLOR in kwargs:
hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
saturation = convert_8_to_16(saturation)
kelvin = 3500
if ATTR_COLOR_TEMP in kwargs:
kelvin = int(color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP]))
saturation = 0
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
hsbk = [hue, saturation, brightness, kelvin]
return None if hsbk == [None]*4 else hsbk
def merge_hsbk(base, change):
"""Copy change on top of base, except when None."""
if change is None:
return None
return list(map(lambda x, y: y if y is not None else x, base, change))
class LIFXManager(object):
"""Representation of all known LIFX entities."""
def __init__(self, hass, async_add_devices):
"""Initialize the light."""
import aiolifx_effects
self.entities = {}
self.hass = hass
self.async_add_devices = async_add_devices
self.effects_conductor = aiolifx_effects.Conductor(loop=hass.loop)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
self.register_set_state(descriptions)
self.register_effects(descriptions)
def register_set_state(self, descriptions):
"""Register the LIFX set_state service call."""
@asyncio.coroutine
def async_service_handle(service):
"""Apply a service."""
@ -99,22 +197,73 @@ class LIFXManager(object):
for light in self.service_to_entities(service):
if service.service == SERVICE_LIFX_SET_STATE:
task = light.async_set_state(**service.data)
tasks.append(hass.async_add_job(task))
tasks.append(self.hass.async_add_job(task))
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
yield from asyncio.wait(tasks, loop=self.hass.loop)
descriptions = self.get_descriptions()
hass.services.async_register(
self.hass.services.async_register(
DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle,
descriptions.get(SERVICE_LIFX_SET_STATE),
schema=LIFX_SET_STATE_SCHEMA)
@staticmethod
def get_descriptions():
"""Load and return descriptions for our own service calls."""
return load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
def register_effects(self, descriptions):
"""Register the LIFX effects as hass service calls."""
@asyncio.coroutine
def async_service_handle(service):
"""Apply a service, i.e. start an effect."""
entities = self.service_to_entities(service)
if entities:
yield from self.start_effect(
entities, service.service, **service.data)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle,
descriptions.get(SERVICE_EFFECT_PULSE),
schema=LIFX_EFFECT_PULSE_SCHEMA)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_COLORLOOP),
schema=LIFX_EFFECT_COLORLOOP_SCHEMA)
self.hass.services.async_register(
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_STOP),
schema=LIFX_EFFECT_STOP_SCHEMA)
@asyncio.coroutine
def start_effect(self, entities, service, **kwargs):
"""Start a light effect on entities."""
import aiolifx_effects
devices = list(map(lambda l: l.device, entities))
if service == SERVICE_EFFECT_PULSE:
effect = aiolifx_effects.EffectPulse(
power_on=kwargs.get(ATTR_POWER_ON),
period=kwargs.get(ATTR_PERIOD),
cycles=kwargs.get(ATTR_CYCLES),
mode=kwargs.get(ATTR_MODE),
hsbk=find_hsbk(**kwargs),
)
yield from self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_COLORLOOP:
preprocess_turn_on_alternatives(kwargs)
brightness = None
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
effect = aiolifx_effects.EffectColorloop(
power_on=kwargs.get(ATTR_POWER_ON),
period=kwargs.get(ATTR_PERIOD),
change=kwargs.get(ATTR_CHANGE),
spread=kwargs.get(ATTR_SPREAD),
transition=kwargs.get(ATTR_TRANSITION),
brightness=brightness,
)
yield from self.effects_conductor.start(effect, devices)
elif service == SERVICE_EFFECT_STOP:
yield from self.effects_conductor.stop(devices)
def service_to_entities(self, service):
"""Return the known devices that a service call mentions."""
@ -148,7 +297,7 @@ class LIFXManager(object):
@callback
def ready(self, device, msg):
"""Handle the device once all data is retrieved."""
entity = LIFXLight(device)
entity = LIFXLight(device, self.effects_conductor)
_LOGGER.debug("%s register READY", entity.who)
self.entities[device.mac_addr] = entity
self.async_add_devices([entity])
@ -182,17 +331,13 @@ class AwaitAioLIFX:
@asyncio.coroutine
def wait(self, method):
"""Call an aiolifx method and wait for its response or a timeout."""
"""Call an aiolifx method and wait for its response."""
self.device = None
self.message = None
self.event.clear()
method(self.callback)
while self.light.available and not self.event.is_set():
try:
with async_timeout.timeout(1.0, loop=self.light.hass.loop):
yield from self.event.wait()
except asyncio.TimeoutError:
pass
method(callb=self.callback)
yield from self.event.wait()
return self.message
@ -209,17 +354,13 @@ def convert_16_to_8(value):
class LIFXLight(Light):
"""Representation of a LIFX light."""
def __init__(self, device):
def __init__(self, device, effects_conductor):
"""Initialize the light."""
self.device = device
self.effects_conductor = effects_conductor
self.registered = True
self.product = device.product
self.blocker = None
self.effect_data = None
self.postponed_update = None
self._name = device.label
self.set_power(device.power_level)
self.set_color(*device.color)
@property
def lifxwhite(self):
@ -235,34 +376,33 @@ class LIFXLight(Light):
@property
def name(self):
"""Return the name of the device."""
return self._name
return self.device.label
@property
def who(self):
"""Return a string identifying the device."""
ip_addr = '-'
if self.device:
ip_addr = self.device.ip_addr[0]
return "%s (%s)" % (ip_addr, self.name)
return "%s (%s)" % (self.device.ip_addr, self.name)
@property
def rgb_color(self):
"""Return the RGB value."""
_LOGGER.debug(
"rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2])
return self._rgb
hue, sat, bri, _ = self.device.color
return color_util.color_hsv_to_RGB(
hue, convert_16_to_8(sat), convert_16_to_8(bri))
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
brightness = convert_16_to_8(self._bri)
brightness = convert_16_to_8(self.device.color[2])
_LOGGER.debug("brightness: %d", brightness)
return brightness
@property
def color_temp(self):
"""Return the color temperature."""
temperature = color_util.color_temperature_kelvin_to_mired(self._kel)
kelvin = self.device.color[3]
temperature = color_util.color_temperature_kelvin_to_mired(kelvin)
_LOGGER.debug("color_temp: %d", temperature)
return temperature
@ -290,13 +430,15 @@ class LIFXLight(Light):
@property
def is_on(self):
"""Return true if device is on."""
_LOGGER.debug("is_on: %d", self._power)
return self._power != 0
return self.device.power_level != 0
@property
def effect(self):
"""Return the currently running effect."""
return self.effect_data.effect.name if self.effect_data else None
"""Return the name of the currently running effect."""
effect = self.effects_conductor.effect(self.device)
if effect:
return 'lifx_effect_' + effect.name
return None
@property
def supported_features(self):
@ -311,38 +453,35 @@ class LIFXLight(Light):
@property
def effect_list(self):
"""Return the list of supported effects."""
return lifx_effects.effect_list(self)
"""Return the list of supported effects for this light."""
if self.lifxwhite:
return [
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
@asyncio.coroutine
def update_after_transition(self, now):
"""Request new status after completion of the last transition."""
self.postponed_update = None
yield from self.refresh_state()
yield from self.async_update_ha_state()
@asyncio.coroutine
def unblock_updates(self, now):
"""Allow async_update after the new state has settled on the bulb."""
self.blocker = None
yield from self.refresh_state()
yield from self.async_update()
yield from self.async_update_ha_state()
def update_later(self, when):
"""Block immediate update requests and schedule one for later."""
if self.blocker:
self.blocker()
self.blocker = async_track_point_in_utc_time(
self.hass, self.unblock_updates,
util.dt.utcnow() + timedelta(milliseconds=BULB_LATENCY))
"""Schedule an update requests when a transition is over."""
if self.postponed_update:
self.postponed_update()
self.postponed_update = None
if when > BULB_LATENCY:
if when > 0:
self.postponed_update = async_track_point_in_utc_time(
self.hass, self.update_after_transition,
util.dt.utcnow() + timedelta(milliseconds=when+BULB_LATENCY))
util.dt.utcnow() + timedelta(milliseconds=when))
@asyncio.coroutine
def async_turn_on(self, **kwargs):
@ -359,10 +498,10 @@ class LIFXLight(Light):
@asyncio.coroutine
def async_set_state(self, **kwargs):
"""Set a color on the light and turn it on/off."""
yield from self.stop_effect()
yield from self.effects_conductor.stop([self.device])
if ATTR_EFFECT in kwargs:
yield from lifx_effects.default_effect(self, **kwargs)
yield from self.default_effect(**kwargs)
return
if ATTR_INFRARED in kwargs:
@ -377,124 +516,44 @@ class LIFXLight(Light):
power_on = kwargs.get(ATTR_POWER, False)
power_off = not kwargs.get(ATTR_POWER, True)
hsbk, changed_color = self.find_hsbk(**kwargs)
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
self.who, self._power, fade, *hsbk)
hsbk = merge_hsbk(self.device.color, find_hsbk(**kwargs))
if self._power == 0:
# Send messages, waiting for ACK each time
ack = AwaitAioLIFX(self).wait
bulb = self.device
if not self.is_on:
if power_off:
self.device.set_power(False, None, 0)
if changed_color:
self.device.set_color(hsbk, None, 0)
yield from ack(partial(bulb.set_power, False))
if hsbk:
yield from ack(partial(bulb.set_color, hsbk))
if power_on:
self.device.set_power(True, None, fade)
yield from ack(partial(bulb.set_power, True, duration=fade))
else:
if power_on:
self.device.set_power(True, None, 0)
if changed_color:
self.device.set_color(hsbk, None, fade)
yield from ack(partial(bulb.set_power, True))
if hsbk:
yield from ack(partial(bulb.set_color, hsbk, duration=fade))
if power_off:
self.device.set_power(False, None, fade)
yield from ack(partial(bulb.set_power, False, duration=fade))
if power_on:
self.update_later(0)
else:
self.update_later(fade)
# Schedule an update when the transition is complete
self.update_later(fade)
if fade <= BULB_LATENCY:
if power_on:
self.set_power(1)
if power_off:
self.set_power(0)
if changed_color:
self.set_color(*hsbk)
@asyncio.coroutine
def default_effect(self, **kwargs):
"""Start an effect with default parameters."""
service = kwargs[ATTR_EFFECT]
data = {
ATTR_ENTITY_ID: self.entity_id,
}
yield from self.hass.services.async_call(DOMAIN, service, data)
@asyncio.coroutine
def async_update(self):
"""Update bulb status (if it is available)."""
"""Update bulb status."""
_LOGGER.debug("%s async_update", self.who)
if self.blocker is None:
yield from self.refresh_state()
@asyncio.coroutine
def stop_effect(self):
"""Stop the currently running effect (if any)."""
if self.effect_data:
yield from self.effect_data.effect.async_restore(self)
@asyncio.coroutine
def refresh_state(self):
"""Ask the device about its current state and update our copy."""
if self.available:
msg = yield from AwaitAioLIFX(self).wait(self.device.get_color)
if msg is not None:
self.set_power(self.device.power_level)
self.set_color(*self.device.color)
self._name = self.device.label
def find_hsbk(self, **kwargs):
"""Find the desired color from a number of possible inputs."""
changed_color = False
hsbk = kwargs.pop(ATTR_HSBK, None)
if hsbk is not None:
return [hsbk, True]
preprocess_turn_on_alternatives(kwargs)
if ATTR_RGB_COLOR in kwargs:
hue, saturation, brightness = \
color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR])
saturation = convert_8_to_16(saturation)
brightness = convert_8_to_16(brightness)
changed_color = True
else:
hue = self._hue
saturation = self._sat
brightness = self._bri
if ATTR_XY_COLOR in kwargs:
hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
saturation = convert_8_to_16(saturation)
changed_color = True
# When color or temperature is set, use a default value for the other
if ATTR_COLOR_TEMP in kwargs:
kelvin = int(color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP]))
if not changed_color:
saturation = 0
changed_color = True
else:
if changed_color:
kelvin = 3500
else:
kelvin = self._kel
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
changed_color = True
else:
brightness = self._bri
return [[hue, saturation, brightness, kelvin], changed_color]
def set_power(self, power):
"""Set power state value."""
_LOGGER.debug("set_power: %d", power)
self._power = (power != 0)
def set_color(self, hue, sat, bri, kel):
"""Set color state values."""
self._hue = hue
self._sat = sat
self._bri = bri
self._kel = kel
red, green, blue = color_util.color_hsv_to_RGB(
hue, convert_16_to_8(sat), convert_16_to_8(bri))
_LOGGER.debug("set_color: %d %d %d %d [%d %d %d]",
hue, sat, bri, kel, red, green, blue)
self._rgb = [red, green, blue]
# Avoid state ping-pong by holding off updates as the state settles
yield from asyncio.sleep(0.25)
yield from AwaitAioLIFX(self).wait(self.device.get_color)

View File

@ -1,388 +0,0 @@
"""Support for light effects for the LIFX light platform."""
import logging
import asyncio
import random
import voluptuous as vol
from homeassistant.components.light import (
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
ATTR_RGB_COLOR, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_EFFECT, ATTR_TRANSITION,
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
from homeassistant.const import (ATTR_ENTITY_ID)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
SERVICE_EFFECT_BREATHE = 'lifx_effect_breathe'
SERVICE_EFFECT_PULSE = 'lifx_effect_pulse'
SERVICE_EFFECT_COLORLOOP = 'lifx_effect_colorloop'
SERVICE_EFFECT_STOP = 'lifx_effect_stop'
ATTR_POWER_ON = 'power_on'
ATTR_PERIOD = 'period'
ATTR_CYCLES = 'cycles'
ATTR_MODE = 'mode'
ATTR_SPREAD = 'spread'
ATTR_CHANGE = 'change'
MODE_BLINK = 'blink'
MODE_BREATHE = 'breathe'
MODE_PING = 'ping'
MODE_STROBE = 'strobe'
MODE_SOLID = 'solid'
MODES = [MODE_BLINK, MODE_BREATHE, MODE_PING, MODE_STROBE, MODE_SOLID]
# aiolifx waveform modes
WAVEFORM_SINE = 1
WAVEFORM_PULSE = 4
NEUTRAL_WHITE = 3500
LIFX_EFFECT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_POWER_ON, default=True): cv.boolean,
})
LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)),
ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)),
})
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA.extend({
vol.Optional(ATTR_MODE, default=MODE_BLINK): vol.In(MODES),
})
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
vol.Optional(ATTR_PERIOD, default=60):
vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
vol.Optional(ATTR_CHANGE, default=20):
vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
vol.Optional(ATTR_SPREAD, default=30):
vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)),
ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Range(min=0)),
})
LIFX_EFFECT_STOP_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_POWER_ON, default=False): cv.boolean,
})
def setup(hass, lifx_manager):
"""Register the LIFX effects as hass service calls."""
@asyncio.coroutine
def async_service_handle(service):
"""Apply a service."""
entities = lifx_manager.service_to_entities(service)
if entities:
yield from start_effect(hass, entities,
service.service, **service.data)
descriptions = lifx_manager.get_descriptions()
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_BREATHE, async_service_handle,
descriptions.get(SERVICE_EFFECT_BREATHE),
schema=LIFX_EFFECT_BREATHE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_PULSE, async_service_handle,
descriptions.get(SERVICE_EFFECT_PULSE),
schema=LIFX_EFFECT_PULSE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_COLORLOOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_COLORLOOP),
schema=LIFX_EFFECT_COLORLOOP_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_EFFECT_STOP, async_service_handle,
descriptions.get(SERVICE_EFFECT_STOP),
schema=LIFX_EFFECT_STOP_SCHEMA)
@asyncio.coroutine
def start_effect(hass, devices, service, **data):
"""Start a light effect."""
tasks = []
for light in devices:
tasks.append(hass.async_add_job(light.stop_effect()))
yield from asyncio.wait(tasks, loop=hass.loop)
if service in SERVICE_EFFECT_BREATHE:
effect = LIFXEffectBreathe(hass, devices)
elif service in SERVICE_EFFECT_PULSE:
effect = LIFXEffectPulse(hass, devices)
elif service == SERVICE_EFFECT_COLORLOOP:
effect = LIFXEffectColorloop(hass, devices)
elif service == SERVICE_EFFECT_STOP:
effect = LIFXEffectStop(hass, devices)
hass.async_add_job(effect.async_perform(**data))
@asyncio.coroutine
def default_effect(light, **kwargs):
"""Start an effect with default parameters."""
service = kwargs[ATTR_EFFECT]
data = {
ATTR_ENTITY_ID: light.entity_id,
}
yield from light.hass.services.async_call(DOMAIN, service, data)
def effect_list(light):
"""Return the list of supported effects for this light."""
if light.lifxwhite:
return [
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
else:
return [
SERVICE_EFFECT_COLORLOOP,
SERVICE_EFFECT_BREATHE,
SERVICE_EFFECT_PULSE,
SERVICE_EFFECT_STOP,
]
class LIFXEffectData(object):
"""Structure describing a running effect."""
def __init__(self, effect, power, color):
"""Initialize data structure."""
self.effect = effect
self.power = power
self.color = color
class LIFXEffect(object):
"""Representation of a light effect running on a number of lights."""
def __init__(self, hass, lights):
"""Initialize the effect."""
self.hass = hass
self.lights = lights
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Do common setup and play the effect."""
yield from self.async_setup(**kwargs)
yield from self.async_play(**kwargs)
@asyncio.coroutine
def async_setup(self, **kwargs):
"""Prepare all lights for the effect."""
for light in self.lights:
# Remember the current state (as far as we know it)
yield from light.refresh_state()
light.effect_data = LIFXEffectData(
self, light.is_on, light.device.color)
# Temporarily turn on power for the effect to be visible
if kwargs[ATTR_POWER_ON] and not light.is_on:
hsbk = self.from_poweroff_hsbk(light, **kwargs)
light.device.set_color(hsbk)
light.device.set_power(True)
# pylint: disable=no-self-use
@asyncio.coroutine
def async_play(self, **kwargs):
"""Play the effect."""
yield None
@asyncio.coroutine
def async_restore(self, light):
"""Restore to the original state (if we are still running)."""
if light in self.lights:
self.lights.remove(light)
if light.effect_data and light.effect_data.effect == self:
if not light.effect_data.power:
light.device.set_power(False)
yield from asyncio.sleep(0.5)
light.device.set_color(light.effect_data.color)
yield from asyncio.sleep(0.5)
light.effect_data = None
yield from light.refresh_state()
def from_poweroff_hsbk(self, light, **kwargs):
"""Return the color when starting from a powered off state."""
return [random.randint(0, 65535), 65535, 0, NEUTRAL_WHITE]
class LIFXEffectPulse(LIFXEffect):
"""Representation of a pulse effect."""
def __init__(self, hass, lights):
"""Initialize the pulse effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_PULSE
@asyncio.coroutine
def async_play(self, **kwargs):
"""Play the effect on all lights."""
for light in self.lights:
self.hass.async_add_job(self.async_light_play(light, **kwargs))
@asyncio.coroutine
def async_light_play(self, light, **kwargs):
"""Play a light effect on the bulb."""
hsbk, color_changed = light.find_hsbk(**kwargs)
if kwargs[ATTR_MODE] == MODE_STROBE:
# Strobe must flash from a dark color
light.device.set_color([0, 0, 0, NEUTRAL_WHITE])
yield from asyncio.sleep(0.1)
default_period = 0.1
default_cycles = 10
else:
default_period = 1.0
default_cycles = 1
period = kwargs.get(ATTR_PERIOD, default_period)
cycles = kwargs.get(ATTR_CYCLES, default_cycles)
# Breathe has a special waveform
if kwargs[ATTR_MODE] == MODE_BREATHE:
waveform = WAVEFORM_SINE
else:
waveform = WAVEFORM_PULSE
# Ping and solid have special duty cycles
if kwargs[ATTR_MODE] == MODE_PING:
ping_duration = int(5000 - min(2500, 300*period))
duty_cycle = 2**15 - ping_duration
elif kwargs[ATTR_MODE] == MODE_SOLID:
duty_cycle = -2**15
else:
duty_cycle = 0
# Set default effect color based on current setting
if not color_changed:
if kwargs[ATTR_MODE] == MODE_STROBE:
# Strobe: cold white
hsbk = [hsbk[0], 0, 65535, 5600]
elif light.lifxwhite or hsbk[1] < 65536/2:
# White: toggle brightness
hsbk[2] = 65535 if hsbk[2] < 65536/2 else 0
else:
# Color: fully desaturate with full brightness
hsbk = [hsbk[0], 0, 65535, 4000]
# Start the effect
args = {
'transient': 1,
'color': hsbk,
'period': int(period*1000),
'cycles': cycles,
'duty_cycle': duty_cycle,
'waveform': waveform,
}
light.device.set_waveform(args)
# Wait for completion and restore the initial state
yield from asyncio.sleep(period*cycles)
yield from self.async_restore(light)
def from_poweroff_hsbk(self, light, **kwargs):
"""Return the color is the target color, but no brightness."""
hsbk, _ = light.find_hsbk(**kwargs)
return [hsbk[0], hsbk[1], 0, hsbk[2]]
class LIFXEffectBreathe(LIFXEffectPulse):
"""Representation of a breathe effect."""
def __init__(self, hass, lights):
"""Initialize the breathe effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_BREATHE
_LOGGER.warning("'lifx_effect_breathe' is deprecated. Please use "
"'lifx_effect_pulse' with 'mode: breathe'")
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Prepare all lights for the effect."""
kwargs[ATTR_MODE] = MODE_BREATHE
yield from super().async_perform(**kwargs)
class LIFXEffectColorloop(LIFXEffect):
"""Representation of a colorloop effect."""
def __init__(self, hass, lights):
"""Initialize the colorloop effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_COLORLOOP
@asyncio.coroutine
def async_play(self, **kwargs):
"""Play the effect on all lights."""
period = kwargs[ATTR_PERIOD]
spread = kwargs[ATTR_SPREAD]
change = kwargs[ATTR_CHANGE]
direction = 1 if random.randint(0, 1) else -1
# Random start
hue = random.uniform(0, 360) % 360
while self.lights:
hue = (hue + direction*change) % 360
random.shuffle(self.lights)
lhue = hue
for light in self.lights:
if ATTR_TRANSITION in kwargs:
transition = int(1000*kwargs[ATTR_TRANSITION])
elif light == self.lights[0] or spread > 0:
transition = int(1000 * random.uniform(period/2, period))
if ATTR_BRIGHTNESS in kwargs:
brightness = int(65535/255*kwargs[ATTR_BRIGHTNESS])
else:
brightness = light.effect_data.color[2]
hsbk = [
int(65535/360*lhue),
int(random.uniform(0.8, 1.0)*65535),
brightness,
NEUTRAL_WHITE,
]
light.device.set_color(hsbk, None, transition)
# Adjust the next light so the full spread is used
if len(self.lights) > 1:
lhue = (lhue + spread/(len(self.lights)-1)) % 360
yield from asyncio.sleep(period)
class LIFXEffectStop(LIFXEffect):
"""A no-op effect, but starting it will stop an existing effect."""
def __init__(self, hass, lights):
"""Initialize the stop effect."""
super().__init__(hass, lights)
self.name = SERVICE_EFFECT_STOP
@asyncio.coroutine
def async_perform(self, **kwargs):
"""Do nothing."""
yield None

View File

@ -1,98 +0,0 @@
lifx_set_state:
description: Set a color/brightness and possibliy turn the light on/off
fields:
entity_id:
description: Name(s) of entities to set a state on
example: 'light.garage'
'...':
description: All turn_on parameters can be used to specify a color
infrared:
description: Automatic infrared level (0..255) when light brightness is low
example: 255
transition:
description: Duration in seconds it takes to get to the final state
example: 10
power:
description: Turn the light on (True) or off (False). Leave out to keep the power as it is.
example: True
lifx_effect_breathe:
description: Deprecated, use lifx_effect_pulse
lifx_effect_pulse:
description: Run a flash effect by changing to a color and back.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.kitchen'
mode:
description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid'
example: strobe
brightness:
description: Number between 0..255 indicating brightness of the temporary color
example: 120
color_name:
description: A human readable color name
example: 'red'
rgb_color:
description: The temporary color in RGB-format
example: '[255, 100, 100]'
period:
description: Duration of the effect in seconds (default 1.0)
example: 3
cycles:
description: Number of times the effect should run (default 1.0)
example: 2
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_colorloop:
description: Run an effect with looping colors.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.disco1, light.disco2, light.disco3'
brightness:
description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light
example: 120
period:
description: Duration (in seconds) between color changes (default 60)
example: 180
change:
description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20)
example: 45
spread:
description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30)
example: 0
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_stop:
description: Stop a running effect.
fields:
entity_id:
description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere.
example: 'light.bedroom'

View File

@ -24,11 +24,13 @@ CONF_BRIDGES = 'bridges'
CONF_GROUPS = 'groups'
CONF_NUMBER = 'number'
CONF_VERSION = 'version'
CONF_FADE = 'fade'
DEFAULT_LED_TYPE = 'rgbw'
DEFAULT_PORT = 5987
DEFAULT_TRANSITION = 0
DEFAULT_VERSION = 6
DEFAULT_FADE = False
LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led']
@ -58,6 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TYPE, default=DEFAULT_LED_TYPE):
vol.In(LED_TYPE),
vol.Required(CONF_NUMBER): cv.positive_int,
vol.Optional(CONF_FADE, default=DEFAULT_FADE): cv.boolean,
}
]),
},
@ -112,7 +115,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
group_conf.get(CONF_NUMBER),
group_conf.get(CONF_NAME),
group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE))
lights.append(LimitlessLEDGroup.factory(group))
lights.append(LimitlessLEDGroup.factory(group, {
'fade': group_conf[CONF_FADE]
}))
add_devices(lights)
@ -152,25 +157,26 @@ def state(new_state):
class LimitlessLEDGroup(Light):
"""Representation of a LimitessLED group."""
def __init__(self, group):
def __init__(self, group, config):
"""Initialize a group."""
self.group = group
self.repeating = False
self._is_on = False
self._brightness = None
self.config = config
@staticmethod
def factory(group):
def factory(group, config):
"""Produce LimitlessLEDGroup objects."""
from limitlessled.group.rgbw import RgbwGroup
from limitlessled.group.white import WhiteGroup
from limitlessled.group.rgbww import RgbwwGroup
if isinstance(group, WhiteGroup):
return LimitlessLEDWhiteGroup(group)
return LimitlessLEDWhiteGroup(group, config)
elif isinstance(group, RgbwGroup):
return LimitlessLEDRGBWGroup(group)
return LimitlessLEDRGBWGroup(group, config)
elif isinstance(group, RgbwwGroup):
return LimitlessLEDRGBWWGroup(group)
return LimitlessLEDRGBWWGroup(group, config)
@property
def should_poll(self):
@ -196,15 +202,17 @@ class LimitlessLEDGroup(Light):
def turn_off(self, transition_time, pipeline, **kwargs):
"""Turn off a group."""
if self.is_on:
pipeline.transition(transition_time, brightness=0.0).off()
if self.config[CONF_FADE]:
pipeline.transition(transition_time, brightness=0.0)
pipeline.off()
class LimitlessLEDWhiteGroup(LimitlessLEDGroup):
"""Representation of a LimitlessLED White group."""
def __init__(self, group):
def __init__(self, group, config):
"""Initialize White group."""
super().__init__(group)
super().__init__(group, config)
# Initialize group with known values.
self.group.on = True
self.group.temperature = 1.0
@ -242,9 +250,9 @@ class LimitlessLEDWhiteGroup(LimitlessLEDGroup):
class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
"""Representation of a LimitlessLED RGBW group."""
def __init__(self, group):
def __init__(self, group, config):
"""Initialize RGBW group."""
super().__init__(group)
super().__init__(group, config)
# Initialize group with known values.
self.group.on = True
self.group.white()
@ -301,9 +309,9 @@ class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
class LimitlessLEDRGBWWGroup(LimitlessLEDGroup):
"""Representation of a LimitlessLED RGBWW group."""
def __init__(self, group):
def __init__(self, group, config):
"""Initialize RGBWW group."""
super().__init__(group)
super().__init__(group, config)
# Initialize group with known values.
self.group.on = True
self.group.white()

View File

@ -101,3 +101,98 @@ hue_activate_scene:
scene_name:
description: Name of hue scene from the hue app
example: "Energize"
lifx_set_state:
description: Set a color/brightness and possibliy turn the light on/off
fields:
entity_id:
description: Name(s) of entities to set a state on
example: 'light.garage'
'...':
description: All turn_on parameters can be used to specify a color
infrared:
description: Automatic infrared level (0..255) when light brightness is low
example: 255
transition:
description: Duration in seconds it takes to get to the final state
example: 10
power:
description: Turn the light on (True) or off (False). Leave out to keep the power as it is.
example: True
lifx_effect_pulse:
description: Run a flash effect by changing to a color and back.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.kitchen'
mode:
description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid'
example: strobe
brightness:
description: Number between 0..255 indicating brightness of the temporary color
example: 120
color_name:
description: A human readable color name
example: 'red'
rgb_color:
description: The temporary color in RGB-format
example: '[255, 100, 100]'
period:
description: Duration of the effect in seconds (default 1.0)
example: 3
cycles:
description: Number of times the effect should run (default 1.0)
example: 2
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_colorloop:
description: Run an effect with looping colors.
fields:
entity_id:
description: Name(s) of entities to run the effect on
example: 'light.disco1, light.disco2, light.disco3'
brightness:
description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light
example: 120
period:
description: Duration (in seconds) between color changes (default 60)
example: 180
change:
description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20)
example: 45
spread:
description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30)
example: 0
power_on:
description: Powered off lights are temporarily turned on during the effect (default True)
example: False
lifx_effect_stop:
description: Stop a running effect.
fields:
entity_id:
description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere.
example: 'light.bedroom'

View File

@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/verisure/
"""
import logging
from time import sleep
from time import time
from homeassistant.components.verisure import HUB as hub
from homeassistant.components.verisure import (CONF_LOCKS, CONF_CODE_DIGITS)
from homeassistant.components.lock import LockDevice
@ -19,28 +20,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Verisure platform."""
locks = []
if int(hub.config.get(CONF_LOCKS, 1)):
hub.update_locks()
hub.update_overview()
locks.extend([
VerisureDoorlock(device_id)
for device_id in hub.lock_status
])
VerisureDoorlock(device_label)
for device_label in hub.get(
"$.doorLockStatusList[*].deviceLabel")])
add_devices(locks)
class VerisureDoorlock(LockDevice):
"""Representation of a Verisure doorlock."""
def __init__(self, device_id):
def __init__(self, device_label):
"""Initialize the Verisure lock."""
self._id = device_id
self._device_label = device_label
self._state = STATE_UNKNOWN
self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None
self._change_timestamp = 0
@property
def name(self):
"""Return the name of the lock."""
return '{}'.format(hub.lock_status[self._id].location)
return hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].area",
self._device_label)
@property
def state(self):
@ -50,7 +55,9 @@ class VerisureDoorlock(LockDevice):
@property
def available(self):
"""Return True if entity is available."""
return hub.available
return hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')]",
self._device_label) is not None
@property
def changed_by(self):
@ -64,32 +71,52 @@ class VerisureDoorlock(LockDevice):
def update(self):
"""Update lock status."""
hub.update_locks()
if hub.lock_status[self._id].status == 'unlocked':
if time() - self._change_timestamp < 10:
return
hub.update_overview()
status = hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].lockedState",
self._device_label)
if status == 'UNLOCKED':
self._state = STATE_UNLOCKED
elif hub.lock_status[self._id].status == 'locked':
elif status == 'LOCKED':
self._state = STATE_LOCKED
elif hub.lock_status[self._id].status != 'pending':
_LOGGER.error(
"Unknown lock state %s", hub.lock_status[self._id].status)
self._changed_by = hub.lock_status[self._id].name
elif status != 'PENDING':
_LOGGER.error('Unknown lock state %s', status)
self._changed_by = hub.get_first(
"$.doorLockStatusList[?(@.deviceLabel=='%s')].userString",
self._device_label)
@property
def is_locked(self):
"""Return true if lock is locked."""
return hub.lock_status[self._id].status
return self._state == STATE_LOCKED
def unlock(self, **kwargs):
"""Send unlock command."""
hub.my_pages.lock.set(kwargs[ATTR_CODE], self._id, 'UNLOCKED')
_LOGGER.debug("Verisure doorlock unlocking")
hub.my_pages.lock.wait_while_pending()
self.update()
if self._state == STATE_UNLOCKED:
return
self.set_lock_state(kwargs[ATTR_CODE], STATE_UNLOCKED)
def lock(self, **kwargs):
"""Send lock command."""
hub.my_pages.lock.set(kwargs[ATTR_CODE], self._id, 'LOCKED')
_LOGGER.debug("Verisure doorlock locking")
hub.my_pages.lock.wait_while_pending()
self.update()
if self._state == STATE_LOCKED:
return
self.set_lock_state(kwargs[ATTR_CODE], STATE_LOCKED)
def set_lock_state(self, code, state):
"""Send set lock state command."""
lock_state = 'lock' if state == STATE_LOCKED else 'unlock'
transaction_id = hub.session.set_lock_state(
code,
self._device_label,
lock_state)['doorLockStateChangeTransactionId']
_LOGGER.debug("Verisure doorlock %s", state)
transaction = {}
while 'result' not in transaction:
sleep(0.5)
transaction = hub.session.get_lock_state_transaction(
transaction_id)
if transaction['result'] == 'OK':
self._state = state
self._change_timestamp = time()

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/media_player.denon/
"""
import logging
from collections import namedtuple
import voluptuous as vol
from homeassistant.components.media_player import (
@ -16,16 +17,19 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY)
from homeassistant.const import (
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
CONF_NAME, STATE_ON)
CONF_NAME, STATE_ON, CONF_ZONE)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.4.4']
REQUIREMENTS = ['denonavr==0.5.1']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = None
DEFAULT_SHOW_SOURCES = False
CONF_SHOW_ALL_SOURCES = 'show_all_sources'
CONF_ZONES = 'zones'
CONF_VALID_ZONES = ['Zone2', 'Zone3']
CONF_INVALID_ZONES_ERR = 'Invalid Zone (expected Zone2 or Zone3)'
KEY_DENON_CACHE = 'denonavr_hosts'
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
@ -36,16 +40,26 @@ SUPPORT_MEDIA_MODES = SUPPORT_PLAY_MEDIA | \
SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET | SUPPORT_PLAY
DENON_ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE): vol.In(CONF_VALID_ZONES, CONF_INVALID_ZONES_ERR),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES):
cv.boolean,
vol.Optional(CONF_ZONES):
vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA])
})
NewHost = namedtuple('NewHost', ['host', 'name'])
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Denon platform."""
# pylint: disable=import-error
import denonavr
# Initialize list with receivers to be started
@ -55,28 +69,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if cache is None:
cache = hass.data[KEY_DENON_CACHE] = set()
# Start assignment of host and name
# Get config option for show_all_sources
show_all_sources = config.get(CONF_SHOW_ALL_SOURCES)
# Get config option for additional zones
zones = config.get(CONF_ZONES)
if zones is not None:
add_zones = {}
for entry in zones:
add_zones[entry[CONF_ZONE]] = entry[CONF_NAME]
else:
add_zones = None
# Start assignment of host and name
new_hosts = []
# 1. option: manual setting
if config.get(CONF_HOST) is not None:
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
# Check if host not in cache, append it and save for later starting
if host not in cache:
cache.add(host)
receivers.append(
DenonDevice(denonavr.DenonAVR(host, name, show_all_sources)))
_LOGGER.info("Denon receiver at host %s initialized", host)
new_hosts.append(NewHost(host=host, name=name))
# 2. option: discovery using netdisco
if discovery_info is not None:
host = discovery_info.get('host')
name = discovery_info.get('name')
# Check if host not in cache, append it and save for later starting
if host not in cache:
cache.add(host)
receivers.append(
DenonDevice(denonavr.DenonAVR(host, name, show_all_sources)))
_LOGGER.info("Denon receiver at host %s initialized", host)
new_hosts.append(NewHost(host=host, name=name))
# 3. option: discovery using denonavr library
if config.get(CONF_HOST) is None and discovery_info is None:
d_receivers = denonavr.discover()
@ -85,14 +103,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for d_receiver in d_receivers:
host = d_receiver["host"]
name = d_receiver["friendlyName"]
# Check if host not in cache, append it and save for later
# starting
if host not in cache:
cache.add(host)
receivers.append(
DenonDevice(
denonavr.DenonAVR(host, name, show_all_sources)))
_LOGGER.info("Denon receiver at host %s initialized", host)
new_hosts.append(NewHost(host=host, name=name))
for entry in new_hosts:
# Check if host not in cache, append it and save for later
# starting
if entry.host not in cache:
new_device = denonavr.DenonAVR(
entry.host, entry.name, show_all_sources, add_zones)
for new_zone in new_device.zones.values():
receivers.append(DenonDevice(new_zone))
cache.add(host)
_LOGGER.info("Denon receiver at host %s initialized", host)
# Add all freshly discovered receivers
if receivers:

View File

@ -21,7 +21,7 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyemby==1.2']
REQUIREMENTS = ['pyemby==1.3']
_LOGGER = logging.getLogger(__name__)

View File

@ -540,7 +540,7 @@ class KodiDevice(MediaPlayerDevice):
elif self._turn_off_action == 'shutdown':
yield from self.server.System.Shutdown()
else:
_LOGGER.warning('turn_off requested but turn_off_action is none')
_LOGGER.warning("turn_off requested but turn_off_action is none")
@cmd
@asyncio.coroutine
@ -694,22 +694,26 @@ class KodiDevice(MediaPlayerDevice):
def async_call_method(self, method, **kwargs):
"""Run Kodi JSONRPC API method with params."""
import jsonrpc_base
_LOGGER.debug('Run API method "%s", kwargs=%s', method, kwargs)
_LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs)
result_ok = False
try:
result = yield from getattr(self.server, method)(**kwargs)
result_ok = True
except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]['error']
_LOGGER.error('Run API method %s.%s(%s) error: %s',
_LOGGER.error("Run API method %s.%s(%s) error: %s",
self.entity_id, method, kwargs, result)
except jsonrpc_base.jsonrpc.TransportError:
result = None
_LOGGER.warning("TransportError trying to run API method "
"%s.%s(%s)", self.entity_id, method, kwargs)
if isinstance(result, dict):
event_data = {'entity_id': self.entity_id,
'result': result,
'result_ok': result_ok,
'input': {'method': method, 'params': kwargs}}
_LOGGER.debug('EVENT kodi_call_method_result: %s', event_data)
_LOGGER.debug("EVENT kodi_call_method_result: %s", event_data)
self.hass.bus.async_fire(EVENT_KODI_CALL_METHOD_RESULT,
event_data=event_data)
return result
@ -753,10 +757,13 @@ class KodiDevice(MediaPlayerDevice):
yield from self.server.Playlist.Add(params)
except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]['error']
_LOGGER.error('Run API method %s.Playlist.Add(%s) error: %s',
_LOGGER.error("Run API method %s.Playlist.Add(%s) error: %s",
self.entity_id, media_type, result)
except jsonrpc_base.jsonrpc.TransportError:
_LOGGER.warning("TransportError trying to add playlist to %s",
self.entity_id)
else:
_LOGGER.warning('No media detected for Playlist.Add')
_LOGGER.warning("No media detected for Playlist.Add")
@asyncio.coroutine
def async_add_all_albums(self, artist_name):
@ -800,7 +807,7 @@ class KodiDevice(MediaPlayerDevice):
artist_name, [a['artist'] for a in artists['artists']])
return artists['artists'][out[0][0]]['artistid']
except KeyError:
_LOGGER.warning('No artists were found: %s', artist_name)
_LOGGER.warning("No artists were found: %s", artist_name)
return None
@asyncio.coroutine
@ -839,7 +846,7 @@ class KodiDevice(MediaPlayerDevice):
album_name, [a['label'] for a in albums['albums']])
return albums['albums'][out[0][0]]['albumid']
except KeyError:
_LOGGER.warning('No albums were found with artist: %s, album: %s',
_LOGGER.warning("No albums were found with artist: %s, album: %s",
artist_name, album_name)
return None

View File

@ -14,7 +14,8 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_PLAY, MEDIA_TYPE_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_SHUFFLE_SET,
SUPPORT_SEEK, MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD,
CONF_HOST, CONF_NAME)
@ -32,7 +33,8 @@ PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=120)
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE
SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE | \
SUPPORT_CLEAR_PLAYLIST | SUPPORT_SHUFFLE_SET | SUPPORT_SEEK
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
@ -266,3 +268,20 @@ class MpdDevice(MediaPlayerDevice):
self.client.clear()
self.client.add(media_id)
self.client.play()
@property
def shuffle(self):
"""Boolean if shuffle is enabled."""
return bool(self.status['random'])
def set_shuffle(self, shuffle):
"""Enable/disable shuffle mode."""
self.client.random(int(shuffle))
def clear_playlist(self):
"""Clear players playlist."""
self.client.clear()
def media_seek(self, position):
"""Send seek command."""
self.client.seekcur(position)

View File

@ -88,6 +88,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
elif discovery_info is not None:
# Parse discovery data
host = discovery_info.get('host')
port = discovery_info.get('port')
host = '%s:%s' % (host, port)
_LOGGER.info("Discovered PLEX server: %s", host)
if host in _CONFIGURING:
@ -106,6 +108,7 @@ def setup_plexserver(host, token, hass, config, add_devices_callback):
try:
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
_LOGGER.info("Discovery configuration done (no token needed)")
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
plexapi.exceptions.NotFound) as error:
_LOGGER.info(error)

View File

@ -20,7 +20,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT,
STATE_PAUSED, STATE_PLAYING,
STATE_UNAVAILABLE)
REQUIREMENTS = ['libsoundtouch==0.3.0']
REQUIREMENTS = ['libsoundtouch==0.6.2']
_LOGGER = logging.getLogger(__name__)

View File

@ -29,7 +29,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA
REQUIREMENTS = ['paho-mqtt==1.2.3']
REQUIREMENTS = ['paho-mqtt==1.3.0']
_LOGGER = logging.getLogger(__name__)

View File

@ -17,8 +17,8 @@ from homeassistant.components.mqtt import CONF_STATE_TOPIC
_LOGGER = logging.getLogger(__name__)
TOPIC_MATCHER = re.compile(
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/(?P<object_id>[a-zA-Z0-9_-]+)'
'/config')
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/'
r'(?:(?P<node_id>[a-zA-Z0-9_-]+)/)?(?P<object_id>[a-zA-Z0-9_-]+)/config')
SUPPORTED_COMPONENTS = ['binary_sensor', 'light', 'sensor', 'switch']
@ -44,7 +44,7 @@ def async_start(hass, discovery_topic, hass_config):
if not match:
return
prefix_topic, component, object_id = match.groups()
prefix_topic, component, node_id, object_id = match.groups()
try:
payload = json.loads(payload)
@ -65,21 +65,25 @@ def async_start(hass, discovery_topic, hass_config):
payload[CONF_PLATFORM] = platform
if CONF_STATE_TOPIC not in payload:
payload[CONF_STATE_TOPIC] = '{}/{}/{}/state'.format(
discovery_topic, component, object_id)
payload[CONF_STATE_TOPIC] = '{}/{}/{}{}/state'.format(
discovery_topic, component, '%s/' % node_id if node_id else '',
object_id)
if ALREADY_DISCOVERED not in hass.data:
hass.data[ALREADY_DISCOVERED] = set()
discovery_hash = (component, object_id)
# If present, the node_id will be included in the discovered object id
discovery_id = '_'.join((node_id, object_id)) if node_id else object_id
discovery_hash = (component, discovery_id)
if discovery_hash in hass.data[ALREADY_DISCOVERED]:
_LOGGER.info("Component has already been discovered: %s %s",
component, object_id)
component, discovery_id)
return
hass.data[ALREADY_DISCOVERED].add(discovery_hash)
_LOGGER.info("Found new component: %s %s", component, object_id)
_LOGGER.info("Found new component: %s %s", component, discovery_id)
yield from async_load_platform(
hass, component, platform, payload, hass_config)

View File

@ -0,0 +1,80 @@
"""
Clicksend platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.clicksend/
"""
import json
import logging
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_API_KEY, CONF_RECIPIENT, HTTP_HEADER_CONTENT_TYPE,
CONTENT_TYPE_JSON)
from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
BASE_API_URL = 'https://rest.clicksend.com/v3'
HEADERS = {HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_RECIPIENT): cv.string,
})
def get_service(hass, config, discovery_info=None):
"""Get the ClickSend notification service."""
if _authenticate(config) is False:
_LOGGER.exception("You are not authorized to access ClickSend")
return None
return ClicksendNotificationService(config)
class ClicksendNotificationService(BaseNotificationService):
"""Implementation of a notification service for the ClickSend service."""
def __init__(self, config):
"""Initialize the service."""
self.username = config.get(CONF_USERNAME)
self.api_key = config.get(CONF_API_KEY)
self.recipient = config.get(CONF_RECIPIENT)
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
data = ({'messages': [{'source': 'hass.notify', 'from': self.recipient,
'to': self.recipient, 'body': message}]})
api_url = "{}/sms/send".format(BASE_API_URL)
resp = requests.post(api_url, data=json.dumps(data), headers=HEADERS,
auth=(self.username, self.api_key), timeout=5)
obj = json.loads(resp.text)
response_msg = obj['response_msg']
response_code = obj['response_code']
if resp.status_code != 200:
_LOGGER.error("Error %s : %s (Code %s)", resp.status_code,
response_msg, response_code)
def _authenticate(config):
"""Authenticate with ClickSend."""
api_url = '{}/account'.format(BASE_API_URL)
resp = requests.get(api_url, headers=HEADERS,
auth=(config.get(CONF_USERNAME),
config.get(CONF_API_KEY)), timeout=5)
if resp.status_code != 200:
return False
return True

View File

@ -25,7 +25,7 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.components.frontend import add_manifest_json_key
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['pywebpush==1.0.4', 'PyJWT==1.5.0']
REQUIREMENTS = ['pywebpush==1.0.5', 'PyJWT==1.5.0']
DEPENDENCIES = ['frontend']

View File

@ -29,16 +29,18 @@ _LOGGER = logging.getLogger(__name__)
ATTR_IMAGES = 'images' # optional embedded image file attachments
ATTR_HTML = 'html'
CONF_STARTTLS = 'starttls'
CONF_ENCRYPTION = 'encryption'
CONF_DEBUG = 'debug'
CONF_SERVER = 'server'
CONF_SENDER_NAME = 'sender_name'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 25
DEFAULT_PORT = 587
DEFAULT_TIMEOUT = 5
DEFAULT_DEBUG = False
DEFAULT_STARTTLS = False
DEFAULT_ENCRYPTION = 'starttls'
ENCRYPTION_OPTIONS = ['tls', 'starttls', 'none']
# pylint: disable=no-value-for-parameter
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -47,7 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
vol.Optional(CONF_ENCRYPTION, default=DEFAULT_ENCRYPTION):
vol.In(ENCRYPTION_OPTIONS),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SENDER_NAME): cv.string,
@ -62,7 +65,7 @@ def get_service(hass, config, discovery_info=None):
config.get(CONF_PORT),
config.get(CONF_TIMEOUT),
config.get(CONF_SENDER),
config.get(CONF_STARTTLS),
config.get(CONF_ENCRYPTION),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
config.get(CONF_RECIPIENT),
@ -78,28 +81,32 @@ def get_service(hass, config, discovery_info=None):
class MailNotificationService(BaseNotificationService):
"""Implement the notification service for E-mail messages."""
def __init__(self, server, port, timeout, sender, starttls, username,
def __init__(self, server, port, timeout, sender, encryption, username,
password, recipients, sender_name, debug):
"""Initialize the SMTP service."""
self._server = server
self._port = port
self._timeout = timeout
self._sender = sender
self.starttls = starttls
self.encryption = encryption
self.username = username
self.password = password
self.recipients = recipients
self._sender_name = sender_name
self._timeout = timeout
self.debug = debug
self.tries = 2
def connect(self):
"""Connect/authenticate to SMTP Server."""
mail = smtplib.SMTP(self._server, self._port, timeout=self._timeout)
if self.encryption == "tls":
mail = smtplib.SMTP_SSL(
self._server, self._port, timeout=self._timeout)
else:
mail = smtplib.SMTP(
self._server, self._port, timeout=self._timeout)
mail.set_debuglevel(self.debug)
mail.ehlo_or_helo_if_needed()
if self.starttls:
if self.encryption == "starttls":
mail.starttls()
mail.ehlo()
if self.username and self.password:

View File

@ -9,7 +9,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import sanitize_filename
DOMAIN = 'python_script'
REQUIREMENTS = ['restrictedpython==4.0a2']
REQUIREMENTS = ['restrictedpython==4.0a3']
FOLDER = 'python_scripts'
_LOGGER = logging.getLogger(__name__)

View File

@ -33,7 +33,7 @@ from . import purge, migration
from .const import DATA_INSTANCE
from .util import session_scope
REQUIREMENTS = ['sqlalchemy==1.1.10']
REQUIREMENTS = ['sqlalchemy==1.1.11']
_LOGGER = logging.getLogger(__name__)

View File

@ -64,6 +64,8 @@ def _apply_update(engine, new_version):
# Create indexes for states
_create_index(engine, "states", "ix_states_last_updated")
_create_index(engine, "states", "ix_states_entity_id_created")
elif new_version == 3:
_create_index(engine, "states", "ix_states_created_domain")
else:
raise ValueError("No schema migration defined for version {}"
.format(new_version))

View File

@ -16,7 +16,7 @@ from homeassistant.remote import JSONEncoder
# pylint: disable=invalid-name
Base = declarative_base()
SCHEMA_VERSION = 2
SCHEMA_VERSION = 3
_LOGGER = logging.getLogger(__name__)
@ -75,7 +75,9 @@ class States(Base): # type: ignore
Index('states__significant_changes',
'domain', 'last_updated', 'entity_id'),
Index('ix_states_entity_id_created',
'entity_id', 'created'),)
'entity_id', 'created'),
Index('ix_states_created_domain',
'created', 'domain'),)
@staticmethod
def from_event(event):

View File

@ -26,12 +26,13 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 5222
DEVICES = []
CONF_DEVICE_CACHE = 'device_cache'
SERVICE_SYNC = 'harmony_sync'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(ATTR_ACTIVITY, default=None): cv.string,
})
@ -44,29 +45,65 @@ HARMONY_SYNC_SCHEMA = vol.Schema({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Harmony platform."""
import pyharmony
global DEVICES
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
_LOGGER.debug("Loading Harmony platform: %s", name)
host = None
activity = None
harmony_conf_file = hass.config.path(
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
if CONF_DEVICE_CACHE not in hass.data:
hass.data[CONF_DEVICE_CACHE] = []
if discovery_info:
# Find the discovered device in the list of user configurations
override = next((c for c in hass.data[CONF_DEVICE_CACHE]
if c.get(CONF_NAME) == discovery_info.get(CONF_NAME)),
False)
port = DEFAULT_PORT
if override:
activity = override.get(ATTR_ACTIVITY)
port = override.get(CONF_PORT, DEFAULT_PORT)
host = (
discovery_info.get(CONF_NAME),
discovery_info.get(CONF_HOST),
port)
# Ignore hub name when checking if this hub is known - ip and port only
if host and host[1:] in set([h[1:] for h in DEVICES]):
_LOGGER.debug("Discovered host already known: %s", host)
return
elif CONF_HOST in config:
host = (
config.get(CONF_NAME),
config.get(CONF_HOST),
config.get(CONF_PORT),
)
activity = config.get(ATTR_ACTIVITY)
else:
hass.data[CONF_DEVICE_CACHE].append(config)
return
name, address, port = host
_LOGGER.info("Loading Harmony Platform: %s at %s:%s, startup activity: %s",
name, address, port, activity)
try:
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
host, port)
token = urllib.parse.quote_plus(pyharmony.ha_get_token(host, port))
address, port)
token = urllib.parse.quote_plus(pyharmony.ha_get_token(address, port))
_LOGGER.debug("Received token: %s", token)
except ValueError as err:
_LOGGER.warning("%s for remote: %s", err.args[0], name)
return False
_LOGGER.debug("Received token: %s", token)
DEVICES = [HarmonyRemote(
config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT),
config.get(ATTR_ACTIVITY), harmony_conf_file, token)]
add_devices(DEVICES, True)
harmony_conf_file = hass.config.path(
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
device = HarmonyRemote(
name, address, port,
activity, harmony_conf_file, token)
DEVICES.append(device)
add_devices([device])
register_services(hass)
return True

View File

@ -10,9 +10,12 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
ATTR_ENTITY_ID, TEMP_CELSIUS,
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
)
from homeassistant.helpers.entity import Entity
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
REQUIREMENTS = ['pyRFXtrx==0.18.0']
@ -27,7 +30,9 @@ ATTR_STATE = 'state'
ATTR_NAME = 'name'
ATTR_FIREEVENT = 'fire_event'
ATTR_DATA_TYPE = 'data_type'
ATTR_DATA_BITS = 'data_bits'
ATTR_DUMMY = 'dummy'
ATTR_OFF_DELAY = 'off_delay'
CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
CONF_DEVICES = 'devices'
EVENT_BUTTON_PRESSED = 'button_pressed'
@ -43,7 +48,8 @@ DATA_TYPES = OrderedDict([
('Total usage', 'W'),
('Sound', ''),
('Sensor Status', ''),
('Counter value', '')])
('Counter value', ''),
('UV', 'uv')])
RECEIVED_EVT_SUBSCRIBERS = []
RFX_DEVICES = {}
@ -77,6 +83,8 @@ def _valid_device(value, device_type):
if device_type == 'sensor':
config[key] = DEVICE_SCHEMA_SENSOR(device)
elif device_type == 'binary_sensor':
config[key] = DEVICE_SCHEMA_BINARYSENSOR(device)
elif device_type == 'light_switch':
config[key] = DEVICE_SCHEMA(device)
else:
@ -92,6 +100,11 @@ def valid_sensor(value):
return _valid_device(value, "sensor")
def valid_binary_sensor(value):
"""Validate binary sensor configuration."""
return _valid_device(value, "binary_sensor")
def _valid_light_switch(value):
return _valid_device(value, "light_switch")
@ -108,6 +121,17 @@ DEVICE_SCHEMA_SENSOR = vol.Schema({
vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]),
})
DEVICE_SCHEMA_BINARYSENSOR = vol.Schema({
vol.Optional(ATTR_NAME, default=None): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=None): cv.string,
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
vol.Optional(ATTR_OFF_DELAY, default=None):
vol.Any(cv.time_period, cv.positive_timedelta),
vol.Optional(ATTR_DATA_BITS, default=None): cv.positive_int,
vol.Optional(CONF_COMMAND_ON, default=None): cv.byte,
vol.Optional(CONF_COMMAND_OFF, default=None): cv.byte
})
DEFAULT_SCHEMA = vol.Schema({
vol.Required("platform"): DOMAIN,
vol.Optional(CONF_DEVICES, default={}): vol.All(dict, _valid_light_switch),
@ -191,6 +215,78 @@ def get_rfx_object(packetid):
return obj
def get_pt2262_deviceid(device_id, nb_data_bits):
"""Extract and return the address bits from a Lighting4/PT2262 packet."""
import binascii
try:
data = bytearray.fromhex(device_id)
except ValueError:
return None
mask = 0xFF & ~((1 << nb_data_bits) - 1)
data[len(data)-1] &= mask
return binascii.hexlify(data)
def get_pt2262_cmd(device_id, data_bits):
"""Extract and return the data bits from a Lighting4/PT2262 packet."""
try:
data = bytearray.fromhex(device_id)
except ValueError:
return None
mask = 0xFF & ((1 << data_bits) - 1)
return hex(data[-1] & mask)
# pylint: disable=unused-variable
def get_pt2262_device(device_id):
"""Look for the device which id matches the given device_id parameter."""
for dev_id, device in RFX_DEVICES.items():
try:
if (device.is_pt2262 and
device.masked_id == get_pt2262_deviceid(
device_id,
device.data_bits)):
_LOGGER.info("rfxtrx: found matching device %s for %s",
device_id,
get_pt2262_deviceid(device_id, device.data_bits))
return device
except AttributeError:
continue
return None
# pylint: disable=unused-variable
def find_possible_pt2262_device(device_id):
"""Look for the device which id matches the given device_id parameter."""
for dev_id, device in RFX_DEVICES.items():
if len(dev_id) == len(device_id):
size = None
for i in range(0, len(dev_id)):
if dev_id[i] != device_id[i]:
break
size = i
if size is not None:
size = len(dev_id) - size - 1
_LOGGER.info("rfxtrx: found possible device %s for %s "
"with the following configuration:\n"
"data_bits=%d\n"
"command_on=0x%s\n"
"command_off=0x%s\n",
device_id,
dev_id,
size * 4,
dev_id[-size:], device_id[-size:])
return device
return None
def get_devices_from_config(config, device, hass):
"""Read rfxtrx configuration."""
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
@ -318,6 +414,11 @@ class RfxtrxDevice(Entity):
"""Return is the device must fire event."""
return self._should_fire_event
@property
def is_pt2262(self):
"""Return true if the device is PT2262-based."""
return False
@property
def is_on(self):
"""Return true if device is on."""

View File

@ -102,7 +102,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pin=pinnum, unit_of_measurement=pin.get(
CONF_UNIT_OF_MEASUREMENT), renderer=renderer))
add_devices(dev)
add_devices(dev, True)
class ArestSensor(Entity):
@ -119,7 +119,6 @@ class ArestSensor(Entity):
self._state = STATE_UNKNOWN
self._unit_of_measurement = unit_of_measurement
self._renderer = renderer
self.update()
if self._pin is not None:
request = requests.get(

View File

@ -0,0 +1,145 @@
"""
Support for BH1750 light sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.bh1750/
"""
import asyncio
from functools import partial
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['i2csense==0.0.4',
'smbus-cffi==0.5.1']
_LOGGER = logging.getLogger(__name__)
CONF_I2C_ADDRESS = 'i2c_address'
CONF_I2C_BUS = 'i2c_bus'
CONF_OPERATION_MODE = 'operation_mode'
CONF_SENSITIVITY = 'sensitivity'
CONF_DELAY = 'measurement_delay_ms'
CONF_MULTIPLIER = 'multiplier'
# Operation modes for BH1750 sensor (from the datasheet). Time typically 120ms
# In one time measurements, device is set to Power Down after each sample.
CONTINUOUS_LOW_RES_MODE = "continuous_low_res_mode"
CONTINUOUS_HIGH_RES_MODE_1 = "continuous_high_res_mode_1"
CONTINUOUS_HIGH_RES_MODE_2 = "continuous_high_res_mode_2"
ONE_TIME_LOW_RES_MODE = "one_time_low_res_mode"
ONE_TIME_HIGH_RES_MODE_1 = "one_time_high_res_mode_1"
ONE_TIME_HIGH_RES_MODE_2 = "one_time_high_res_mode_2"
OPERATION_MODES = {
CONTINUOUS_LOW_RES_MODE: (0x13, True), # 4lx resolution
CONTINUOUS_HIGH_RES_MODE_1: (0x10, True), # 1lx resolution.
CONTINUOUS_HIGH_RES_MODE_2: (0X11, True), # 0.5lx resolution.
ONE_TIME_LOW_RES_MODE: (0x23, False), # 4lx resolution.
ONE_TIME_HIGH_RES_MODE_1: (0x20, False), # 1lx resolution.
ONE_TIME_HIGH_RES_MODE_2: (0x21, False), # 0.5lx resolution.
}
SENSOR_UNIT = 'lx'
DEFAULT_NAME = 'BH1750 Light Sensor'
DEFAULT_I2C_ADDRESS = '0x23'
DEFAULT_I2C_BUS = 1
DEFAULT_MODE = CONTINUOUS_HIGH_RES_MODE_1
DEFAULT_DELAY_MS = 120
DEFAULT_SENSITIVITY = 69 # from 31 to 254
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.string,
vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int),
vol.Optional(CONF_OPERATION_MODE, default=DEFAULT_MODE):
vol.In(OPERATION_MODES),
vol.Optional(CONF_SENSITIVITY, default=DEFAULT_SENSITIVITY):
cv.positive_int,
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY_MS): cv.positive_int,
vol.Optional(CONF_MULTIPLIER, default=1.): vol.Range(min=0.1, max=10),
})
# pylint: disable=import-error
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the BH1750 sensor."""
import smbus
from i2csense.bh1750 import BH1750
name = config.get(CONF_NAME)
bus_number = config.get(CONF_I2C_BUS)
i2c_address = config.get(CONF_I2C_ADDRESS)
operation_mode = config.get(CONF_OPERATION_MODE)
bus = smbus.SMBus(bus_number)
sensor = yield from hass.async_add_job(
partial(BH1750, bus, i2c_address,
operation_mode=operation_mode,
measurement_delay=config.get(CONF_DELAY),
sensitivity=config.get(CONF_SENSITIVITY),
logger=_LOGGER)
)
if not sensor.sample_ok:
_LOGGER.error("BH1750 sensor not detected at %s", i2c_address)
return False
dev = [BH1750Sensor(sensor, name, SENSOR_UNIT,
config.get(CONF_MULTIPLIER))]
_LOGGER.info("Setup of BH1750 light sensor at %s in mode %s is complete.",
i2c_address, operation_mode)
async_add_devices(dev)
class BH1750Sensor(Entity):
"""Implementation of the BH1750 sensor."""
def __init__(self, bh1750_sensor, name, unit, multiplier=1.):
"""Initialize the sensor."""
self._name = name
self._unit_of_measurement = unit
self._multiplier = multiplier
self.bh1750_sensor = bh1750_sensor
if self.bh1750_sensor.light_level >= 0:
self._state = int(round(self.bh1750_sensor.light_level))
else:
self._state = None
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def state(self) -> int:
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self) -> str:
"""Return the unit of measurement of the sensor."""
return self._unit_of_measurement
@property
def device_class(self) -> str:
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'light'
@asyncio.coroutine
def async_update(self):
"""Get the latest data from the BH1750 and update the states."""
yield from self.hass.async_add_job(self.bh1750_sensor.update)
if self.bh1750_sensor.sample_ok \
and self.bh1750_sensor.light_level >= 0:
self._state = int(round(self.bh1750_sensor.light_level
* self._multiplier))
else:
_LOGGER.warning("Bad Update of sensor.%s: %s",
self.name, self.bh1750_sensor.light_level)

Some files were not shown because too many files have changed in this diff Show More