mirror of
https://github.com/home-assistant/core.git
synced 2025-08-04 13:15:18 +02:00
12
.coveragerc
12
.coveragerc
@@ -20,6 +20,9 @@ omit =
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
@@ -81,6 +84,9 @@ omit =
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
@@ -111,6 +117,8 @@ omit =
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
homeassistant/components/garage_door/rpi_gpio.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
@@ -120,7 +128,9 @@ omit =
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
@@ -170,6 +180,7 @@ omit =
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
@@ -180,6 +191,7 @@ omit =
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
|
@@ -22,6 +22,9 @@ http:
|
||||
# Set to 1 to enable development mode
|
||||
# development: 1
|
||||
|
||||
frontend:
|
||||
# enable the frontend
|
||||
|
||||
light:
|
||||
# platform: hue
|
||||
|
||||
|
@@ -3,7 +3,6 @@
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from threading import RLock
|
||||
@@ -11,22 +10,16 @@ from threading import RLock
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components as core_components
|
||||
import homeassistant.components.group as group
|
||||
import homeassistant.config as config_util
|
||||
from homeassistant.components import group, persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.util.location as loc_util
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.const import (
|
||||
CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME,
|
||||
CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__)
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SETUP_LOCK = RLock()
|
||||
@@ -208,11 +201,6 @@ def prepare_setup_platform(hass, config, domain, platform_name):
|
||||
return platform
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir):
|
||||
"""Add local library to Python Path."""
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
|
||||
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
verbose=False, skip_pip=False,
|
||||
@@ -226,18 +214,17 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
if config_dir is not None:
|
||||
config_dir = os.path.abspath(config_dir)
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
_mount_local_lib_path(config_dir)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA(
|
||||
core_config))
|
||||
except vol.MultipleInvalid as ex:
|
||||
conf_util.process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
|
||||
return None
|
||||
|
||||
process_ha_config_upgrade(hass)
|
||||
conf_util.process_ha_config_upgrade(hass)
|
||||
|
||||
if enable_log:
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
@@ -262,9 +249,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
|
||||
return hass
|
||||
|
||||
persistent_notification.setup(hass, config)
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
@@ -291,12 +279,12 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
|
||||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
_mount_local_lib_path(config_dir)
|
||||
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
try:
|
||||
config_dict = config_util.load_yaml_config_file(config_path)
|
||||
config_dict = conf_util.load_yaml_config_file(config_path)
|
||||
except HomeAssistantError:
|
||||
return None
|
||||
|
||||
@@ -355,101 +343,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None):
|
||||
'Unable to setup error log %s (access denied)', err_log_path)
|
||||
|
||||
|
||||
def process_ha_config_upgrade(hass):
|
||||
"""Upgrade config if necessary."""
|
||||
version_path = hass.config.path('.HA_VERSION')
|
||||
|
||||
try:
|
||||
with open(version_path, 'rt') as inp:
|
||||
conf_version = inp.readline().strip()
|
||||
except FileNotFoundError:
|
||||
# Last version to not have this file
|
||||
conf_version = '0.7.7'
|
||||
|
||||
if conf_version == __version__:
|
||||
return
|
||||
|
||||
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
|
||||
__version__)
|
||||
|
||||
# This was where dependencies were installed before v0.18
|
||||
# Probably should keep this around until ~v0.20.
|
||||
lib_path = hass.config.path('lib')
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
lib_path = hass.config.path('deps')
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
with open(version_path, 'wt') as outp:
|
||||
outp.write(__version__)
|
||||
|
||||
|
||||
def process_ha_core_config(hass, config):
|
||||
"""Process the [homeassistant] section from the config."""
|
||||
hac = hass.config
|
||||
|
||||
def set_time_zone(time_zone_str):
|
||||
"""Helper method to set time zone."""
|
||||
if time_zone_str is None:
|
||||
return
|
||||
|
||||
time_zone = date_util.get_time_zone(time_zone_str)
|
||||
|
||||
if time_zone:
|
||||
hac.time_zone = time_zone
|
||||
date_util.set_default_time_zone(time_zone)
|
||||
else:
|
||||
_LOGGER.error('Received invalid time zone %s', time_zone_str)
|
||||
|
||||
for key, attr in ((CONF_LATITUDE, 'latitude'),
|
||||
(CONF_LONGITUDE, 'longitude'),
|
||||
(CONF_NAME, 'location_name')):
|
||||
if key in config:
|
||||
setattr(hac, attr, config[key])
|
||||
|
||||
if CONF_TIME_ZONE in config:
|
||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||
|
||||
for entity_id, attrs in config.get(CONF_CUSTOMIZE).items():
|
||||
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
|
||||
|
||||
if CONF_TEMPERATURE_UNIT in config:
|
||||
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
|
||||
|
||||
# If we miss some of the needed values, auto detect them
|
||||
if None not in (
|
||||
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
|
||||
return
|
||||
|
||||
_LOGGER.warning('Incomplete core config. Auto detecting location and '
|
||||
'temperature unit')
|
||||
|
||||
info = loc_util.detect_location_info()
|
||||
|
||||
if info is None:
|
||||
_LOGGER.error('Could not detect location information')
|
||||
return
|
||||
|
||||
if hac.latitude is None and hac.longitude is None:
|
||||
hac.latitude = info.latitude
|
||||
hac.longitude = info.longitude
|
||||
|
||||
if hac.temperature_unit is None:
|
||||
if info.use_fahrenheit:
|
||||
hac.temperature_unit = TEMP_FAHRENHEIT
|
||||
else:
|
||||
hac.temperature_unit = TEMP_CELSIUS
|
||||
|
||||
if hac.location_name is None:
|
||||
hac.location_name = info.city
|
||||
|
||||
if hac.time_zone is None:
|
||||
set_time_zone(info.time_zone)
|
||||
|
||||
|
||||
def _ensure_loader_prepared(hass):
|
||||
"""Ensure Home Assistant loader is prepared."""
|
||||
if not loader.PREPARED:
|
||||
loader.prepare(hass)
|
||||
|
||||
|
||||
def _mount_local_lib_path(config_dir):
|
||||
"""Add local library to Python Path."""
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
|
@@ -19,6 +19,8 @@ from homeassistant.const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
"""Load up the module to call the is_on method.
|
||||
@@ -73,6 +75,11 @@ def toggle(hass, entity_id=None, **service_data):
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
|
||||
|
||||
|
||||
def reload_core_config(hass):
|
||||
"""Reload the core config."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup general services related to Home Assistant."""
|
||||
def handle_turn_service(service):
|
||||
@@ -111,4 +118,21 @@ def setup(hass, config):
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
|
||||
def handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import config as conf_util
|
||||
|
||||
try:
|
||||
path = conf_util.find_config_file(hass.config.config_dir)
|
||||
conf = conf_util.load_yaml_config_file(path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
|
||||
handle_reload_config)
|
||||
|
||||
return True
|
||||
|
105
homeassistant/components/alarm_control_panel/envisalink.py
Normal file
105
homeassistant/components/alarm_control_panel/envisalink.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
EnvisalinkDevice,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_CODE,
|
||||
CONF_PARTITIONNAME,
|
||||
SIGNAL_PARTITION_UPDATE,
|
||||
SIGNAL_KEYPAD_UPDATE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
_code = discovery_info[CONF_CODE]
|
||||
for part_num in _configured_partitions:
|
||||
_device_config_data = PARTITION_SCHEMA(
|
||||
_configured_partitions[part_num])
|
||||
_device = EnvisalinkAlarm(
|
||||
part_num,
|
||||
_device_config_data[CONF_PARTITIONNAME],
|
||||
_code,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Represents the Envisalink-based alarm panel."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, partition_number, alarm_name, code, info, controller):
|
||||
"""Initialize the alarm panel."""
|
||||
from pydispatch import dispatcher
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
_LOGGER.debug('Setting up alarm: ' + alarm_name)
|
||||
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
def _update_callback(self, partition):
|
||||
"""Update HA state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._info['status']['alarm']:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
elif self._info['status']['armed_away']:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif self._info['status']['armed_stay']:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._info['status']['alpha']:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Not possible for us."""
|
||||
raise NotImplementedError()
|
@@ -6,7 +6,7 @@ https://home-assistant.io/developers/api/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from time import time
|
||||
import queue
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
@@ -72,19 +72,14 @@ class APIEventStream(HomeAssistantView):
|
||||
|
||||
def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
from eventlet.queue import LightQueue, Empty
|
||||
import eventlet
|
||||
|
||||
cur_hub = eventlet.hubs.get_hub()
|
||||
request.environ['eventlet.minimum_write_chunk_size'] = 0
|
||||
to_write = LightQueue()
|
||||
stop_obj = object()
|
||||
to_write = queue.Queue()
|
||||
|
||||
restrict = request.args.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',')
|
||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||
|
||||
def thread_forward_events(event):
|
||||
def forward_events(event):
|
||||
"""Forward events to the open request."""
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
@@ -99,28 +94,20 @@ class APIEventStream(HomeAssistantView):
|
||||
else:
|
||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||
|
||||
cur_hub.schedule_call_global(0, lambda: to_write.put(data))
|
||||
to_write.put(data)
|
||||
|
||||
def stream():
|
||||
"""Stream events to response."""
|
||||
self.hass.bus.listen(MATCH_ALL, thread_forward_events)
|
||||
self.hass.bus.listen(MATCH_ALL, forward_events)
|
||||
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
|
||||
last_msg = time()
|
||||
# Fire off one message right away to have browsers fire open event
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Somehow our queue.get sometimes takes too long to
|
||||
# be notified of arrival of data. Probably
|
||||
# because of our spawning on hub in other thread
|
||||
# hack. Because current goal is to get this out,
|
||||
# We just timeout every second because it will
|
||||
# return right away if qsize() > 0.
|
||||
# So yes, we're basically polling :(
|
||||
payload = to_write.get(timeout=1)
|
||||
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
|
||||
|
||||
if payload is stop_obj:
|
||||
break
|
||||
@@ -129,15 +116,13 @@ class APIEventStream(HomeAssistantView):
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
yield msg.encode("UTF-8")
|
||||
last_msg = time()
|
||||
except Empty:
|
||||
if time() - last_msg > 50:
|
||||
except queue.Empty:
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
except GeneratorExit:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
break
|
||||
|
||||
self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events)
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
self.hass.bus.remove_listener(MATCH_ALL, forward_events)
|
||||
|
||||
return self.Response(stream(), mimetype='text/event-stream')
|
||||
|
||||
@@ -204,11 +189,12 @@ class APIEntityStateView(HomeAssistantView):
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = request.json.get('attributes')
|
||||
force_update = request.json.get('force_update', False)
|
||||
|
||||
is_new_state = self.hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
self.hass.states.set(entity_id, new_state, attributes)
|
||||
self.hass.states.set(entity_id, new_state, attributes, force_update)
|
||||
|
||||
# Read the state back for our response
|
||||
resp = self.json(self.hass.states.get(entity_id))
|
||||
|
71
homeassistant/components/binary_sensor/envisalink.py
Normal file
71
homeassistant/components/binary_sensor/envisalink.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Support for Envisalink zone states- represented as binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.envisalink/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
ZONE_SCHEMA,
|
||||
CONF_ZONENAME,
|
||||
CONF_ZONETYPE,
|
||||
EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink sensor devices."""
|
||||
_configured_zones = discovery_info['zones']
|
||||
for zone_num in _configured_zones:
|
||||
_device_config_data = ZONE_SCHEMA(_configured_zones[zone_num])
|
||||
_device = EnvisalinkBinarySensor(
|
||||
zone_num,
|
||||
_device_config_data[CONF_ZONENAME],
|
||||
_device_config_data[CONF_ZONETYPE],
|
||||
EVL_CONTROLLER.alarm_state['zone'][zone_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""Representation of an envisalink Binary Sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
self._zone_type = zone_type
|
||||
self._zone_number = zone_number
|
||||
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
EnvisalinkDevice.__init__(self, zone_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault']
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._info['status']['open']
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
def _update_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self.update_ha_state()
|
100
homeassistant/components/binary_sensor/homematic.py
Normal file
100
homeassistant/components/binary_sensor/homematic.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Support for Homematic binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematic/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
SENSOR_TYPES_CLASS = {
|
||||
"Remote": None,
|
||||
"ShutterContact": "opening",
|
||||
"Smoke": "smoke",
|
||||
"SmokeV2": "smoke",
|
||||
"Motion": "motion",
|
||||
"MotionV2": "motion",
|
||||
"RemoteMotion": None
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the Homematic binary sensor platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
return homematic.setup_hmdevice_discovery_helper(HMBinarySensor,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
|
||||
"""Representation of a binary Homematic device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
if not self.available:
|
||||
return False
|
||||
return bool(self._hm_get_state())
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
if not self.available:
|
||||
return None
|
||||
|
||||
# If state is MOTION (RemoteMotion works only)
|
||||
if self._state == "MOTION":
|
||||
return "motion"
|
||||
return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None)
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the HM Object as this HA type."""
|
||||
from pyhomematic.devicetypes.sensors import HMBinarySensor\
|
||||
as pyHMBinarySensor
|
||||
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
# check if the Homematic device correct for this HA device
|
||||
if not isinstance(self._hmdevice, pyHMBinarySensor):
|
||||
_LOGGER.critical("This %s can't be use as binary", self._name)
|
||||
return False
|
||||
|
||||
# if exists user value?
|
||||
if self._state and self._state not in self._hmdevice.BINARYNODE:
|
||||
_LOGGER.critical("This %s have no binary with %s", self._name,
|
||||
self._state)
|
||||
return False
|
||||
|
||||
# only check and give a warning to the user
|
||||
if self._state is None and len(self._hmdevice.BINARYNODE) > 1:
|
||||
_LOGGER.critical("%s have multiple binary params. It use all "
|
||||
"binary nodes as one. Possible param values: %s",
|
||||
self._name, str(self._hmdevice.BINARYNODE))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data struct (self._data) from the Homematic metadata."""
|
||||
super()._init_data_struct()
|
||||
|
||||
# object have 1 binary
|
||||
if self._state is None and len(self._hmdevice.BINARYNODE) == 1:
|
||||
for value in self._hmdevice.BINARYNODE:
|
||||
self._state = value
|
||||
|
||||
# add state to data struct
|
||||
if self._state:
|
||||
_LOGGER.debug("%s init datastruct with main node '%s'", self._name,
|
||||
self._state)
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
@@ -7,10 +7,12 @@ at https://home-assistant.io/components/sensor.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
@@ -41,14 +43,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
self.wink = wink
|
||||
super().__init__(wink)
|
||||
wink = get_component('wink')
|
||||
self._unit_of_measurement = self.wink.UNIT
|
||||
self._battery = self.wink.battery_level
|
||||
self.capability = self.wink.capability()
|
||||
|
||||
@property
|
||||
@@ -67,35 +69,3 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this wink sensor."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor if any."""
|
||||
return self.wink.name()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def update(self):
|
||||
"""Update state of the sensor."""
|
||||
self.wink.update_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
@@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -81,8 +82,6 @@ class Camera(Entity):
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
import eventlet
|
||||
|
||||
def stream():
|
||||
"""Stream images as mjpeg stream."""
|
||||
try:
|
||||
@@ -99,7 +98,7 @@ class Camera(Entity):
|
||||
|
||||
last_image = img_bytes
|
||||
|
||||
eventlet.sleep(0.5)
|
||||
time.sleep(0.5)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
|
||||
|
@@ -1,5 +1,9 @@
|
||||
"""Camera platform that has a Raspberry Pi camera."""
|
||||
"""
|
||||
Camera platform that has a Raspberry Pi camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.rpi_camera/
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
@@ -43,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class RaspberryCamera(Camera):
|
||||
"""Raspberry Pi camera."""
|
||||
"""Representation of a Raspberry Pi camera."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
"""Initialize Raspberry Pi camera component."""
|
||||
|
@@ -37,6 +37,7 @@ def setup(hass, config):
|
||||
"""Setup a demo environment."""
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
|
||||
config.setdefault(ha.DOMAIN, {})
|
||||
config.setdefault(DOMAIN, {})
|
||||
@@ -59,6 +60,11 @@ def setup(hass, config):
|
||||
demo_config[component] = {CONF_PLATFORM: 'demo'}
|
||||
bootstrap.setup_component(hass, component, demo_config)
|
||||
|
||||
# Setup example persistent notification
|
||||
persistent_notification.create(
|
||||
hass, 'This is an example of a persistent notification.',
|
||||
title='Example Notification')
|
||||
|
||||
# Setup room groups
|
||||
lights = sorted(hass.states.entity_ids('light'))
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
|
@@ -83,8 +83,8 @@ class AsusWrtDeviceScanner(object):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = str(config[CONF_USERNAME])
|
||||
self.password = str(config.get(CONF_PASSWORD))
|
||||
self.pub_key = str(config.get('pub_key'))
|
||||
self.password = str(config.get(CONF_PASSWORD, ""))
|
||||
self.pub_key = str(config.get('pub_key', ""))
|
||||
self.protocol = config.get('protocol')
|
||||
self.mode = config.get('mode')
|
||||
|
||||
|
210
homeassistant/components/envisalink.py
Normal file
210
homeassistant/components/envisalink.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Support for Envisalink devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/envisalink/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['pyenvisalink==1.0', 'pydispatcher==2.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'envisalink'
|
||||
|
||||
EVL_CONTROLLER = None
|
||||
|
||||
CONF_EVL_HOST = 'host'
|
||||
CONF_EVL_PORT = 'port'
|
||||
CONF_PANEL_TYPE = 'panel_type'
|
||||
CONF_EVL_VERSION = 'evl_version'
|
||||
CONF_CODE = 'code'
|
||||
CONF_USERNAME = 'user_name'
|
||||
CONF_PASS = 'password'
|
||||
CONF_EVL_KEEPALIVE = 'keepalive_interval'
|
||||
CONF_ZONEDUMP_INTERVAL = 'zonedump_interval'
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_PARTITIONS = 'partitions'
|
||||
|
||||
CONF_ZONENAME = 'name'
|
||||
CONF_ZONETYPE = 'type'
|
||||
CONF_PARTITIONNAME = 'name'
|
||||
|
||||
DEFAULT_PORT = 4025
|
||||
DEFAULT_EVL_VERSION = 3
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_ZONEDUMP_INTERVAL = 30
|
||||
DEFAULT_ZONETYPE = 'opening'
|
||||
|
||||
SIGNAL_ZONE_UPDATE = 'zones_updated'
|
||||
SIGNAL_PARTITION_UPDATE = 'partition_updated'
|
||||
SIGNAL_KEYPAD_UPDATE = 'keypad_updated'
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONENAME): cv.string,
|
||||
vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string})
|
||||
|
||||
PARTITION_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PARTITIONNAME): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_EVL_HOST): cv.string,
|
||||
vol.Required(CONF_PANEL_TYPE):
|
||||
vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASS): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
|
||||
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=3, max=4)),
|
||||
vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||
vol.Optional(CONF_ZONEDUMP_INTERVAL,
|
||||
default=DEFAULT_ZONEDUMP_INTERVAL):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, too-many-function-args, too-many-locals
|
||||
# pylint: disable=too-many-return-statements
|
||||
def setup(hass, base_config):
|
||||
"""Common setup for Envisalink devices."""
|
||||
from pyenvisalink import EnvisalinkAlarmPanel
|
||||
from pydispatch import dispatcher
|
||||
|
||||
global EVL_CONTROLLER
|
||||
|
||||
config = base_config.get(DOMAIN)
|
||||
|
||||
_host = config.get(CONF_EVL_HOST)
|
||||
_port = config.get(CONF_EVL_PORT)
|
||||
_code = config.get(CONF_CODE)
|
||||
_panel_type = config.get(CONF_PANEL_TYPE)
|
||||
_version = config.get(CONF_EVL_VERSION)
|
||||
_user = config.get(CONF_USERNAME)
|
||||
_pass = config.get(CONF_PASS)
|
||||
_keep_alive = config.get(CONF_EVL_KEEPALIVE)
|
||||
_zone_dump = config.get(CONF_ZONEDUMP_INTERVAL)
|
||||
_zones = config.get(CONF_ZONES)
|
||||
_partitions = config.get(CONF_PARTITIONS)
|
||||
_connect_status = {}
|
||||
EVL_CONTROLLER = EnvisalinkAlarmPanel(_host,
|
||||
_port,
|
||||
_panel_type,
|
||||
_version,
|
||||
_user,
|
||||
_pass,
|
||||
_zone_dump,
|
||||
_keep_alive)
|
||||
|
||||
def login_fail_callback(data):
|
||||
"""Callback for when the evl rejects our login."""
|
||||
_LOGGER.error("The envisalink rejected your credentials.")
|
||||
_connect_status['fail'] = 1
|
||||
|
||||
def connection_fail_callback(data):
|
||||
"""Network failure callback."""
|
||||
_LOGGER.error("Could not establish a connection with the envisalink.")
|
||||
_connect_status['fail'] = 1
|
||||
|
||||
def connection_success_callback(data):
|
||||
"""Callback for a successful connection."""
|
||||
_LOGGER.info("Established a connection with the envisalink.")
|
||||
_connect_status['success'] = 1
|
||||
|
||||
def zones_updated_callback(data):
|
||||
"""Handle zone timer updates."""
|
||||
_LOGGER.info("Envisalink sent a zone update event. Updating zones...")
|
||||
dispatcher.send(signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=None,
|
||||
zone=data)
|
||||
|
||||
def alarm_data_updated_callback(data):
|
||||
"""Handle non-alarm based info updates."""
|
||||
_LOGGER.info("Envisalink sent new alarm info. Updating alarms...")
|
||||
dispatcher.send(signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
|
||||
def partition_updated_callback(data):
|
||||
"""Handle partition changes thrown by evl (including alarms)."""
|
||||
_LOGGER.info("The envisalink sent a partition update event.")
|
||||
dispatcher.send(signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
|
||||
def stop_envisalink(event):
|
||||
"""Shutdown envisalink connection and thread on exit."""
|
||||
_LOGGER.info("Shutting down envisalink.")
|
||||
EVL_CONTROLLER.stop()
|
||||
|
||||
def start_envisalink(event):
|
||||
"""Startup process for the envisalink."""
|
||||
EVL_CONTROLLER.start()
|
||||
for _ in range(10):
|
||||
if 'success' in _connect_status:
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
|
||||
return True
|
||||
elif 'fail' in _connect_status:
|
||||
return False
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
_LOGGER.error("Timeout occurred while establishing evl connection.")
|
||||
return False
|
||||
|
||||
EVL_CONTROLLER.callback_zone_timer_dump = zones_updated_callback
|
||||
EVL_CONTROLLER.callback_zone_state_change = zones_updated_callback
|
||||
EVL_CONTROLLER.callback_partition_state_change = partition_updated_callback
|
||||
EVL_CONTROLLER.callback_keypad_update = alarm_data_updated_callback
|
||||
EVL_CONTROLLER.callback_login_failure = login_fail_callback
|
||||
EVL_CONTROLLER.callback_login_timeout = connection_fail_callback
|
||||
EVL_CONTROLLER.callback_login_success = connection_success_callback
|
||||
|
||||
_result = start_envisalink(None)
|
||||
if not _result:
|
||||
return False
|
||||
|
||||
# Load sub-components for envisalink
|
||||
if _partitions:
|
||||
load_platform(hass, 'alarm_control_panel', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
load_platform(hass, 'sensor', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
if _zones:
|
||||
load_platform(hass, 'binary_sensor', 'envisalink',
|
||||
{'zones': _zones}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkDevice(Entity):
|
||||
"""Representation of an envisalink devicetity."""
|
||||
|
||||
def __init__(self, name, info, controller):
|
||||
"""Initialize the device."""
|
||||
self._controller = controller
|
||||
self._info = info
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
@@ -1,3 +1,3 @@
|
||||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
||||
CORE = "7962327e4a29e51d4a6f4ee6cca9acc3"
|
||||
UI = "570e1b8744a58024fc4e256f5e024424"
|
||||
CORE = "db0bb387f4d3bcace002d62b94baa348"
|
||||
UI = "5b306b7e7d36799b7b67f592cbe94703"
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
BIN
homeassistant/components/frontend/www_static/images/smart-tv.png
Normal file
BIN
homeassistant/components/frontend/www_static/images/smart-tv.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
@@ -29,7 +29,7 @@
|
||||
|
||||
|
||||
/* eslint-disable quotes, comma-spacing */
|
||||
var PrecacheConfig = [["/","595e12c9755af231fd80191e4cc74d2e"],["/devEvent","595e12c9755af231fd80191e4cc74d2e"],["/devInfo","595e12c9755af231fd80191e4cc74d2e"],["/devService","595e12c9755af231fd80191e4cc74d2e"],["/devState","595e12c9755af231fd80191e4cc74d2e"],["/devTemplate","595e12c9755af231fd80191e4cc74d2e"],["/history","595e12c9755af231fd80191e4cc74d2e"],["/logbook","595e12c9755af231fd80191e4cc74d2e"],["/map","595e12c9755af231fd80191e4cc74d2e"],["/states","595e12c9755af231fd80191e4cc74d2e"],["/static/core-7962327e4a29e51d4a6f4ee6cca9acc3.js","9c07ffb3f81cfb74f8a051b80cc8f9f0"],["/static/frontend-570e1b8744a58024fc4e256f5e024424.html","595e12c9755af231fd80191e4cc74d2e"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
|
||||
var PrecacheConfig = [["/","70eeeca780a5f23c7632c2876dd1795a"],["/devEvent","70eeeca780a5f23c7632c2876dd1795a"],["/devInfo","70eeeca780a5f23c7632c2876dd1795a"],["/devService","70eeeca780a5f23c7632c2876dd1795a"],["/devState","70eeeca780a5f23c7632c2876dd1795a"],["/devTemplate","70eeeca780a5f23c7632c2876dd1795a"],["/history","70eeeca780a5f23c7632c2876dd1795a"],["/logbook","70eeeca780a5f23c7632c2876dd1795a"],["/map","70eeeca780a5f23c7632c2876dd1795a"],["/states","70eeeca780a5f23c7632c2876dd1795a"],["/static/core-db0bb387f4d3bcace002d62b94baa348.js","f938163a392465dc87af3a0094376621"],["/static/frontend-5b306b7e7d36799b7b67f592cbe94703.html","70eeeca780a5f23c7632c2876dd1795a"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
|
||||
/* eslint-enable quotes, comma-spacing */
|
||||
var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-';
|
||||
|
||||
|
Binary file not shown.
96
homeassistant/components/garage_door/rpi_gpio.py
Normal file
96
homeassistant/components/garage_door/rpi_gpio.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Support for building a Raspberry Pi garage controller in HA.
|
||||
|
||||
Instructions for building the controller can be found here
|
||||
https://github.com/andrewshilliday/garage-door-controller
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/garage_door.rpi_gpio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from time import sleep
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DOORS_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.Schema({
|
||||
'name': str,
|
||||
'relay_pin': int,
|
||||
'state_pin': int,
|
||||
})
|
||||
]
|
||||
)
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
'platform': str,
|
||||
vol.Required('doors'): _DOORS_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the garage door platform."""
|
||||
doors = []
|
||||
doors_conf = config.get('doors')
|
||||
|
||||
for door in doors_conf:
|
||||
doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'],
|
||||
door['state_pin']))
|
||||
add_devices(doors)
|
||||
|
||||
|
||||
class RPiGPIOGarageDoor(GarageDoorDevice):
|
||||
"""Representation of a Raspberry garage door."""
|
||||
|
||||
def __init__(self, name, relay_pin, state_pin):
|
||||
"""Initialize the garage door."""
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._relay_pin = relay_pin
|
||||
self._state_pin = state_pin
|
||||
rpi_gpio.setup_output(self._relay_pin)
|
||||
rpi_gpio.setup_input(self._state_pin, 'DOWN')
|
||||
rpi_gpio.write_output(self._relay_pin, True)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this garage door."""
|
||||
return "{}.{}".format(self.__class__, self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door if any."""
|
||||
return self._name
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the garage door."""
|
||||
self._state = rpi_gpio.read_input(self._state_pin) is True
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if door is closed."""
|
||||
return self._state
|
||||
|
||||
def _trigger(self):
|
||||
"""Trigger the door."""
|
||||
rpi_gpio.write_output(self._relay_pin, False)
|
||||
sleep(0.2)
|
||||
rpi_gpio.write_output(self._relay_pin, True)
|
||||
|
||||
def close_door(self):
|
||||
"""Close the door."""
|
||||
if not self.is_closed:
|
||||
self._trigger()
|
||||
|
||||
def open_door(self):
|
||||
"""Open the door."""
|
||||
if self.is_closed:
|
||||
self._trigger()
|
@@ -7,9 +7,10 @@ https://home-assistant.io/components/garage_door.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
pywink.get_garage_doors())
|
||||
|
||||
|
||||
class WinkGarageDoorDevice(GarageDoorDevice):
|
||||
class WinkGarageDoorDevice(WinkDevice, GarageDoorDevice):
|
||||
"""Representation of a Wink garage door."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the garage door."""
|
||||
self.wink = wink
|
||||
self._battery = self.wink.battery_level
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this wink garage door."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the garage door."""
|
||||
self.wink.update_state()
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if door is closed."""
|
||||
return self.wink.state() == 0
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def close_door(self):
|
||||
"""Close the door."""
|
||||
self.wink.set_state(0)
|
||||
@@ -70,16 +51,3 @@ class WinkGarageDoorDevice(GarageDoorDevice):
|
||||
def open_door(self):
|
||||
"""Open the door."""
|
||||
self.wink.set_state(1)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
70
homeassistant/components/garage_door/zwave.py
Normal file
70
homeassistant/components/garage_door/zwave.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Support for Zwave garage door components.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/garagedoor.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.garage_door import DOMAIN
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
|
||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave garage door device."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY:
|
||||
return
|
||||
if value.type != zwave.TYPE_BOOL:
|
||||
return
|
||||
if value.genre != zwave.GENRE_USER:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
|
||||
|
||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
|
||||
"""Representation of an Zwave garage door device."""
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave garage door."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._node = value.node
|
||||
self._state = value.data
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self._state = value.data
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of Zwave garage door."""
|
||||
return not self._state
|
||||
|
||||
def close_door(self):
|
||||
"""Close the garage door."""
|
||||
self._value.node.set_switch(self._value.value_id, False)
|
||||
|
||||
def open_door(self):
|
||||
"""Open the garage door."""
|
||||
self._value.node.set_switch(self._value.value_id, True)
|
122
homeassistant/components/hdmi_cec.py
Normal file
122
homeassistant/components/hdmi_cec.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
CEC component.
|
||||
|
||||
Requires libcec + Python bindings.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_CEC = None
|
||||
DOMAIN = 'hdmi_cec'
|
||||
SERVICE_SELECT_DEVICE = 'select_device'
|
||||
SERVICE_POWER_ON = 'power_on'
|
||||
SERVICE_STANDBY = 'standby'
|
||||
CONF_DEVICES = 'devices'
|
||||
ATTR_DEVICE = 'device'
|
||||
MAX_DEPTH = 4
|
||||
|
||||
|
||||
# pylint: disable=unnecessary-lambda
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.All(cv.positive_int): vol.Any(lambda devices: DEVICE_SCHEMA(devices),
|
||||
cv.string)
|
||||
})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICES): DEVICE_SCHEMA
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def parse_mapping(mapping, parents=None):
|
||||
"""Parse configuration device mapping."""
|
||||
if parents is None:
|
||||
parents = []
|
||||
for addr, val in mapping.items():
|
||||
cur = parents + [str(addr)]
|
||||
if isinstance(val, dict):
|
||||
yield from parse_mapping(val, cur)
|
||||
elif isinstance(val, str):
|
||||
yield (val, cur)
|
||||
|
||||
|
||||
def pad_physical_address(addr):
|
||||
"""Right-pad a physical address."""
|
||||
return addr + ['0'] * (MAX_DEPTH - len(addr))
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup CEC capability."""
|
||||
global _CEC
|
||||
|
||||
# cec is only available if libcec is properly installed
|
||||
# and the Python bindings are accessible.
|
||||
try:
|
||||
import cec
|
||||
except ImportError:
|
||||
_LOGGER.error("libcec must be installed")
|
||||
return False
|
||||
|
||||
# Parse configuration into a dict of device name
|
||||
# to physical address represented as a list of
|
||||
# four elements.
|
||||
flat = {}
|
||||
for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})):
|
||||
flat[pair[0]] = pad_physical_address(pair[1])
|
||||
|
||||
# Configure libcec.
|
||||
cfg = cec.libcec_configuration()
|
||||
cfg.strDeviceName = 'HASS'
|
||||
cfg.bActivateSource = 0
|
||||
cfg.bMonitorOnly = 1
|
||||
cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT
|
||||
|
||||
# Set up CEC adapter.
|
||||
_CEC = cec.ICECAdapter.Create(cfg)
|
||||
|
||||
def _power_on(call):
|
||||
"""Power on all devices."""
|
||||
_CEC.PowerOnDevices()
|
||||
|
||||
def _standby(call):
|
||||
"""Standby all devices."""
|
||||
_CEC.StandbyDevices()
|
||||
|
||||
def _select_device(call):
|
||||
"""Select the active device."""
|
||||
path = flat.get(call.data[ATTR_DEVICE])
|
||||
if not path:
|
||||
_LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE])
|
||||
cmds = []
|
||||
for i in range(1, MAX_DEPTH - 1):
|
||||
addr = pad_physical_address(path[:i])
|
||||
cmds.append('1f:82:{}{}:{}{}'.format(*addr))
|
||||
cmds.append('1f:86:{}{}:{}{}'.format(*addr))
|
||||
for cmd in cmds:
|
||||
_CEC.Transmit(_CEC.CommandFromString(cmd))
|
||||
_LOGGER.info("Selected %s", call.data[ATTR_DEVICE])
|
||||
|
||||
def _start_cec(event):
|
||||
"""Open CEC adapter."""
|
||||
adapters = _CEC.DetectAdapters()
|
||||
if len(adapters) == 0:
|
||||
_LOGGER.error("No CEC adapter found")
|
||||
return
|
||||
|
||||
if _CEC.Open(adapters[0].strComName):
|
||||
hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on)
|
||||
hass.services.register(DOMAIN, SERVICE_STANDBY, _standby)
|
||||
hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE,
|
||||
_select_device)
|
||||
else:
|
||||
_LOGGER.error("Failed to open adapter")
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec)
|
||||
return True
|
534
homeassistant/components/homematic.py
Normal file
534
homeassistant/components/homematic.py
Normal file
@@ -0,0 +1,534 @@
|
||||
"""
|
||||
Support for Homematic devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/homematic/
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from functools import partial
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ['pyhomematic==0.1.8']
|
||||
|
||||
HOMEMATIC = None
|
||||
HOMEMATIC_LINK_DELAY = 0.5
|
||||
|
||||
DISCOVER_SWITCHES = "homematic.switch"
|
||||
DISCOVER_LIGHTS = "homematic.light"
|
||||
DISCOVER_SENSORS = "homematic.sensor"
|
||||
DISCOVER_BINARY_SENSORS = "homematic.binary_sensor"
|
||||
DISCOVER_ROLLERSHUTTER = "homematic.rollershutter"
|
||||
DISCOVER_THERMOSTATS = "homematic.thermostat"
|
||||
|
||||
ATTR_DISCOVER_DEVICES = "devices"
|
||||
ATTR_PARAM = "param"
|
||||
ATTR_CHANNEL = "channel"
|
||||
ATTR_NAME = "name"
|
||||
ATTR_ADDRESS = "address"
|
||||
|
||||
EVENT_KEYPRESS = "homematic.keypress"
|
||||
|
||||
HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"],
|
||||
DISCOVER_LIGHTS: ["Dimmer"],
|
||||
DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2",
|
||||
"RemoteMotion", "ThermostatWall", "AreaThermostat",
|
||||
"RotaryHandleSensor", "WaterSensor"],
|
||||
DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"],
|
||||
DISCOVER_BINARY_SENSORS: ["ShutterContact", "Smoke", "SmokeV2",
|
||||
"Motion", "MotionV2", "RemoteMotion"],
|
||||
DISCOVER_ROLLERSHUTTER: ["Blind"]
|
||||
}
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE = [
|
||||
"ACTUAL_TEMPERATURE"
|
||||
]
|
||||
|
||||
HM_ATTRIBUTE_SUPPORT = {
|
||||
"LOWBAT": ["Battery", {0: "High", 1: "Low"}],
|
||||
"ERROR": ["Sabotage", {0: "No", 1: "Yes"}],
|
||||
"RSSI_DEVICE": ["RSSI", {}],
|
||||
"VALVE_STATE": ["Valve", {}],
|
||||
"BATTERY_STATE": ["Battery", {}],
|
||||
"CONTROL_MODE": ["Mode", {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost"}],
|
||||
"POWER": ["Power", {}],
|
||||
"CURRENT": ["Current", {}],
|
||||
"VOLTAGE": ["Voltage", {}]
|
||||
}
|
||||
|
||||
HM_PRESS_EVENTS = [
|
||||
"PRESS_SHORT",
|
||||
"PRESS_LONG",
|
||||
"PRESS_CONT",
|
||||
"PRESS_LONG_RELEASE"
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Setup the Homematic component."""
|
||||
global HOMEMATIC, HOMEMATIC_LINK_DELAY
|
||||
|
||||
from pyhomematic import HMConnection
|
||||
|
||||
local_ip = config[DOMAIN].get("local_ip", None)
|
||||
local_port = config[DOMAIN].get("local_port", 8943)
|
||||
remote_ip = config[DOMAIN].get("remote_ip", None)
|
||||
remote_port = config[DOMAIN].get("remote_port", 2001)
|
||||
resolvenames = config[DOMAIN].get("resolvenames", False)
|
||||
username = config[DOMAIN].get("username", "Admin")
|
||||
password = config[DOMAIN].get("password", "")
|
||||
HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5)
|
||||
|
||||
if remote_ip is None or local_ip is None:
|
||||
_LOGGER.error("Missing remote CCU/Homegear or local address")
|
||||
return False
|
||||
|
||||
# Create server thread
|
||||
bound_system_callback = partial(system_callback_handler, hass, config)
|
||||
HOMEMATIC = HMConnection(local=local_ip,
|
||||
localport=local_port,
|
||||
remote=remote_ip,
|
||||
remoteport=remote_port,
|
||||
systemcallback=bound_system_callback,
|
||||
resolvenames=resolvenames,
|
||||
rpcusername=username,
|
||||
rpcpassword=password,
|
||||
interface_id="homeassistant")
|
||||
|
||||
# Start server thread, connect to peer, initialize to receive events
|
||||
HOMEMATIC.start()
|
||||
|
||||
# Stops server when Homeassistant is shutting down
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop)
|
||||
hass.config.components.append(DOMAIN)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def system_callback_handler(hass, config, src, *args):
|
||||
"""Callback handler."""
|
||||
if src == 'newDevices':
|
||||
_LOGGER.debug("newDevices with: %s", str(args))
|
||||
# pylint: disable=unused-variable
|
||||
(interface_id, dev_descriptions) = args
|
||||
key_dict = {}
|
||||
# Get list of all keys of the devices (ignoring channels)
|
||||
for dev in dev_descriptions:
|
||||
key_dict[dev['ADDRESS'].split(':')[0]] = True
|
||||
|
||||
# Register EVENTS
|
||||
# Search all device with a EVENTNODE that include data
|
||||
bound_event_callback = partial(_hm_event_handler, hass)
|
||||
for dev in key_dict:
|
||||
if dev not in HOMEMATIC.devices:
|
||||
continue
|
||||
|
||||
hmdevice = HOMEMATIC.devices.get(dev)
|
||||
# have events?
|
||||
if len(hmdevice.EVENTNODE) > 0:
|
||||
_LOGGER.debug("Register Events from %s", dev)
|
||||
hmdevice.setEventCallback(callback=bound_event_callback,
|
||||
bequeath=True)
|
||||
|
||||
# If configuration allows autodetection of devices,
|
||||
# all devices not configured are added.
|
||||
if key_dict:
|
||||
for component_name, discovery_type in (
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
('light', DISCOVER_LIGHTS),
|
||||
('rollershutter', DISCOVER_ROLLERSHUTTER),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS),
|
||||
('sensor', DISCOVER_SENSORS),
|
||||
('thermostat', DISCOVER_THERMOSTATS)):
|
||||
# Get all devices of a specific type
|
||||
found_devices = _get_devices(discovery_type, key_dict)
|
||||
|
||||
# When devices of this type are found
|
||||
# they are setup in HA and an event is fired
|
||||
if found_devices:
|
||||
# Fire discovery event
|
||||
discovery.load_platform(hass, component_name, DOMAIN, {
|
||||
ATTR_DISCOVER_DEVICES: found_devices
|
||||
}, config)
|
||||
|
||||
|
||||
def _get_devices(device_type, keys):
|
||||
"""Get the Homematic devices."""
|
||||
# run
|
||||
device_arr = []
|
||||
for key in keys:
|
||||
device = HOMEMATIC.devices[key]
|
||||
class_name = device.__class__.__name__
|
||||
metadata = {}
|
||||
|
||||
# is class supported by discovery type
|
||||
if class_name not in HM_DEVICE_TYPES[device_type]:
|
||||
continue
|
||||
|
||||
# Load metadata if needed to generate a param list
|
||||
if device_type == DISCOVER_SENSORS:
|
||||
metadata.update(device.SENSORNODE)
|
||||
elif device_type == DISCOVER_BINARY_SENSORS:
|
||||
metadata.update(device.BINARYNODE)
|
||||
|
||||
params = _create_params_list(device, metadata, device_type)
|
||||
if params:
|
||||
# Generate options for 1...n elements with 1...n params
|
||||
for channel in range(1, device.ELEMENT + 1):
|
||||
_LOGGER.debug("Handling %s:%i", key, channel)
|
||||
if channel in params:
|
||||
for param in params[channel]:
|
||||
name = _create_ha_name(name=device.NAME,
|
||||
channel=channel,
|
||||
param=param)
|
||||
device_dict = dict(platform="homematic",
|
||||
address=key,
|
||||
name=name,
|
||||
channel=channel)
|
||||
if param is not None:
|
||||
device_dict[ATTR_PARAM] = param
|
||||
|
||||
# Add new device
|
||||
device_arr.append(device_dict)
|
||||
else:
|
||||
_LOGGER.debug("Channel %i not in params", channel)
|
||||
else:
|
||||
_LOGGER.debug("Got no params for %s", key)
|
||||
_LOGGER.debug("%s autodiscovery: %s",
|
||||
device_type, str(device_arr))
|
||||
return device_arr
|
||||
|
||||
|
||||
def _create_params_list(hmdevice, metadata, device_type):
|
||||
"""Create a list from HMDevice with all possible parameters in config."""
|
||||
params = {}
|
||||
merge = False
|
||||
|
||||
# use merge?
|
||||
if device_type == DISCOVER_SENSORS:
|
||||
merge = True
|
||||
elif device_type == DISCOVER_BINARY_SENSORS:
|
||||
merge = True
|
||||
|
||||
# Search in sensor and binary metadata per elements
|
||||
for channel in range(1, hmdevice.ELEMENT + 1):
|
||||
param_chan = []
|
||||
for node, meta_chan in metadata.items():
|
||||
try:
|
||||
# Is this attribute ignored?
|
||||
if node in HM_IGNORE_DISCOVERY_NODE:
|
||||
continue
|
||||
if meta_chan == 'c' or meta_chan is None:
|
||||
# Only channel linked data
|
||||
param_chan.append(node)
|
||||
elif channel == 1:
|
||||
# First channel can have other data channel
|
||||
param_chan.append(node)
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.error("Exception generating %s (%s)",
|
||||
hmdevice.ADDRESS, str(metadata))
|
||||
|
||||
# default parameter is merge is off
|
||||
if len(param_chan) == 0 and not merge:
|
||||
param_chan.append(None)
|
||||
|
||||
# Add to channel
|
||||
if len(param_chan) > 0:
|
||||
params.update({channel: param_chan})
|
||||
|
||||
_LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS,
|
||||
str(params))
|
||||
return params
|
||||
|
||||
|
||||
def _create_ha_name(name, channel, param):
|
||||
"""Generate a unique object name."""
|
||||
# HMDevice is a simple device
|
||||
if channel == 1 and param is None:
|
||||
return name
|
||||
|
||||
# Has multiple elements/channels
|
||||
if channel > 1 and param is None:
|
||||
return "{} {}".format(name, channel)
|
||||
|
||||
# With multiple param first elements
|
||||
if channel == 1 and param is not None:
|
||||
return "{} {}".format(name, param)
|
||||
|
||||
# Multiple param on object with multiple elements
|
||||
if channel > 1 and param is not None:
|
||||
return "{} {} {}".format(name, channel, param)
|
||||
|
||||
|
||||
def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info,
|
||||
add_callback_devices):
|
||||
"""Helper to setup Homematic devices with discovery info."""
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
_LOGGER.debug("Add device %s from config: %s",
|
||||
str(hmdevicetype), str(config))
|
||||
|
||||
# create object and add to HA
|
||||
new_device = hmdevicetype(config)
|
||||
add_callback_devices([new_device])
|
||||
|
||||
# link to HM
|
||||
new_device.link_homematic()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _hm_event_handler(hass, device, caller, attribute, value):
|
||||
"""Handle all pyhomematic device events."""
|
||||
try:
|
||||
channel = int(device.split(":")[1])
|
||||
address = device.split(":")[0]
|
||||
hmdevice = HOMEMATIC.devices.get(address)
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.error("Event handling channel convert error!")
|
||||
return
|
||||
|
||||
# is not a event?
|
||||
if attribute not in hmdevice.EVENTNODE:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Event %s for %s channel %i", attribute,
|
||||
hmdevice.NAME, channel)
|
||||
|
||||
# a keypress event
|
||||
if attribute in HM_PRESS_EVENTS:
|
||||
hass.bus.fire(EVENT_KEYPRESS, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
ATTR_PARAM: attribute,
|
||||
ATTR_CHANNEL: channel
|
||||
})
|
||||
return
|
||||
|
||||
_LOGGER.warning("Event is unknown and not forwarded to HA")
|
||||
|
||||
|
||||
class HMDevice(Entity):
|
||||
"""The Homematic device base object."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, config):
|
||||
"""Initialize a generic Homematic device."""
|
||||
self._name = config.get(ATTR_NAME, None)
|
||||
self._address = config.get(ATTR_ADDRESS, None)
|
||||
self._channel = config.get(ATTR_CHANNEL, 1)
|
||||
self._state = config.get(ATTR_PARAM, None)
|
||||
self._data = {}
|
||||
self._hmdevice = None
|
||||
self._connected = False
|
||||
self._available = False
|
||||
|
||||
# Set param to uppercase
|
||||
if self._state:
|
||||
self._state = self._state.upper()
|
||||
|
||||
# Generate name
|
||||
if not self._name:
|
||||
self._name = _create_ha_name(name=self._address,
|
||||
channel=self._channel,
|
||||
param=self._state)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. Homematic states are pushed by the XML RPC Server."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return true if unable to access real state of the device."""
|
||||
return not self._available
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true if device is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {}
|
||||
|
||||
# no data available to create
|
||||
if not self.available:
|
||||
return attr
|
||||
|
||||
# Generate an attributes list
|
||||
for node, data in HM_ATTRIBUTE_SUPPORT.items():
|
||||
# Is an attributes and exists for this object
|
||||
if node in self._data:
|
||||
value = data[1].get(self._data[node], self._data[node])
|
||||
attr[data[0]] = value
|
||||
|
||||
# static attributes
|
||||
attr["ID"] = self._hmdevice.ADDRESS
|
||||
|
||||
return attr
|
||||
|
||||
def link_homematic(self):
|
||||
"""Connect to Homematic."""
|
||||
# device is already linked
|
||||
if self._connected:
|
||||
return True
|
||||
|
||||
# Does a HMDevice from pyhomematic exist?
|
||||
if self._address in HOMEMATIC.devices:
|
||||
# Init
|
||||
self._hmdevice = HOMEMATIC.devices[self._address]
|
||||
self._connected = True
|
||||
|
||||
# Check if Homematic class is okay for HA class
|
||||
_LOGGER.info("Start linking %s to %s", self._address, self._name)
|
||||
if self._check_hm_to_ha_object():
|
||||
try:
|
||||
# Init datapoints of this object
|
||||
self._init_data_struct()
|
||||
if HOMEMATIC_LINK_DELAY:
|
||||
# We delay / pause loading of data to avoid overloading
|
||||
# of CCU / Homegear when doing auto detection
|
||||
time.sleep(HOMEMATIC_LINK_DELAY)
|
||||
self._load_init_data_from_hm()
|
||||
_LOGGER.debug("%s datastruct: %s",
|
||||
self._name, str(self._data))
|
||||
|
||||
# Link events from pyhomatic
|
||||
self._subscribe_homematic_events()
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
self._connected = False
|
||||
_LOGGER.error("Exception while linking %s: %s",
|
||||
self._address, str(err))
|
||||
else:
|
||||
_LOGGER.critical("Delink %s object from HM", self._name)
|
||||
self._connected = False
|
||||
|
||||
# Update HA
|
||||
_LOGGER.debug("%s linking done, send update_ha_state", self._name)
|
||||
self.update_ha_state()
|
||||
else:
|
||||
_LOGGER.debug("%s not found in HOMEMATIC.devices", self._address)
|
||||
|
||||
def _hm_event_callback(self, device, caller, attribute, value):
|
||||
"""Handle all pyhomematic device events."""
|
||||
_LOGGER.debug("%s received event '%s' value: %s", self._name,
|
||||
attribute, value)
|
||||
have_change = False
|
||||
|
||||
# Is data needed for this instance?
|
||||
if attribute in self._data:
|
||||
# Did data change?
|
||||
if self._data[attribute] != value:
|
||||
self._data[attribute] = value
|
||||
have_change = True
|
||||
|
||||
# If available it has changed
|
||||
if attribute is "UNREACH":
|
||||
self._available = bool(value)
|
||||
have_change = True
|
||||
|
||||
# If it has changed data point, update HA
|
||||
if have_change:
|
||||
_LOGGER.debug("%s update_ha_state after '%s'", self._name,
|
||||
attribute)
|
||||
self.update_ha_state()
|
||||
|
||||
def _subscribe_homematic_events(self):
|
||||
"""Subscribe all required events to handle job."""
|
||||
channels_to_sub = {}
|
||||
|
||||
# Push data to channels_to_sub from hmdevice metadata
|
||||
for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE,
|
||||
self._hmdevice.ATTRIBUTENODE,
|
||||
self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE,
|
||||
self._hmdevice.ACTIONNODE):
|
||||
for node, channel in metadata.items():
|
||||
# Data is needed for this instance
|
||||
if node in self._data:
|
||||
# chan is current channel
|
||||
if channel == 'c' or channel is None:
|
||||
channel = self._channel
|
||||
# Prepare for subscription
|
||||
try:
|
||||
if int(channel) > 0:
|
||||
channels_to_sub.update({int(channel): True})
|
||||
except (ValueError, TypeError):
|
||||
_LOGGER("Invalid channel in metadata from %s",
|
||||
self._name)
|
||||
|
||||
# Set callbacks
|
||||
for channel in channels_to_sub:
|
||||
_LOGGER.debug("Subscribe channel %s from %s",
|
||||
str(channel), self._name)
|
||||
self._hmdevice.setEventCallback(callback=self._hm_event_callback,
|
||||
bequeath=False,
|
||||
channel=channel)
|
||||
|
||||
def _load_init_data_from_hm(self):
|
||||
"""Load first value from pyhomematic."""
|
||||
if not self._connected:
|
||||
return False
|
||||
|
||||
# Read data from pyhomematic
|
||||
for metadata, funct in (
|
||||
(self._hmdevice.ATTRIBUTENODE,
|
||||
self._hmdevice.getAttributeData),
|
||||
(self._hmdevice.WRITENODE, self._hmdevice.getWriteData),
|
||||
(self._hmdevice.SENSORNODE, self._hmdevice.getSensorData),
|
||||
(self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)):
|
||||
for node in metadata:
|
||||
if node in self._data:
|
||||
self._data[node] = funct(name=node, channel=self._channel)
|
||||
|
||||
return True
|
||||
|
||||
def _hm_set_state(self, value):
|
||||
"""Set data to main datapoint."""
|
||||
if self._state in self._data:
|
||||
self._data[self._state] = value
|
||||
|
||||
def _hm_get_state(self):
|
||||
"""Get data from main datapoint."""
|
||||
if self._state in self._data:
|
||||
return self._data[self._state]
|
||||
return None
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if it is possible to use the Homematic object as this HA type.
|
||||
|
||||
NEEDS overwrite by inherit!
|
||||
"""
|
||||
if not self._connected or self._hmdevice is None:
|
||||
_LOGGER.error("HA object is not linked to homematic.")
|
||||
return False
|
||||
|
||||
# Check if button option is correctly set for this object
|
||||
if self._channel > self._hmdevice.ELEMENT:
|
||||
_LOGGER.critical("Button option is not correct for this object!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata.
|
||||
|
||||
NEEDS overwrite by inherit!
|
||||
"""
|
||||
# Add all attributes to data dict
|
||||
for data_note in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data.update({data_note: STATE_UNKNOWN})
|
@@ -1,25 +1,31 @@
|
||||
"""This module provides WSGI application to serve the Home Assistant API."""
|
||||
"""
|
||||
This module provides WSGI application to serve the Home Assistant API.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/http/
|
||||
"""
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import threading
|
||||
import re
|
||||
import ssl
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant import util
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS)
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = "http"
|
||||
REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5",)
|
||||
REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10")
|
||||
|
||||
CONF_API_PASSWORD = "api_password"
|
||||
CONF_SERVER_HOST = "server_host"
|
||||
@@ -31,6 +37,25 @@ CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
|
||||
# TLS configuation follows the best-practice guidelines
|
||||
# specified here: https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
# Intermediate guidelines are followed.
|
||||
SSL_VERSION = ssl.PROTOCOL_SSLv23
|
||||
SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_COMPRESSION
|
||||
CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \
|
||||
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \
|
||||
"DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \
|
||||
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \
|
||||
"ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \
|
||||
"ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \
|
||||
"ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \
|
||||
"DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \
|
||||
"DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \
|
||||
"ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \
|
||||
"AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \
|
||||
"AES256-SHA:DES-CBC3-SHA:!DSS"
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -93,11 +118,17 @@ def setup(hass, config):
|
||||
cors_origins=cors_origins
|
||||
)
|
||||
|
||||
hass.bus.listen_once(
|
||||
ha.EVENT_HOMEASSISTANT_START,
|
||||
lambda event:
|
||||
threading.Thread(target=server.start, daemon=True,
|
||||
name='WSGI-server').start())
|
||||
def start_wsgi_server(event):
|
||||
"""Start the WSGI server."""
|
||||
server.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server)
|
||||
|
||||
def stop_wsgi_server(event):
|
||||
"""Stop the WSGI server."""
|
||||
server.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server)
|
||||
|
||||
hass.wsgi = server
|
||||
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
|
||||
@@ -216,6 +247,7 @@ class HomeAssistantWSGI(object):
|
||||
self.server_port = server_port
|
||||
self.cors_origins = cors_origins
|
||||
self.event_forwarder = None
|
||||
self.server = None
|
||||
|
||||
def register_view(self, view):
|
||||
"""Register a view with the WSGI server.
|
||||
@@ -283,14 +315,34 @@ class HomeAssistantWSGI(object):
|
||||
|
||||
def start(self):
|
||||
"""Start the wsgi server."""
|
||||
from eventlet import wsgi
|
||||
import eventlet
|
||||
from cherrypy import wsgiserver
|
||||
from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter
|
||||
|
||||
# pylint: disable=too-few-public-methods,super-init-not-called
|
||||
class ContextSSLAdapter(BuiltinSSLAdapter):
|
||||
"""SSL Adapter that takes in an SSL context."""
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
# pylint: disable=no-member
|
||||
self.server = wsgiserver.CherryPyWSGIServer(
|
||||
(self.server_host, self.server_port), self,
|
||||
server_name='Home Assistant')
|
||||
|
||||
sock = eventlet.listen((self.server_host, self.server_port))
|
||||
if self.ssl_certificate:
|
||||
sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate,
|
||||
keyfile=self.ssl_key, server_side=True)
|
||||
wsgi.server(sock, self, log=_LOGGER)
|
||||
context = ssl.SSLContext(SSL_VERSION)
|
||||
context.options |= SSL_OPTS
|
||||
context.set_ciphers(CIPHERS)
|
||||
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
|
||||
self.server.ssl_adapter = ContextSSLAdapter(context)
|
||||
|
||||
threading.Thread(target=self.server.start, daemon=True,
|
||||
name='WSGI-server').start()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the wsgi server."""
|
||||
self.server.stop()
|
||||
|
||||
def dispatch_request(self, request):
|
||||
"""Handle incoming request."""
|
||||
@@ -337,6 +389,10 @@ class HomeAssistantWSGI(object):
|
||||
"""Handle a request for base app + extra apps."""
|
||||
from werkzeug.wsgi import DispatcherMiddleware
|
||||
|
||||
if not self.hass.is_running:
|
||||
from werkzeug.exceptions import BadRequest
|
||||
return BadRequest()(environ, start_response)
|
||||
|
||||
app = DispatcherMiddleware(self.base_app, self.extra_apps)
|
||||
# Strip out any cachebusting MD5 fingerprints
|
||||
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
|
||||
@@ -395,7 +451,12 @@ class HomeAssistantView(object):
|
||||
self.hass.wsgi.api_password):
|
||||
authenticated = True
|
||||
|
||||
if self.requires_auth and not authenticated:
|
||||
if authenticated:
|
||||
_LOGGER.info('Successful login/request from %s',
|
||||
request.remote_addr)
|
||||
elif self.requires_auth and not authenticated:
|
||||
_LOGGER.warning('Login attempt or request with an invalid'
|
||||
'password from %s', request.remote_addr)
|
||||
raise Unauthorized()
|
||||
|
||||
request.authenticated = authenticated
|
||||
@@ -437,7 +498,7 @@ class HomeAssistantView(object):
|
||||
mimetype = mimetypes.guess_type(fil)[0]
|
||||
|
||||
try:
|
||||
fil = open(fil)
|
||||
fil = open(fil, mode='br')
|
||||
except IOError:
|
||||
raise NotFound()
|
||||
|
||||
|
@@ -425,39 +425,39 @@ class HvacDevice(Entity):
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set new target humidity."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
@@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-arguments, abstract-method
|
||||
class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
"""Represents a HeatControl hvac."""
|
||||
|
||||
@@ -98,7 +98,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.node == value.node:
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_properties()
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
@@ -211,6 +211,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
value.data = int(round(temperature, 0))
|
||||
else:
|
||||
value.data = int(temperature)
|
||||
break
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
@@ -218,6 +219,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
if value.command_class == 68 and value.index == 0:
|
||||
value.data = bytes(fan, 'utf-8')
|
||||
break
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
@@ -225,6 +227,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
if value.command_class == 64 and value.index == 0:
|
||||
value.data = bytes(operation_mode, 'utf-8')
|
||||
break
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
@@ -233,3 +236,4 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
value.data = int(swing_mode)
|
||||
break
|
||||
|
@@ -23,7 +23,7 @@ DEFAULT_DATABASE = 'home_assistant'
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
|
||||
REQUIREMENTS = ['influxdb==2.12.0']
|
||||
REQUIREMENTS = ['influxdb==3.0.0']
|
||||
|
||||
CONF_HOST = 'host'
|
||||
CONF_PORT = 'port'
|
||||
|
@@ -248,7 +248,8 @@ def setup(hass, config):
|
||||
class Light(ToggleEntity):
|
||||
"""Representation of a light."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
# pylint: disable=no-self-use, abstract-method
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
|
@@ -4,7 +4,6 @@ Support for EnOcean light sources.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.enocean/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
@@ -86,7 +85,7 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
|
||||
self._on_state = False
|
||||
|
||||
def value_changed(self, val):
|
||||
"""Update the internal state of this device in HA."""
|
||||
"""Update the internal state of this device."""
|
||||
self._brightness = math.floor(val / 100.0 * 256.0)
|
||||
self._on_state = bool(val != 0)
|
||||
self.update_ha_state()
|
||||
|
102
homeassistant/components/light/homematic.py
Normal file
102
homeassistant/components/light/homematic.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Support for Homematic lighs.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.homematic/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.light import (ATTR_BRIGHTNESS, Light)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the Homematic light platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
return homematic.setup_hmdevice_discovery_helper(HMLight,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
class HMLight(homematic.HMDevice, Light):
|
||||
"""Representation of a Homematic light."""
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
if not self.available:
|
||||
return None
|
||||
# Is dimmer?
|
||||
if self._state is "LEVEL":
|
||||
return int(self._hm_get_state() * 255)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
try:
|
||||
return self._hm_get_state() > 0
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if not self.available:
|
||||
return
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs and self._state is "LEVEL":
|
||||
percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255
|
||||
self._hmdevice.set_level(percent_bright, self._channel)
|
||||
else:
|
||||
self._hmdevice.on(self._channel)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
if self.available:
|
||||
self._hmdevice.off(self._channel)
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the Homematic object as this HA type."""
|
||||
from pyhomematic.devicetypes.actors import Dimmer, Switch
|
||||
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
# Check if the Homematic device is correct for this HA device
|
||||
if isinstance(self._hmdevice, Switch):
|
||||
return True
|
||||
if isinstance(self._hmdevice, Dimmer):
|
||||
return True
|
||||
|
||||
_LOGGER.critical("This %s can't be use as light", self._name)
|
||||
return False
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
from pyhomematic.devicetypes.actors import Dimmer, Switch
|
||||
|
||||
super()._init_data_struct()
|
||||
|
||||
# Use STATE
|
||||
if isinstance(self._hmdevice, Switch):
|
||||
self._state = "STATE"
|
||||
|
||||
# Use LEVEL
|
||||
if isinstance(self._hmdevice, Dimmer):
|
||||
self._state = "LEVEL"
|
||||
|
||||
# Add state to data dict
|
||||
if self._state:
|
||||
_LOGGER.debug("%s init datadict with main node '%s'", self._name,
|
||||
self._state)
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
else:
|
||||
_LOGGER.critical("Can't correctly init light %s.", self._name)
|
@@ -4,6 +4,7 @@ Support for LimitlessLED bulbs.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.limitlessled/
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
|
@@ -4,6 +4,7 @@ Support for MySensors lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.mysensors/
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
|
@@ -1,19 +1,9 @@
|
||||
"""
|
||||
Support for Osram Lightify.
|
||||
|
||||
Uses: https://github.com/aneumeier/python-lightify for the Osram light
|
||||
interface.
|
||||
|
||||
In order to use the platform just add the following to the configuration.yaml:
|
||||
|
||||
light:
|
||||
platform: osramlightify
|
||||
host: <hostname_or_ip>
|
||||
|
||||
Todo:
|
||||
Add support for Non RGBW lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.osramlightify/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from datetime import timedelta
|
||||
@@ -40,7 +30,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Find and return lights."""
|
||||
"""Setup Osram Lightify lights."""
|
||||
import lightify
|
||||
host = config.get(CONF_HOST)
|
||||
if host:
|
||||
@@ -85,7 +75,7 @@ def setup_bridge(bridge, add_devices_callback):
|
||||
|
||||
|
||||
class OsramLightifyLight(Light):
|
||||
"""Defines an Osram Lightify Light."""
|
||||
"""Representation of an Osram Lightify Light."""
|
||||
|
||||
def __init__(self, light_id, light, update_lights):
|
||||
"""Initialize the light."""
|
||||
|
@@ -8,12 +8,13 @@ import logging
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||
Light, ATTR_RGB_COLOR
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.util import color as color_util
|
||||
from homeassistant.util.color import \
|
||||
color_temperature_mired_to_kelvin as mired_to_kelvin
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
@@ -35,26 +36,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
WinkLight(light) for light in pywink.get_bulbs())
|
||||
|
||||
|
||||
class WinkLight(Light):
|
||||
class WinkLight(WinkDevice, Light):
|
||||
"""Representation of a Wink light."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""
|
||||
Initialize the light.
|
||||
|
||||
:type wink: pywink.devices.standard.bulb.WinkBulb
|
||||
"""
|
||||
self.wink = wink
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this Wink light."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the light if any."""
|
||||
return self.wink.name()
|
||||
"""Initialize the Wink device."""
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -66,11 +53,6 @@ class WinkLight(Light):
|
||||
"""Return the brightness of the light."""
|
||||
return int(self.wink.brightness() * 255)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
@property
|
||||
def xy_color(self):
|
||||
"""Current bulb color in CIE 1931 (XY) color space."""
|
||||
@@ -112,7 +94,3 @@ class WinkLight(Light):
|
||||
def turn_off(self):
|
||||
"""Turn the switch off."""
|
||||
self.wink.set_state(False)
|
||||
|
||||
def update(self):
|
||||
"""Update state of the light."""
|
||||
self.wink.update_state(require_desired_state_fulfilled=True)
|
||||
|
@@ -4,13 +4,31 @@ Support for Z-Wave lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.zwave/
|
||||
"""
|
||||
import logging
|
||||
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
from threading import Timer
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN, Light
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||
ATTR_RGB_COLOR, DOMAIN, Light
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
|
||||
color_temperature_mired_to_kelvin, color_temperature_to_rgb
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
COLOR_CHANNEL_WARM_WHITE = 0x01
|
||||
COLOR_CHANNEL_COLD_WHITE = 0x02
|
||||
COLOR_CHANNEL_RED = 0x04
|
||||
COLOR_CHANNEL_GREEN = 0x08
|
||||
COLOR_CHANNEL_BLUE = 0x10
|
||||
|
||||
# Generate midpoint color temperatures for bulbs that have limited
|
||||
# support for white light colors
|
||||
TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN
|
||||
TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN
|
||||
TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -29,6 +47,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
|
||||
if node.has_command_class(zwave.COMMAND_CLASS_COLOR):
|
||||
try:
|
||||
add_devices([ZwaveColorLight(value)])
|
||||
except ValueError as exception:
|
||||
_LOGGER.warning(
|
||||
"Error initializing as color bulb: %s "
|
||||
"Initializing as standard dimmer.", exception)
|
||||
add_devices([ZwaveDimmer(value)])
|
||||
else:
|
||||
add_devices([ZwaveDimmer(value)])
|
||||
|
||||
|
||||
@@ -50,8 +78,9 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
from pydispatch import dispatcher
|
||||
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
|
||||
self._brightness, self._state = brightness_state(value)
|
||||
self._brightness = None
|
||||
self._state = None
|
||||
self.update_properties()
|
||||
|
||||
# Used for value change event handling
|
||||
self._refreshing = False
|
||||
@@ -60,6 +89,11 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
dispatcher.connect(
|
||||
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def update_properties(self):
|
||||
"""Update internal properties based on zwave values."""
|
||||
# Brightness
|
||||
self._brightness, self._state = brightness_state(self._value)
|
||||
|
||||
def _value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id != value.value_id:
|
||||
@@ -67,7 +101,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
|
||||
if self._refreshing:
|
||||
self._refreshing = False
|
||||
self._brightness, self._state = brightness_state(value)
|
||||
self.update_properties()
|
||||
else:
|
||||
def _refresh_value():
|
||||
"""Used timer callback for delayed value refresh."""
|
||||
@@ -108,3 +142,168 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
"""Turn the device off."""
|
||||
if self._value.node.set_dimmer(self._value.value_id, 0):
|
||||
self._state = STATE_OFF
|
||||
|
||||
|
||||
def ct_to_rgb(temp):
|
||||
"""Convert color temperature (mireds) to RGB."""
|
||||
colorlist = list(
|
||||
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
|
||||
return [int(val) for val in colorlist]
|
||||
|
||||
|
||||
class ZwaveColorLight(ZwaveDimmer):
|
||||
"""Representation of a Z-Wave color changing light."""
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the light."""
|
||||
self._value_color = None
|
||||
self._value_color_channels = None
|
||||
self._color_channels = None
|
||||
self._rgb = None
|
||||
self._ct = None
|
||||
|
||||
# Here we attempt to find a zwave color value with the same instance
|
||||
# id as the dimmer value. Currently zwave nodes that change colors
|
||||
# only include one dimmer and one color command, but this will
|
||||
# hopefully provide some forward compatibility for new devices that
|
||||
# have multiple color changing elements.
|
||||
for value_color in value.node.get_rgbbulbs().values():
|
||||
if value.instance == value_color.instance:
|
||||
self._value_color = value_color
|
||||
|
||||
if self._value_color is None:
|
||||
raise ValueError("No matching color command found.")
|
||||
|
||||
for value_color_channels in value.node.get_values(
|
||||
class_id=zwave.COMMAND_CLASS_COLOR, genre='System',
|
||||
type="Int").values():
|
||||
self._value_color_channels = value_color_channels
|
||||
|
||||
if self._value_color_channels is None:
|
||||
raise ValueError("Color Channels not found.")
|
||||
|
||||
super().__init__(value)
|
||||
|
||||
def update_properties(self):
|
||||
"""Update internal properties based on zwave values."""
|
||||
super().update_properties()
|
||||
|
||||
# Color Channels
|
||||
self._color_channels = self._value_color_channels.data
|
||||
|
||||
# Color Data String
|
||||
data = self._value_color.data
|
||||
|
||||
# RGB is always present in the openzwave color data string.
|
||||
self._rgb = [
|
||||
int(data[1:3], 16),
|
||||
int(data[3:5], 16),
|
||||
int(data[5:7], 16)]
|
||||
|
||||
# Parse remaining color channels. Openzwave appends white channels
|
||||
# that are present.
|
||||
index = 7
|
||||
|
||||
# Warm white
|
||||
if self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
||||
warm_white = int(data[index:index+2], 16)
|
||||
index += 2
|
||||
else:
|
||||
warm_white = 0
|
||||
|
||||
# Cold white
|
||||
if self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
||||
cold_white = int(data[index:index+2], 16)
|
||||
index += 2
|
||||
else:
|
||||
cold_white = 0
|
||||
|
||||
# Color temperature. With two white channels, only two color
|
||||
# temperatures are supported for the bulb. The channel values
|
||||
# indicate brightness for warm/cold color temperature.
|
||||
if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and
|
||||
self._color_channels & COLOR_CHANNEL_COLD_WHITE):
|
||||
if warm_white > 0:
|
||||
self._ct = TEMP_WARM_HASS
|
||||
self._rgb = ct_to_rgb(self._ct)
|
||||
elif cold_white > 0:
|
||||
self._ct = TEMP_COLD_HASS
|
||||
self._rgb = ct_to_rgb(self._ct)
|
||||
else:
|
||||
# RGB color is being used. Just report midpoint.
|
||||
self._ct = TEMP_MID_HASS
|
||||
|
||||
# If only warm white is reported 0-255 is color temperature.
|
||||
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
||||
self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * (
|
||||
warm_white / 255)
|
||||
self._rgb = ct_to_rgb(self._ct)
|
||||
|
||||
# If only cold white is reported 0-255 is negative color temperature.
|
||||
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
||||
self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * (
|
||||
(255 - cold_white) / 255)
|
||||
self._rgb = ct_to_rgb(self._ct)
|
||||
|
||||
# If no rgb channels supported, report None.
|
||||
if not (self._color_channels & COLOR_CHANNEL_RED or
|
||||
self._color_channels & COLOR_CHANNEL_GREEN or
|
||||
self._color_channels & COLOR_CHANNEL_BLUE):
|
||||
self._rgb = None
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the rgb color."""
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature."""
|
||||
return self._ct
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
rgbw = None
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
# With two white channels, only two color temperatures are
|
||||
# supported for the bulb.
|
||||
if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and
|
||||
self._color_channels & COLOR_CHANNEL_COLD_WHITE):
|
||||
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
|
||||
self._ct = TEMP_WARM_HASS
|
||||
rgbw = b'#000000FF00'
|
||||
else:
|
||||
self._ct = TEMP_COLD_HASS
|
||||
rgbw = b'#00000000FF'
|
||||
|
||||
# If only warm white is reported 0-255 is color temperature
|
||||
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
||||
rgbw = b'#000000'
|
||||
temp = (
|
||||
(kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) /
|
||||
(HASS_COLOR_MAX - HASS_COLOR_MIN) * 255)
|
||||
rgbw += format(int(temp)).encode('utf-8')
|
||||
|
||||
# If only cold white is reported 0-255 is negative color temp
|
||||
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
||||
rgbw = b'#000000'
|
||||
temp = (
|
||||
255 - (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) /
|
||||
(HASS_COLOR_MAX - HASS_COLOR_MIN) * 255)
|
||||
rgbw += format(int(temp)).encode('utf-8')
|
||||
|
||||
elif ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
|
||||
rgbw = b'#'
|
||||
for colorval in self._rgb:
|
||||
rgbw += format(colorval, '02x').encode('utf-8')
|
||||
rgbw += b'0000'
|
||||
|
||||
if rgbw is None:
|
||||
_LOGGER.warning("rgbw string was not generated for turn_on")
|
||||
else:
|
||||
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
|
||||
|
||||
super().turn_on(**kwargs)
|
||||
|
65
homeassistant/components/lock/vera.py
Normal file
65
homeassistant/components/lock/vera.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Support for Vera locks.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/lock.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED)
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Find and return Vera locks."""
|
||||
add_devices_callback(
|
||||
VeraLock(device, VERA_CONTROLLER) for
|
||||
device in VERA_DEVICES['lock'])
|
||||
|
||||
|
||||
class VeraLock(VeraDevice, LockDevice):
|
||||
"""Representation of a Vera lock."""
|
||||
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
self._state = None
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
attr = {}
|
||||
if self.vera_device.has_battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
||||
return attr
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
self.vera_device.lock()
|
||||
self._state = STATE_LOCKED
|
||||
self.update_ha_state()
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
self.vera_device.unlock()
|
||||
self._state = STATE_UNLOCKED
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if device is on."""
|
||||
return self._state == STATE_LOCKED
|
||||
|
||||
def update(self):
|
||||
"""Called by the Vera device callback to update state."""
|
||||
self._state = (STATE_LOCKED if self.vera_device.is_locked(True)
|
||||
else STATE_UNLOCKED)
|
@@ -7,9 +7,10 @@ https://home-assistant.io/components/lock.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -30,38 +31,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(WinkLockDevice(lock) for lock in pywink.get_locks())
|
||||
|
||||
|
||||
class WinkLockDevice(LockDevice):
|
||||
class WinkLockDevice(WinkDevice, LockDevice):
|
||||
"""Representation of a Wink lock."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the lock."""
|
||||
self.wink = wink
|
||||
self._battery = self.wink.battery_level
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of this wink lock."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the lock if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the lock."""
|
||||
self.wink.update_state()
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if device is locked."""
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
self.wink.set_state(True)
|
||||
@@ -69,16 +50,3 @@ class WinkLockDevice(LockDevice):
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
self.wink.set_state(False)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
370
homeassistant/components/media_player/braviatv.py
Normal file
370
homeassistant/components/media_player/braviatv.py
Normal file
@@ -0,0 +1,370 @@
|
||||
"""
|
||||
Support for interface with a Sony Bravia TV.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.braviatv/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/aparraga/braviarc/archive/0.3.2.zip'
|
||||
'#braviarc==0.3.2']
|
||||
|
||||
BRAVIA_CONFIG_FILE = 'bravia.conf'
|
||||
CLIENTID_PREFIX = 'HomeAssistant'
|
||||
NICKNAME = 'Home Assistant'
|
||||
|
||||
# Map ip to request id for configuring
|
||||
_CONFIGURING = {}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
|
||||
|
||||
def _get_mac_address(ip_address):
|
||||
"""Get the MAC address of the device."""
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
pid = Popen(["arp", "-n", ip_address], stdout=PIPE)
|
||||
pid_component = pid.communicate()[0]
|
||||
mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'),
|
||||
pid_component).groups()[0]
|
||||
return mac
|
||||
|
||||
|
||||
def _config_from_file(filename, config=None):
|
||||
"""Create the configuration from a file."""
|
||||
if config:
|
||||
# We're writing configuration
|
||||
bravia_config = _config_from_file(filename)
|
||||
if bravia_config is None:
|
||||
bravia_config = {}
|
||||
new_config = bravia_config.copy()
|
||||
new_config.update(config)
|
||||
try:
|
||||
with open(filename, 'w') as fdesc:
|
||||
fdesc.write(json.dumps(new_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 ValueError as error:
|
||||
return {}
|
||||
except IOError as error:
|
||||
_LOGGER.error('Reading config file failed: %s', error)
|
||||
# This won't work yet
|
||||
return False
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the Sony Bravia TV platform."""
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
if host is None:
|
||||
return # if no host configured, do not continue
|
||||
|
||||
pin = None
|
||||
bravia_config = _config_from_file(hass.config.path(BRAVIA_CONFIG_FILE))
|
||||
while len(bravia_config):
|
||||
# Setup a configured TV
|
||||
host_ip, host_config = bravia_config.popitem()
|
||||
if host_ip == host:
|
||||
pin = host_config['pin']
|
||||
mac = host_config['mac']
|
||||
name = config.get(CONF_NAME)
|
||||
add_devices_callback([BraviaTVDevice(host, mac, name, pin)])
|
||||
return
|
||||
|
||||
setup_bravia(config, pin, hass, add_devices_callback)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup_bravia(config, pin, hass, add_devices_callback):
|
||||
"""Setup a Sony Bravia TV based on host parameter."""
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
if name is None:
|
||||
name = "Sony Bravia TV"
|
||||
|
||||
if pin is None:
|
||||
request_configuration(config, hass, add_devices_callback)
|
||||
return
|
||||
else:
|
||||
mac = _get_mac_address(host)
|
||||
if mac is not None:
|
||||
mac = mac.decode('utf8')
|
||||
# If we came here and configuring this host, mark as done
|
||||
if host in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(host)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
_LOGGER.info('Discovery configuration done!')
|
||||
|
||||
# Save config
|
||||
if not _config_from_file(
|
||||
hass.config.path(BRAVIA_CONFIG_FILE),
|
||||
{host: {'pin': pin, 'host': host, 'mac': mac}}):
|
||||
_LOGGER.error('failed to save config file')
|
||||
|
||||
add_devices_callback([BraviaTVDevice(host, mac, name, pin)])
|
||||
|
||||
|
||||
def request_configuration(config, hass, add_devices_callback):
|
||||
"""Request configuration steps from the user."""
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
if name is None:
|
||||
name = "Sony Bravia"
|
||||
|
||||
configurator = get_component('configurator')
|
||||
|
||||
# We got an error if this method is called while we are configuring
|
||||
if host in _CONFIGURING:
|
||||
configurator.notify_errors(
|
||||
_CONFIGURING[host], "Failed to register, please try again.")
|
||||
return
|
||||
|
||||
def bravia_configuration_callback(data):
|
||||
"""Callback after user enter PIN."""
|
||||
from braviarc import braviarc
|
||||
|
||||
pin = data.get('pin')
|
||||
braviarc = braviarc.BraviaRC(host)
|
||||
braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME)
|
||||
if braviarc.is_connected():
|
||||
setup_bravia(config, pin, hass, add_devices_callback)
|
||||
else:
|
||||
request_configuration(config, hass, add_devices_callback)
|
||||
|
||||
_CONFIGURING[host] = configurator.request_config(
|
||||
hass, name, bravia_configuration_callback,
|
||||
description='Enter the Pin shown on your Sony Bravia TV.' +
|
||||
'If no Pin is shown, enter 0000 to let TV show you a Pin.',
|
||||
description_image="/static/images/smart-tv.png",
|
||||
submit_caption="Confirm",
|
||||
fields=[{'id': 'pin', 'name': 'Enter the pin', 'type': ''}]
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method, too-many-public-methods,
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
class BraviaTVDevice(MediaPlayerDevice):
|
||||
"""Representation of a Sony Bravia TV."""
|
||||
|
||||
def __init__(self, host, mac, name, pin):
|
||||
"""Initialize the Sony Bravia device."""
|
||||
from braviarc import braviarc
|
||||
|
||||
self._pin = pin
|
||||
self._braviarc = braviarc.BraviaRC(host, mac)
|
||||
self._name = name
|
||||
self._state = STATE_OFF
|
||||
self._muted = False
|
||||
self._program_name = None
|
||||
self._channel_name = None
|
||||
self._channel_number = None
|
||||
self._source = None
|
||||
self._source_list = []
|
||||
self._original_content_list = []
|
||||
self._content_mapping = {}
|
||||
self._duration = None
|
||||
self._content_uri = None
|
||||
self._id = None
|
||||
self._playing = False
|
||||
self._start_date_time = None
|
||||
self._program_media_type = None
|
||||
self._min_volume = None
|
||||
self._max_volume = None
|
||||
self._volume = None
|
||||
|
||||
self._braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME)
|
||||
if self._braviarc.is_connected():
|
||||
self.update()
|
||||
else:
|
||||
self._state = STATE_OFF
|
||||
|
||||
def update(self):
|
||||
"""Update TV info."""
|
||||
if not self._braviarc.is_connected():
|
||||
self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME)
|
||||
if not self._braviarc.is_connected():
|
||||
return
|
||||
|
||||
# Retrieve the latest data.
|
||||
try:
|
||||
if self._state == STATE_ON:
|
||||
# refresh volume info:
|
||||
self._refresh_volume()
|
||||
self._refresh_channels()
|
||||
|
||||
playing_info = self._braviarc.get_playing_info()
|
||||
if playing_info is None or len(playing_info) == 0:
|
||||
self._state = STATE_OFF
|
||||
else:
|
||||
self._state = STATE_ON
|
||||
self._program_name = playing_info.get('programTitle')
|
||||
self._channel_name = playing_info.get('title')
|
||||
self._program_media_type = playing_info.get(
|
||||
'programMediaType')
|
||||
self._channel_number = playing_info.get('dispNum')
|
||||
self._source = playing_info.get('source')
|
||||
self._content_uri = playing_info.get('uri')
|
||||
self._duration = playing_info.get('durationSec')
|
||||
self._start_date_time = playing_info.get('startDateTime')
|
||||
|
||||
except Exception as exception_instance: # pylint: disable=broad-except
|
||||
_LOGGER.error(exception_instance)
|
||||
self._state = STATE_OFF
|
||||
|
||||
def _refresh_volume(self):
|
||||
"""Refresh volume information."""
|
||||
volume_info = self._braviarc.get_volume_info()
|
||||
if volume_info is not None:
|
||||
self._volume = volume_info.get('volume')
|
||||
self._min_volume = volume_info.get('minVolume')
|
||||
self._max_volume = volume_info.get('maxVolume')
|
||||
self._muted = volume_info.get('mute')
|
||||
|
||||
def _refresh_channels(self):
|
||||
if len(self._source_list) == 0:
|
||||
self._content_mapping = self._braviarc. \
|
||||
load_source_list()
|
||||
self._source_list = []
|
||||
for key in self._content_mapping:
|
||||
self._source_list.append(key)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
return self._source
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return self._source_list
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
if self._volume is not None:
|
||||
return self._volume / 100
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Boolean if volume is currently muted."""
|
||||
return self._muted
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_BRAVIA
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return_value = None
|
||||
if self._channel_name is not None:
|
||||
return_value = self._channel_name
|
||||
if self._program_name is not None:
|
||||
return_value = return_value + ': ' + self._program_name
|
||||
return return_value
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
return self._channel_name
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
return self._duration
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
self._braviarc.set_volume_level(volume)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
self._braviarc.turn_on()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self._braviarc.turn_off()
|
||||
|
||||
def volume_up(self):
|
||||
"""Volume up the media player."""
|
||||
self._braviarc.volume_up()
|
||||
|
||||
def volume_down(self):
|
||||
"""Volume down media player."""
|
||||
self._braviarc.volume_down()
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Send mute command."""
|
||||
self._braviarc.mute_volume(mute)
|
||||
|
||||
def select_source(self, source):
|
||||
"""Set the input source."""
|
||||
if source in self._content_mapping:
|
||||
uri = self._content_mapping[source]
|
||||
self._braviarc.play_content(uri)
|
||||
|
||||
def media_play_pause(self):
|
||||
"""Simulate play pause media player."""
|
||||
if self._playing:
|
||||
self.media_pause()
|
||||
else:
|
||||
self.media_play()
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
self._playing = True
|
||||
self._braviarc.media_play()
|
||||
|
||||
def media_pause(self):
|
||||
"""Send media pause command to media player."""
|
||||
self._playing = False
|
||||
self._braviarc.media_pause()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self._braviarc.media_next_track()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send the previous track command."""
|
||||
self._braviarc.media_previous_track()
|
213
homeassistant/components/media_player/cmus.py
Normal file
213
homeassistant/components/media_player/cmus.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
Support for interacting with and controlling the cmus music player.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.cmus/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD,
|
||||
CONF_PORT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pycmus==0.1.0']
|
||||
|
||||
SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
|
||||
SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discover_info=None):
|
||||
"""Setup the CMUS platform."""
|
||||
from pycmus import exceptions
|
||||
|
||||
host = config.get(CONF_HOST, None)
|
||||
password = config.get(CONF_PASSWORD, None)
|
||||
port = config.get(CONF_PORT, None)
|
||||
name = config.get(CONF_NAME, None)
|
||||
if host and not password:
|
||||
_LOGGER.error("A password must be set if using a remote cmus server")
|
||||
return False
|
||||
try:
|
||||
cmus_remote = CmusDevice(host, password, port, name)
|
||||
except exceptions.InvalidPassword:
|
||||
_LOGGER.error("The provided password was rejected by cmus")
|
||||
return False
|
||||
add_devices([cmus_remote])
|
||||
|
||||
|
||||
class CmusDevice(MediaPlayerDevice):
|
||||
"""Representation of a running CMUS."""
|
||||
|
||||
# pylint: disable=no-member, too-many-public-methods, abstract-method
|
||||
def __init__(self, server, password, port, name):
|
||||
"""Initialize the CMUS device."""
|
||||
from pycmus import remote
|
||||
|
||||
if server:
|
||||
port = port or 3000
|
||||
self.cmus = remote.PyCmus(server=server, password=password,
|
||||
port=port)
|
||||
auto_name = "cmus-%s" % server
|
||||
else:
|
||||
self.cmus = remote.PyCmus()
|
||||
auto_name = "cmus-local"
|
||||
self._name = name or auto_name
|
||||
self.status = {}
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
status = self.cmus.get_status_dict()
|
||||
if not status:
|
||||
_LOGGER.warning("Recieved no status from cmus")
|
||||
else:
|
||||
self.status = status
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the media state."""
|
||||
if 'status' not in self.status:
|
||||
self.update()
|
||||
if self.status['status'] == 'playing':
|
||||
return STATE_PLAYING
|
||||
elif self.status['status'] == 'paused':
|
||||
return STATE_PAUSED
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
return self.status.get('file')
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
"""Content type of the current playing media."""
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
return self.status.get('duration')
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self.status['tag'].get('title')
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
return self.status['tag'].get('artist')
|
||||
|
||||
@property
|
||||
def media_track(self):
|
||||
"""Track number of current playing media, music track only."""
|
||||
return self.status['tag'].get('tracknumber')
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album name of current playing media, music track only."""
|
||||
return self.status['tag'].get('album')
|
||||
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
"""Album artist of current playing media, music track only."""
|
||||
return self.status['tag'].get('albumartist')
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Return the volume level."""
|
||||
left = self.status['set'].get('vol_left')[0]
|
||||
right = self.status['set'].get('vol_right')[0]
|
||||
if left != right:
|
||||
volume = float(left + right) / 2
|
||||
else:
|
||||
volume = left
|
||||
return int(volume)/100
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_CMUS
|
||||
|
||||
def turn_off(self):
|
||||
"""Service to send the CMUS the command to stop playing."""
|
||||
self.cmus.player_stop()
|
||||
|
||||
def turn_on(self):
|
||||
"""Service to send the CMUS the command to start playing."""
|
||||
self.cmus.player_play()
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
self.cmus.set_volume(int(volume * 100))
|
||||
|
||||
def volume_up(self):
|
||||
"""Function to send CMUS the command for volume up."""
|
||||
left = self.status['set'].get('vol_left')
|
||||
right = self.status['set'].get('vol_right')
|
||||
if left != right:
|
||||
current_volume = float(left + right) / 2
|
||||
else:
|
||||
current_volume = left
|
||||
|
||||
if current_volume <= 100:
|
||||
self.cmus.set_volume(int(current_volume) + 5)
|
||||
|
||||
def volume_down(self):
|
||||
"""Function to send CMUS the command for volume down."""
|
||||
left = self.status['set'].get('vol_left')
|
||||
right = self.status['set'].get('vol_right')
|
||||
if left != right:
|
||||
current_volume = float(left + right) / 2
|
||||
else:
|
||||
current_volume = left
|
||||
|
||||
if current_volume <= 100:
|
||||
self.cmus.set_volume(int(current_volume) - 5)
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Send the play command."""
|
||||
if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]:
|
||||
self.cmus.player_play_file(media_id)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Invalid media type %s. Only %s and %s are supported",
|
||||
media_type, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST)
|
||||
|
||||
def media_pause(self):
|
||||
"""Send the pause command."""
|
||||
self.cmus.player_pause()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.cmus.player_next()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send next track command."""
|
||||
self.cmus.player_prev()
|
||||
|
||||
def media_seek(self, position):
|
||||
"""Send seek command."""
|
||||
self.cmus.seek(position)
|
||||
|
||||
def media_play(self):
|
||||
"""Send the play command."""
|
||||
self.cmus.player_play()
|
||||
|
||||
def media_stop(self):
|
||||
"""Send the stop command."""
|
||||
self.cmus.stop()
|
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['websocket-client==0.35.0']
|
||||
REQUIREMENTS = ['websocket-client==0.37.0']
|
||||
SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
|
||||
|
||||
|
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['jsonrpc-requests==0.2']
|
||||
REQUIREMENTS = ['jsonrpc-requests==0.3']
|
||||
|
||||
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
@@ -301,4 +301,4 @@ class KodiDevice(MediaPlayerDevice):
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Send the play_media command to the media player."""
|
||||
self._server.Player.Open({media_type: media_id}, {})
|
||||
self._server.Player.Open({"item": {"file": str(media_id)}})
|
||||
|
@@ -4,7 +4,6 @@ Support for the roku media player.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.roku/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -77,7 +76,8 @@ class RokuDevice(MediaPlayerDevice):
|
||||
self.current_app = self.roku.current_app
|
||||
else:
|
||||
self.current_app = None
|
||||
except requests.exceptions.ConnectionError:
|
||||
except (requests.exceptions.ConnectionError,
|
||||
requests.exceptions.ReadTimeout):
|
||||
self.current_app = None
|
||||
|
||||
def get_source_list(self):
|
||||
|
@@ -154,12 +154,20 @@ sonos_group_players:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
sonos_unjoin:
|
||||
description: Unjoin the player from a group.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will be unjoined from their group. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
sonos_snapshot:
|
||||
description: Take a snapshot of the media player.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
description: Name(s) of entites that will be snapshot. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
sonos_restore:
|
||||
@@ -167,5 +175,5 @@ sonos_restore:
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
description: Name(s) of entites that will be restored. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
@@ -4,7 +4,6 @@ Support for interacting with Snapcast clients.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.snapcast/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
|
||||
|
@@ -34,11 +34,12 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
||||
SUPPORT_SEEK
|
||||
|
||||
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
|
||||
SERVICE_UNJOIN = 'sonos_unjoin'
|
||||
SERVICE_SNAPSHOT = 'sonos_snapshot'
|
||||
SERVICE_RESTORE = 'sonos_restore'
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=unused-argument, too-many-locals
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Sonos platform."""
|
||||
import soco
|
||||
@@ -72,47 +73,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(devices)
|
||||
_LOGGER.info('Added %s Sonos speakers', len(players))
|
||||
|
||||
def _apply_service(service, service_func, *service_func_args):
|
||||
"""Internal func for applying a service."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
service_func(device, *service_func_args)
|
||||
device.update_ha_state(True)
|
||||
|
||||
def group_players_service(service):
|
||||
"""Group media players, use player as coordinator."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
_apply_service(service, SonosDevice.group_players)
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
def unjoin_service(service):
|
||||
"""Unjoin the player from a group."""
|
||||
_apply_service(service, SonosDevice.unjoin)
|
||||
|
||||
for device in _devices:
|
||||
device.group_players()
|
||||
device.update_ha_state(True)
|
||||
|
||||
def snapshot(service):
|
||||
def snapshot_service(service):
|
||||
"""Take a snapshot."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
_apply_service(service, SonosDevice.snapshot)
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
device.snapshot(service)
|
||||
device.update_ha_state(True)
|
||||
|
||||
def restore(service):
|
||||
def restore_service(service):
|
||||
"""Restore a snapshot."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
device.restore(service)
|
||||
device.update_ha_state(True)
|
||||
_apply_service(service, SonosDevice.restore)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
@@ -121,12 +110,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
group_players_service,
|
||||
descriptions.get(SERVICE_GROUP_PLAYERS))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_UNJOIN,
|
||||
unjoin_service,
|
||||
descriptions.get(SERVICE_UNJOIN))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
|
||||
snapshot,
|
||||
snapshot_service,
|
||||
descriptions.get(SERVICE_SNAPSHOT))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_RESTORE,
|
||||
restore,
|
||||
restore_service,
|
||||
descriptions.get(SERVICE_RESTORE))
|
||||
|
||||
return True
|
||||
@@ -356,12 +349,17 @@ class SonosDevice(MediaPlayerDevice):
|
||||
self._player.partymode()
|
||||
|
||||
@only_if_coordinator
|
||||
def snapshot(self, service):
|
||||
def unjoin(self):
|
||||
"""Unjoin the player from a group."""
|
||||
self._player.unjoin()
|
||||
|
||||
@only_if_coordinator
|
||||
def snapshot(self):
|
||||
"""Snapshot the player."""
|
||||
self.soco_snapshot.snapshot()
|
||||
|
||||
@only_if_coordinator
|
||||
def restore(self, service):
|
||||
def restore(self):
|
||||
"""Restore snapshot for the player."""
|
||||
self.soco_snapshot.restore(True)
|
||||
|
||||
|
@@ -4,7 +4,6 @@ Combination of multiple media players into one for a universal controller.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.universal/
|
||||
"""
|
||||
|
||||
import logging
|
||||
# pylint: disable=import-error
|
||||
from copy import copy
|
||||
|
@@ -29,7 +29,7 @@ MQTT_CLIENT = None
|
||||
SERVICE_PUBLISH = 'publish'
|
||||
EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received'
|
||||
|
||||
REQUIREMENTS = ['paho-mqtt==1.1']
|
||||
REQUIREMENTS = ['paho-mqtt==1.2']
|
||||
|
||||
CONF_EMBEDDED = 'embedded'
|
||||
CONF_BROKER = 'broker'
|
||||
|
@@ -7,7 +7,7 @@ https://home-assistant.io/components/notify.pushover/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TITLE, DOMAIN, BaseNotificationService)
|
||||
ATTR_TITLE, ATTR_TARGET, ATTR_DATA, DOMAIN, BaseNotificationService)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
@@ -51,7 +51,18 @@ class PushoverNotificationService(BaseNotificationService):
|
||||
"""Send a message to a user."""
|
||||
from pushover import RequestError
|
||||
|
||||
# Make a copy and use empty dict if necessary
|
||||
data = dict(kwargs.get(ATTR_DATA) or {})
|
||||
|
||||
data['title'] = kwargs.get(ATTR_TITLE)
|
||||
|
||||
target = kwargs.get(ATTR_TARGET)
|
||||
if target is not None:
|
||||
data['device'] = target
|
||||
|
||||
try:
|
||||
self.pushover.send_message(message, title=kwargs.get(ATTR_TITLE))
|
||||
self.pushover.send_message(message, **data)
|
||||
except ValueError as val_err:
|
||||
_LOGGER.error(str(val_err))
|
||||
except RequestError:
|
||||
_LOGGER.exception("Could not send pushover notification")
|
||||
|
@@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['slacker==0.9.16']
|
||||
REQUIREMENTS = ['slacker==0.9.17']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
80
homeassistant/components/persistent_notification.py
Normal file
80
homeassistant/components/persistent_notification.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
A component which is collecting configuration errors.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/persistent_notification/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template, config_validation as cv
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = 'persistent_notification'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SERVICE_CREATE = 'create'
|
||||
ATTR_TITLE = 'title'
|
||||
ATTR_MESSAGE = 'message'
|
||||
ATTR_NOTIFICATION_ID = 'notification_id'
|
||||
|
||||
SCHEMA_SERVICE_CREATE = vol.Schema({
|
||||
vol.Required(ATTR_MESSAGE): cv.template,
|
||||
vol.Optional(ATTR_TITLE): cv.template,
|
||||
vol.Optional(ATTR_NOTIFICATION_ID): cv.string,
|
||||
})
|
||||
|
||||
|
||||
DEFAULT_OBJECT_ID = 'notification'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create(hass, message, title=None, notification_id=None):
|
||||
"""Turn all or specified light off."""
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
(ATTR_TITLE, title),
|
||||
(ATTR_MESSAGE, message),
|
||||
(ATTR_NOTIFICATION_ID, notification_id),
|
||||
] if value is not None
|
||||
}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_CREATE, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the persistent notification component."""
|
||||
def create_service(call):
|
||||
"""Handle a create notification service call."""
|
||||
title = call.data.get(ATTR_TITLE)
|
||||
message = call.data.get(ATTR_MESSAGE)
|
||||
notification_id = call.data.get(ATTR_NOTIFICATION_ID)
|
||||
|
||||
if notification_id is not None:
|
||||
entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))
|
||||
else:
|
||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID,
|
||||
hass=hass)
|
||||
attr = {}
|
||||
if title is not None:
|
||||
try:
|
||||
title = template.render(hass, title)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error('Error rendering title %s: %s', title, ex)
|
||||
|
||||
attr[ATTR_TITLE] = title
|
||||
|
||||
try:
|
||||
message = template.render(hass, message)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error('Error rendering message %s: %s', message, ex)
|
||||
|
||||
hass.states.set(entity_id, message, attr)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_CREATE, create_service, {},
|
||||
SCHEMA_SERVICE_CREATE)
|
||||
|
||||
return True
|
101
homeassistant/components/rollershutter/homematic.py
Normal file
101
homeassistant/components/rollershutter/homematic.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
The homematic rollershutter platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/rollershutter.homematic/
|
||||
|
||||
Important: For this platform to work the homematic component has to be
|
||||
properly configured.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN)
|
||||
from homeassistant.components.rollershutter import RollershutterDevice,\
|
||||
ATTR_CURRENT_POSITION
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
return homematic.setup_hmdevice_discovery_helper(HMRollershutter,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
class HMRollershutter(homematic.HMDevice, RollershutterDevice):
|
||||
"""Represents a Homematic Rollershutter in Home Assistant."""
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""
|
||||
Return current position of rollershutter.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
if self.available:
|
||||
return int((1 - self._hm_get_state()) * 100)
|
||||
return None
|
||||
|
||||
def position(self, **kwargs):
|
||||
"""Move to a defined position: 0 (closed) and 100 (open)."""
|
||||
if self.available:
|
||||
if ATTR_CURRENT_POSITION in kwargs:
|
||||
position = float(kwargs[ATTR_CURRENT_POSITION])
|
||||
position = min(100, max(0, position))
|
||||
level = (100 - position) / 100.0
|
||||
self._hmdevice.set_level(level, self._channel)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the rollershutter."""
|
||||
current = self.current_position
|
||||
if current is None:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
return STATE_CLOSED if current == 100 else STATE_OPEN
|
||||
|
||||
def move_up(self, **kwargs):
|
||||
"""Move the rollershutter up."""
|
||||
if self.available:
|
||||
self._hmdevice.move_up(self._channel)
|
||||
|
||||
def move_down(self, **kwargs):
|
||||
"""Move the rollershutter down."""
|
||||
if self.available:
|
||||
self._hmdevice.move_down(self._channel)
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the device if in motion."""
|
||||
if self.available:
|
||||
self._hmdevice.stop(self._channel)
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the HM Object as this HA type."""
|
||||
from pyhomematic.devicetypes.actors import Blind
|
||||
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
# Check if the homematic device is correct for this HA device
|
||||
if isinstance(self._hmdevice, Blind):
|
||||
return True
|
||||
|
||||
_LOGGER.critical("This %s can't be use as rollershutter!", self._name)
|
||||
return False
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from hm metadata."""
|
||||
super()._init_data_struct()
|
||||
|
||||
# Add state to data dict
|
||||
self._state = "LEVEL"
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
@@ -7,9 +7,10 @@ https://home-assistant.io/components/rollershutter.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.rollershutter import RollershutterDevice
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
pywink.get_shades())
|
||||
|
||||
|
||||
class WinkRollershutterDevice(RollershutterDevice):
|
||||
class WinkRollershutterDevice(WinkDevice, RollershutterDevice):
|
||||
"""Representation of a Wink rollershutter (shades)."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the rollershutter."""
|
||||
self.wink = wink
|
||||
self._battery = None
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Wink Shades don't track their position."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this wink rollershutter."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the rollershutter if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the rollershutter."""
|
||||
return self.wink.update_state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def move_down(self):
|
||||
"""Close the shade."""
|
||||
self.wink.set_state(0)
|
||||
|
76
homeassistant/components/rollershutter/zwave.py
Normal file
76
homeassistant/components/rollershutter/zwave.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Support for Zwave roller shutter components.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/rollershutter.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.rollershutter import DOMAIN
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.rollershutter import RollershutterDevice
|
||||
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
|
||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave roller shutters."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||
return
|
||||
if value.index != 0:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
|
||||
|
||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
||||
"""Representation of an Zwave roller shutter."""
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._node = value.node
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""Return the current position of Zwave roller shutter."""
|
||||
return self._value.data
|
||||
|
||||
def move_up(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
self._node.set_dimmer(self._value.value_id, 100)
|
||||
|
||||
def move_down(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
self._node.set_dimmer(self._value.value_id, 0)
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_BINARY).values():
|
||||
# Rollershutter will toggle between UP (True), DOWN (False).
|
||||
# It also stops the shutter if the same value is sent while moving.
|
||||
value.data = value.data
|
||||
break
|
@@ -1,38 +1,43 @@
|
||||
"""
|
||||
Support for information about the German trans system.
|
||||
Support for information about the German train system.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.deutsche_bahn/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta, datetime
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_PLATFORM)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['schiene==0.17']
|
||||
|
||||
CONF_START = 'from'
|
||||
CONF_DESTINATION = 'to'
|
||||
ICON = 'mdi:train'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'deutsche_bahn',
|
||||
vol.Required(CONF_START): cv.string,
|
||||
vol.Required(CONF_DESTINATION): cv.string,
|
||||
})
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Deutsche Bahn Sensor."""
|
||||
start = config.get('from')
|
||||
goal = config.get('to')
|
||||
start = config.get(CONF_START)
|
||||
destination = config.get(CONF_DESTINATION)
|
||||
|
||||
if start is None:
|
||||
_LOGGER.error('Missing required variable: "from"')
|
||||
return False
|
||||
|
||||
if goal is None:
|
||||
_LOGGER.error('Missing required variable: "to"')
|
||||
return False
|
||||
|
||||
dev = []
|
||||
dev.append(DeutscheBahnSensor(start, goal))
|
||||
add_devices_callback(dev)
|
||||
add_devices([DeutscheBahnSensor(start, destination)])
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
@@ -63,16 +68,17 @@ class DeutscheBahnSensor(Entity):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self.data.connections[0]
|
||||
connections = self.data.connections[0]
|
||||
connections['next'] = self.data.connections[1]['departure']
|
||||
connections['next_on'] = self.data.connections[2]['departure']
|
||||
return connections
|
||||
|
||||
def update(self):
|
||||
"""Get the latest delay from bahn.de and updates the state."""
|
||||
self.data.update()
|
||||
self._state = self.data.connections[0].get('departure', 'Unknown')
|
||||
if self.data.connections[0]['delay'] != 0:
|
||||
self._state += " + {}".format(
|
||||
self.data.connections[0]['delay']
|
||||
)
|
||||
self._state += " + {}".format(self.data.connections[0]['delay'])
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
@@ -90,18 +96,15 @@ class SchieneData(object):
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update the connection data."""
|
||||
self.connections = self.schiene.connections(self.start,
|
||||
self.goal,
|
||||
datetime.now())
|
||||
self.connections = self.schiene.connections(self.start, self.goal)
|
||||
|
||||
for con in self.connections:
|
||||
# Details info is not useful.
|
||||
# Having a more consistent interface simplifies
|
||||
# usage of Template sensors later on
|
||||
# Detail info is not useful. Having a more consistent interface
|
||||
# simplifies usage of template sensors.
|
||||
if 'details' in con:
|
||||
con.pop('details')
|
||||
delay = con.get('delay',
|
||||
{'delay_departure': 0,
|
||||
delay = con.get('delay', {'delay_departure': 0,
|
||||
'delay_arrival': 0})
|
||||
# IMHO only delay_departure is usefull
|
||||
# IMHO only delay_departure is useful
|
||||
con['delay'] = delay['delay_departure']
|
||||
con['ontime'] = con.get('ontime', False)
|
||||
|
68
homeassistant/components/sensor/envisalink.py
Normal file
68
homeassistant/components/sensor/envisalink.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Support for Envisalink sensors (shows panel info).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.envisalink/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_PARTITIONNAME,
|
||||
EnvisalinkDevice,
|
||||
SIGNAL_KEYPAD_UPDATE)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink sensor devices."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
for part_num in _configured_partitions:
|
||||
_device_config_data = PARTITION_SCHEMA(
|
||||
_configured_partitions[part_num])
|
||||
_device = EnvisalinkSensor(
|
||||
_device_config_data[CONF_PARTITIONNAME],
|
||||
part_num,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
|
||||
class EnvisalinkSensor(EnvisalinkDevice):
|
||||
"""Representation of an envisalink keypad."""
|
||||
|
||||
def __init__(self, partition_name, partition_number, info, controller):
|
||||
"""Initialize the sensor."""
|
||||
from pydispatch import dispatcher
|
||||
self._icon = 'mdi:alarm'
|
||||
self._partition_number = partition_number
|
||||
_LOGGER.debug('Setting up sensor for partition: ' + partition_name)
|
||||
EnvisalinkDevice.__init__(self,
|
||||
partition_name + ' Keypad',
|
||||
info,
|
||||
controller)
|
||||
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the overall state."""
|
||||
return self._info['status']['alpha']
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self._info['status']
|
||||
|
||||
def _update_callback(self, partition):
|
||||
"""Update the partition state in HA, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.update_ha_state()
|
125
homeassistant/components/sensor/fixer.py
Normal file
125
homeassistant/components/sensor/fixer.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Currency exchange rate support that comes from fixer.io.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.fixer/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_PLATFORM, CONF_NAME)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['fixerio==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Exchange rate"
|
||||
ICON = 'mdi:currency'
|
||||
|
||||
CONF_BASE = 'base'
|
||||
CONF_TARGET = 'target'
|
||||
|
||||
STATE_ATTR_BASE = 'Base currency'
|
||||
STATE_ATTR_TARGET = 'Target currency'
|
||||
STATE_ATTR_EXCHANGE_RATE = 'Exchange rate'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'fixer',
|
||||
vol.Optional(CONF_BASE): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_TARGET): cv.string,
|
||||
})
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(days=1)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Fixer.io sensor."""
|
||||
from fixerio import (Fixerio, exceptions)
|
||||
|
||||
name = config.get(CONF_NAME, DEFAULT_NAME)
|
||||
base = config.get(CONF_BASE, 'USD')
|
||||
target = config.get(CONF_TARGET)
|
||||
|
||||
try:
|
||||
Fixerio(base=base, symbols=[target], secure=True).latest()
|
||||
except exceptions.FixerioException:
|
||||
_LOGGER.error('One of the given currencies is not supported')
|
||||
return False
|
||||
|
||||
data = ExchangeData(base, target)
|
||||
add_devices([ExchangeRateSensor(data, name, target)])
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class ExchangeRateSensor(Entity):
|
||||
"""Representation of a Exchange sensor."""
|
||||
|
||||
def __init__(self, data, name, target):
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self._target = target
|
||||
self._name = name
|
||||
self._state = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._target
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self.data.rate is not None:
|
||||
return {
|
||||
STATE_ATTR_BASE: self.data.rate['base'],
|
||||
STATE_ATTR_TARGET: self._target,
|
||||
STATE_ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target]
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return ICON
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the states."""
|
||||
self.data.update()
|
||||
self._state = round(self.data.rate['rates'][self._target], 3)
|
||||
|
||||
|
||||
class ExchangeData(object):
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, base_currency, target_currency):
|
||||
"""Initialize the data object."""
|
||||
from fixerio import Fixerio
|
||||
|
||||
self.rate = None
|
||||
self.base_currency = base_currency
|
||||
self.target_currency = target_currency
|
||||
self.exchange = Fixerio(base=self.base_currency,
|
||||
symbols=[self.target_currency],
|
||||
secure=True)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from Fixer.io."""
|
||||
self.rate = self.exchange.latest()
|
115
homeassistant/components/sensor/homematic.py
Normal file
115
homeassistant/components/sensor/homematic.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
The homematic sensor platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.homematic/
|
||||
|
||||
Important: For this platform to work the homematic component has to be
|
||||
properly configured.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
HM_STATE_HA_CAST = {
|
||||
"RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"},
|
||||
"WaterSensor": {0: "dry", 1: "wet", 2: "water"}
|
||||
}
|
||||
|
||||
HM_UNIT_HA_CAST = {
|
||||
"HUMIDITY": "%",
|
||||
"TEMPERATURE": "°C",
|
||||
"BRIGHTNESS": "#",
|
||||
"POWER": "W",
|
||||
"CURRENT": "mA",
|
||||
"VOLTAGE": "V",
|
||||
"ENERGY_COUNTER": "Wh"
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
return homematic.setup_hmdevice_discovery_helper(HMSensor,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
class HMSensor(homematic.HMDevice):
|
||||
"""Represents various Homematic sensors in Home Assistant."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if not self.available:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
# Does a cast exist for this class?
|
||||
name = self._hmdevice.__class__.__name__
|
||||
if name in HM_STATE_HA_CAST:
|
||||
return HM_STATE_HA_CAST[name].get(self._hm_get_state(), None)
|
||||
|
||||
# No cast, return original value
|
||||
return self._hm_get_state()
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
if not self.available:
|
||||
return None
|
||||
|
||||
return HM_UNIT_HA_CAST.get(self._state, None)
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the HM Object as this HA type."""
|
||||
from pyhomematic.devicetypes.sensors import HMSensor as pyHMSensor
|
||||
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
# Check if the homematic device is correct for this HA device
|
||||
if not isinstance(self._hmdevice, pyHMSensor):
|
||||
_LOGGER.critical("This %s can't be use as sensor!", self._name)
|
||||
return False
|
||||
|
||||
# Does user defined value exist?
|
||||
if self._state and self._state not in self._hmdevice.SENSORNODE:
|
||||
# pylint: disable=logging-too-many-args
|
||||
_LOGGER.critical("This %s have no sensor with %s! Values are",
|
||||
self._name, self._state,
|
||||
str(self._hmdevice.SENSORNODE.keys()))
|
||||
return False
|
||||
|
||||
# No param is set and more than 1 sensor nodes are present
|
||||
if self._state is None and len(self._hmdevice.SENSORNODE) > 1:
|
||||
_LOGGER.critical("This %s has multiple sensor nodes. " +
|
||||
"Please us param. Values are: %s", self._name,
|
||||
str(self._hmdevice.SENSORNODE.keys()))
|
||||
return False
|
||||
|
||||
_LOGGER.debug("%s is okay for linking", self._name)
|
||||
return True
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from hm metadata."""
|
||||
super()._init_data_struct()
|
||||
|
||||
if self._state is None and len(self._hmdevice.SENSORNODE) == 1:
|
||||
for value in self._hmdevice.SENSORNODE:
|
||||
self._state = value
|
||||
|
||||
# Add state to data dict
|
||||
if self._state:
|
||||
_LOGGER.debug("%s init datadict with main node '%s'", self._name,
|
||||
self._state)
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
else:
|
||||
_LOGGER.critical("Can't correctly init sensor %s.", self._name)
|
@@ -11,6 +11,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -24,6 +25,15 @@ SENSOR_TYPES = {
|
||||
'rain': ['Rain', 'mm', 'mdi:weather-rainy'],
|
||||
'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'],
|
||||
'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'],
|
||||
'battery_vp': ['Battery', '', 'mdi:battery'],
|
||||
'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||
'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||
'WindAngle': ['Angle', '', 'mdi:compass'],
|
||||
'WindStrength': ['Strength', 'km/h', 'mdi:weather-windy'],
|
||||
'GustAngle': ['Gust Angle', '', 'mdi:compass'],
|
||||
'GustStrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'],
|
||||
'rf_status': ['Radio', '', 'mdi:signal'],
|
||||
'wifi_status': ['Wifi', '', 'mdi:wifi']
|
||||
}
|
||||
|
||||
CONF_STATION = 'station'
|
||||
@@ -97,6 +107,8 @@ class NetAtmoSensor(Entity):
|
||||
return self._unit_of_measurement
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
# Fix for pylint too many statements error
|
||||
# pylint: disable=too-many-statements
|
||||
def update(self):
|
||||
"""Get the latest data from NetAtmo API and updates the states."""
|
||||
self.netatmo_data.update()
|
||||
@@ -118,6 +130,79 @@ class NetAtmoSensor(Entity):
|
||||
self._state = data['CO2']
|
||||
elif self.type == 'pressure':
|
||||
self._state = round(data['Pressure'], 1)
|
||||
elif self.type == 'battery_vp':
|
||||
if data['battery_vp'] >= 5500:
|
||||
self._state = "Full"
|
||||
elif data['battery_vp'] >= 5100:
|
||||
self._state = "High"
|
||||
elif data['battery_vp'] >= 4600:
|
||||
self._state = "Medium"
|
||||
elif data['battery_vp'] >= 4100:
|
||||
self._state = "Low"
|
||||
elif data['battery_vp'] < 4100:
|
||||
self._state = "Very Low"
|
||||
elif self.type == 'min_temp':
|
||||
self._state = data['min_temp']
|
||||
elif self.type == 'max_temp':
|
||||
self._state = data['max_temp']
|
||||
elif self.type == 'WindAngle':
|
||||
if data['WindAngle'] >= 330:
|
||||
self._state = "North (%d\xb0)" % data['WindAngle']
|
||||
elif data['WindAngle'] >= 300:
|
||||
self._state = "North-West (%d\xb0)" % data['WindAngle']
|
||||
elif data['WindAngle'] >= 240:
|
||||
self._state = "West (%d\xb0)" % data['WindAngle']
|
||||
elif data['WindAngle'] >= 210:
|
||||
self._state = "South-West (%d\xb0)" % data['WindAngle']
|
||||
elif data['WindAngle'] >= 150:
|
||||
self._state = "South (%d\xb0)" % data['WindAngle']
|
||||
elif data['WindAngle'] >= 120:
|
||||
self._state = "South-East (%d\xb0)" % data['WindAngle']
|
||||
elif data['WindAngle'] >= 60:
|
||||
self._state = "East (%d\xb0)" % data['WindAngle']
|
||||
elif data['WindAngle'] >= 30:
|
||||
self._state = "North-East (%d\xb0)" % data['WindAngle']
|
||||
elif data['WindAngle'] >= 0:
|
||||
self._state = "North (%d\xb0)" % data['WindAngle']
|
||||
elif self.type == 'WindStrength':
|
||||
self._state = data['WindStrength']
|
||||
elif self.type == 'GustAngle':
|
||||
if data['GustAngle'] >= 330:
|
||||
self._state = "North (%d\xb0)" % data['GustAngle']
|
||||
elif data['GustAngle'] >= 300:
|
||||
self._state = "North-West (%d\xb0)" % data['GustAngle']
|
||||
elif data['GustAngle'] >= 240:
|
||||
self._state = "West (%d\xb0)" % data['GustAngle']
|
||||
elif data['GustAngle'] >= 210:
|
||||
self._state = "South-West (%d\xb0)" % data['GustAngle']
|
||||
elif data['GustAngle'] >= 150:
|
||||
self._state = "South (%d\xb0)" % data['GustAngle']
|
||||
elif data['GustAngle'] >= 120:
|
||||
self._state = "South-East (%d\xb0)" % data['GustAngle']
|
||||
elif data['GustAngle'] >= 60:
|
||||
self._state = "East (%d\xb0)" % data['GustAngle']
|
||||
elif data['GustAngle'] >= 30:
|
||||
self._state = "North-East (%d\xb0)" % data['GustAngle']
|
||||
elif data['GustAngle'] >= 0:
|
||||
self._state = "North (%d\xb0)" % data['GustAngle']
|
||||
elif self.type == 'GustStrength':
|
||||
self._state = data['GustStrength']
|
||||
elif self.type == 'rf_status':
|
||||
if data['rf_status'] >= 90:
|
||||
self._state = "Low"
|
||||
elif data['rf_status'] >= 76:
|
||||
self._state = "Medium"
|
||||
elif data['rf_status'] >= 60:
|
||||
self._state = "High"
|
||||
elif data['rf_status'] <= 59:
|
||||
self._state = "Full"
|
||||
elif self.type == 'wifi_status':
|
||||
if data['wifi_status'] >= 86:
|
||||
self._state = "Bad"
|
||||
elif data['wifi_status'] >= 71:
|
||||
self._state = "Middle"
|
||||
elif data['wifi_status'] <= 70:
|
||||
self._state = "Good"
|
||||
|
||||
|
||||
class NetAtmoData(object):
|
||||
|
@@ -12,21 +12,24 @@ from glob import glob
|
||||
from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
BASE_DIR = '/sys/bus/w1/devices/'
|
||||
DEVICE_FOLDERS = glob(os.path.join(BASE_DIR, '28*'))
|
||||
SENSOR_IDS = []
|
||||
DEVICE_FILES = []
|
||||
for device_folder in DEVICE_FOLDERS:
|
||||
SENSOR_IDS.append(os.path.split(device_folder)[1])
|
||||
DEVICE_FILES.append(os.path.join(device_folder, 'w1_slave'))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the one wire Sensors."""
|
||||
if DEVICE_FILES == []:
|
||||
base_dir = config.get('mount_dir', '/sys/bus/w1/devices/')
|
||||
device_folders = glob(os.path.join(base_dir, '[10,22,28,3B,42]*'))
|
||||
sensor_ids = []
|
||||
device_files = []
|
||||
for device_folder in device_folders:
|
||||
sensor_ids.append(os.path.split(device_folder)[1])
|
||||
if base_dir.startswith('/sys/bus/w1/devices'):
|
||||
device_files.append(os.path.join(device_folder, 'w1_slave'))
|
||||
else:
|
||||
device_files.append(os.path.join(device_folder, 'temperature'))
|
||||
|
||||
if device_files == []:
|
||||
_LOGGER.error('No onewire sensor found.')
|
||||
_LOGGER.error('Check if dtoverlay=w1-gpio,gpiopin=4.')
|
||||
_LOGGER.error('is in your /boot/config.txt and')
|
||||
@@ -34,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devs = []
|
||||
names = SENSOR_IDS
|
||||
names = sensor_ids
|
||||
|
||||
for key in config.keys():
|
||||
if key == "names":
|
||||
@@ -47,9 +50,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
# map names to ids.
|
||||
elif isinstance(config['names'], dict):
|
||||
names = []
|
||||
for sensor_id in SENSOR_IDS:
|
||||
for sensor_id in sensor_ids:
|
||||
names.append(config['names'].get(sensor_id, sensor_id))
|
||||
for device_file, name in zip(DEVICE_FILES, names):
|
||||
for device_file, name in zip(device_files, names):
|
||||
devs.append(OneWire(name, device_file))
|
||||
add_devices(devs)
|
||||
|
||||
@@ -88,6 +91,8 @@ class OneWire(Entity):
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from the device."""
|
||||
temp = -99
|
||||
if self._device_file.startswith('/sys/bus/w1/devices'):
|
||||
lines = self._read_temp_raw()
|
||||
while lines[0].strip()[-3:] != 'YES':
|
||||
time.sleep(0.2)
|
||||
@@ -96,6 +101,17 @@ class OneWire(Entity):
|
||||
if equals_pos != -1:
|
||||
temp_string = lines[1][equals_pos+2:]
|
||||
temp = round(float(temp_string) / 1000.0, 1)
|
||||
else:
|
||||
ds_device_file = open(self._device_file, 'r')
|
||||
temp_read = ds_device_file.readlines()
|
||||
ds_device_file.close()
|
||||
if len(temp_read) == 1:
|
||||
try:
|
||||
temp = round(float(temp_read[0]), 1)
|
||||
except ValueError:
|
||||
_LOGGER.warning('Invalid temperature value read from ' +
|
||||
self._device_file)
|
||||
|
||||
if temp < -55 or temp > 125:
|
||||
return
|
||||
self._state = temp
|
||||
|
105
homeassistant/components/sensor/openexchangerates.py
Normal file
105
homeassistant/components/sensor/openexchangerates.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Support for openexchangerates.org exchange rates service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.openexchangerates/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import requests
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
|
||||
_RESOURCE = 'https://openexchangerates.org/api/latest.json'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=100)
|
||||
CONF_BASE = 'base'
|
||||
CONF_QUOTE = 'quote'
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'Exchange Rate Sensor'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Openexchangerates sensor."""
|
||||
payload = config.get('payload', None)
|
||||
rest = OpenexchangeratesData(
|
||||
_RESOURCE,
|
||||
config.get(CONF_API_KEY),
|
||||
config.get(CONF_BASE, 'USD'),
|
||||
config.get(CONF_QUOTE),
|
||||
payload
|
||||
)
|
||||
response = requests.get(_RESOURCE, params={'base': config.get(CONF_BASE,
|
||||
'USD'),
|
||||
'app_id':
|
||||
config.get(CONF_API_KEY)},
|
||||
timeout=10)
|
||||
if response.status_code != 200:
|
||||
_LOGGER.error("Check your OpenExchangeRates API")
|
||||
return False
|
||||
rest.update()
|
||||
add_devices([OpenexchangeratesSensor(rest, config.get(CONF_NAME,
|
||||
DEFAULT_NAME),
|
||||
config.get(CONF_QUOTE))])
|
||||
|
||||
|
||||
class OpenexchangeratesSensor(Entity):
|
||||
"""Representation of an Openexchangerates sensor."""
|
||||
|
||||
def __init__(self, rest, name, quote):
|
||||
"""Initialize the sensor."""
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
self._quote = quote
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return other attributes of the sensor."""
|
||||
return self.rest.data
|
||||
|
||||
def update(self):
|
||||
"""Update current conditions."""
|
||||
self.rest.update()
|
||||
value = self.rest.data
|
||||
self._state = round(value[str(self._quote)], 4)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class OpenexchangeratesData(object):
|
||||
"""Get data from Openexchangerates.org."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, resource, api_key, base, quote, data):
|
||||
"""Initialize the data object."""
|
||||
self._resource = resource
|
||||
self._api_key = api_key
|
||||
self._base = base
|
||||
self._quote = quote
|
||||
self.data = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from openexchangerates.org."""
|
||||
try:
|
||||
result = requests.get(self._resource, params={'base': self._base,
|
||||
'app_id':
|
||||
self._api_key},
|
||||
timeout=10)
|
||||
self.data = result.json()['rates']
|
||||
except requests.exceptions.HTTPError:
|
||||
_LOGGER.error("Check Openexchangerates API Key")
|
||||
self.data = None
|
||||
return False
|
@@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['beautifulsoup4==4.4.1', 'lxml==3.6.0']
|
||||
REQUIREMENTS = ['beautifulsoup4==4.4.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_RESOURCE = 'http://www.hydrodaten.admin.ch/en/'
|
||||
@@ -47,7 +47,7 @@ HydroData = collections.namedtuple(
|
||||
'temperature_max'])
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -148,7 +148,7 @@ class HydrologicalData(object):
|
||||
|
||||
try:
|
||||
tables = BeautifulSoup(response.content,
|
||||
'lxml').findChildren('table')
|
||||
'html.parser').findChildren('table')
|
||||
rows = tables[0].findChildren(['th', 'tr'])
|
||||
|
||||
details = []
|
||||
|
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for monitoring the local system..
|
||||
Support for monitoring the local system.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.systemmonitor/
|
||||
@@ -10,7 +10,7 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['psutil==4.2.0']
|
||||
REQUIREMENTS = ['psutil==4.3.0']
|
||||
SENSOR_TYPES = {
|
||||
'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'],
|
||||
'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'],
|
||||
|
@@ -1,4 +1,9 @@
|
||||
"""Support for ThinkingCleaner."""
|
||||
"""
|
||||
Support for ThinkingCleaner.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.thinkingcleaner/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
|
@@ -7,11 +7,12 @@ at https://home-assistant.io/components/sensor.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED,
|
||||
STATE_OPEN, TEMP_CELSIUS,
|
||||
ATTR_BATTERY_LEVEL)
|
||||
STATE_OPEN, TEMP_CELSIUS)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
|
||||
|
||||
SENSOR_TYPES = ['temperature', 'humidity']
|
||||
|
||||
@@ -38,14 +39,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays())
|
||||
|
||||
|
||||
class WinkSensorDevice(Entity):
|
||||
class WinkSensorDevice(WinkDevice, Entity):
|
||||
"""Representation of a Wink sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the sensor."""
|
||||
self.wink = wink
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink)
|
||||
wink = get_component('wink')
|
||||
self.capability = self.wink.capability()
|
||||
self._battery = self.wink.battery_level
|
||||
if self.wink.UNIT == "°":
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
else:
|
||||
@@ -55,9 +56,9 @@ class WinkSensorDevice(Entity):
|
||||
def state(self):
|
||||
"""Return the state."""
|
||||
if self.capability == "humidity":
|
||||
return self.wink.humidity_percentage()
|
||||
return round(self.wink.humidity_percentage())
|
||||
elif self.capability == "temperature":
|
||||
return self.wink.temperature_float()
|
||||
return round(self.wink.temperature_float(), 1)
|
||||
else:
|
||||
return STATE_OPEN if self.is_open else STATE_CLOSED
|
||||
|
||||
@@ -66,80 +67,20 @@ class WinkSensorDevice(Entity):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this wink sensor."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor if any."""
|
||||
return self.wink.name()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def update(self):
|
||||
"""Update state of the sensor."""
|
||||
self.wink.update_state()
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
"""Return true if door is open."""
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
||||
|
||||
class WinkEggMinder(Entity):
|
||||
class WinkEggMinder(WinkDevice, Entity):
|
||||
"""Representation of a Wink Egg Minder."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the sensor."""
|
||||
self.wink = wink
|
||||
self._battery = self.wink.battery_level
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state."""
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of this wink Egg Minder."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Egg Minder if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update state of the Egg Minder."""
|
||||
self.wink.update_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
@@ -15,7 +15,6 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util import location
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,16 +53,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Yr.no sensor."""
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
elevation = config.get(CONF_ELEVATION)
|
||||
elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0)
|
||||
|
||||
if None in (latitude, longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return False
|
||||
|
||||
if elevation is None:
|
||||
elevation = location.elevation(latitude,
|
||||
longitude)
|
||||
|
||||
coordinates = dict(lat=latitude,
|
||||
lon=longitude,
|
||||
msl=elevation)
|
||||
|
@@ -6,6 +6,7 @@ https://home-assistant.io/components/shell_command/
|
||||
"""
|
||||
import logging
|
||||
import subprocess
|
||||
import shlex
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -23,8 +24,6 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SHELL_COMMAND_SCHEMA = vol.Schema({})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the shell_command component."""
|
||||
@@ -44,8 +43,7 @@ def setup(hass, config):
|
||||
_LOGGER.exception('Error running command: %s', cmd)
|
||||
|
||||
for name in conf.keys():
|
||||
hass.services.register(DOMAIN, name, service_handler,
|
||||
schema=SHELL_COMMAND_SCHEMA)
|
||||
hass.services.register(DOMAIN, name, service_handler)
|
||||
return True
|
||||
|
||||
|
||||
@@ -64,6 +62,6 @@ def _parse_command(hass, cmd, variables):
|
||||
shell = True
|
||||
else:
|
||||
# template used. Must break into list and use shell=False for security
|
||||
cmd = [prog] + rendered_args.split()
|
||||
cmd = [prog] + shlex.split(rendered_args)
|
||||
shell = False
|
||||
return cmd, shell
|
||||
|
@@ -12,7 +12,6 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import (
|
||||
track_point_in_utc_time, track_utc_time_change)
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util import location as location_util
|
||||
from homeassistant.const import CONF_ELEVATION
|
||||
|
||||
REQUIREMENTS = ['astral==1.2']
|
||||
@@ -108,7 +107,7 @@ def setup(hass, config):
|
||||
|
||||
elevation = platform_config.get(CONF_ELEVATION)
|
||||
if elevation is None:
|
||||
elevation = location_util.elevation(latitude, longitude)
|
||||
elevation = hass.config.elevation or 0
|
||||
|
||||
from astral import Location
|
||||
|
||||
|
@@ -108,7 +108,7 @@ def setup(hass, config):
|
||||
class SwitchDevice(ToggleEntity):
|
||||
"""Representation of a switch."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
# pylint: disable=no-self-use, abstract-method
|
||||
@property
|
||||
def current_power_mwh(self):
|
||||
"""Return the current power usage in mWh."""
|
||||
|
@@ -4,7 +4,6 @@ Use serial protocol of acer projector to obtain state of the projector.
|
||||
This component allows to control almost all projectors from acer using
|
||||
their RS232 serial communication protocol.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -61,7 +60,8 @@ class AcerSwitch(SwitchDevice):
|
||||
write_timeout=write_timeout, **kwargs)
|
||||
self._serial_port = serial_port
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
self._state = False
|
||||
self._available = False
|
||||
self._attributes = {
|
||||
LAMP_HOURS: STATE_UNKNOWN,
|
||||
INPUT_SOURCE: STATE_UNKNOWN,
|
||||
@@ -100,14 +100,19 @@ class AcerSwitch(SwitchDevice):
|
||||
return match.group(1)
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if projector is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name of the projector."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state of the projector."""
|
||||
def is_on(self):
|
||||
"""Return if the projector is turned on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
@@ -120,11 +125,13 @@ class AcerSwitch(SwitchDevice):
|
||||
msg = CMD_DICT[LAMP]
|
||||
awns = self._write_read_format(msg)
|
||||
if awns == 'Lamp 1':
|
||||
self._state = STATE_ON
|
||||
self._state = True
|
||||
self._available = True
|
||||
elif awns == 'Lamp 0':
|
||||
self._state = STATE_OFF
|
||||
self._state = False
|
||||
self._available = True
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
self._available = False
|
||||
|
||||
for key in self._attributes.keys():
|
||||
msg = CMD_DICT.get(key, None)
|
||||
|
@@ -4,6 +4,7 @@ Support for device running with the aREST RESTful framework.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.arest/
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
102
homeassistant/components/switch/homematic.py
Normal file
102
homeassistant/components/switch/homematic.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Support for Homematic switches.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.homematic/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the Homematic switch platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
return homematic.setup_hmdevice_discovery_helper(HMSwitch,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
class HMSwitch(homematic.HMDevice, SwitchDevice):
|
||||
"""Representation of a Homematic switch."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if switch is on."""
|
||||
try:
|
||||
return self._hm_get_state() > 0
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_power_mwh(self):
|
||||
"""Return the current power usage in mWh."""
|
||||
if "ENERGY_COUNTER" in self._data:
|
||||
try:
|
||||
return self._data["ENERGY_COUNTER"] / 1000
|
||||
except ZeroDivisionError:
|
||||
return 0
|
||||
|
||||
return None
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on."""
|
||||
if self.available:
|
||||
self._hmdevice.on(self._channel)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the switch off."""
|
||||
if self.available:
|
||||
self._hmdevice.off(self._channel)
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the Homematic object as this HA type."""
|
||||
from pyhomematic.devicetypes.actors import Dimmer, Switch
|
||||
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
# Check if the Homematic device is correct for this HA device
|
||||
if isinstance(self._hmdevice, Switch):
|
||||
return True
|
||||
if isinstance(self._hmdevice, Dimmer):
|
||||
return True
|
||||
|
||||
_LOGGER.critical("This %s can't be use as switch", self._name)
|
||||
return False
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
from pyhomematic.devicetypes.actors import Dimmer,\
|
||||
Switch, SwitchPowermeter
|
||||
|
||||
super()._init_data_struct()
|
||||
|
||||
# Use STATE
|
||||
if isinstance(self._hmdevice, Switch):
|
||||
self._state = "STATE"
|
||||
|
||||
# Use LEVEL
|
||||
if isinstance(self._hmdevice, Dimmer):
|
||||
self._state = "LEVEL"
|
||||
|
||||
# Need sensor values for SwitchPowermeter
|
||||
if isinstance(self._hmdevice, SwitchPowermeter):
|
||||
for node in self._hmdevice.SENSORNODE:
|
||||
self._data.update({node: STATE_UNKNOWN})
|
||||
|
||||
# Add state to data dict
|
||||
if self._state:
|
||||
_LOGGER.debug("%s init data dict with main node '%s'", self._name,
|
||||
self._state)
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
else:
|
||||
_LOGGER.critical("Can't correctly init light %s.", self._name)
|
@@ -1,4 +1,9 @@
|
||||
"""Support for ThinkingCleaner."""
|
||||
"""
|
||||
Support for ThinkingCleaner.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.thinkingcleaner/
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
@@ -52,7 +52,7 @@ class WOLSwitch(SwitchDevice):
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if switch is on."""
|
||||
"""Return true if switch is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
@@ -65,6 +65,10 @@ class WOLSwitch(SwitchDevice):
|
||||
self._wol.send_magic_packet(self._mac_address)
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self):
|
||||
"""Do nothing."""
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
"""Check if device is on and update the state."""
|
||||
if platform.system().lower() == "windows":
|
||||
|
@@ -6,10 +6,11 @@ https://home-assistant.io/components/switch.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -31,3 +32,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(WinkToggleDevice(switch) for switch in
|
||||
pywink.get_powerstrip_outlets())
|
||||
add_devices(WinkToggleDevice(switch) for switch in pywink.get_sirens())
|
||||
|
||||
|
||||
class WinkToggleDevice(WinkDevice, ToggleEntity):
|
||||
"""Represents a Wink toggle (switch) device."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the Wink device."""
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self.wink.state()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
self.wink.set_state(True)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the device off."""
|
||||
self.wink.set_state(False)
|
||||
|
@@ -273,29 +273,29 @@ class ThermostatDevice(Entity):
|
||||
"""Return true if the fan is on."""
|
||||
return None
|
||||
|
||||
def set_temperate(self, temperature):
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_hvac_mode(self, hvac_mode):
|
||||
"""Set hvac mode."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_fan_on(self):
|
||||
"""Turn fan on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_fan_off(self):
|
||||
"""Turn fan off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
@@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-arguments, abstract-method
|
||||
class DemoThermostat(ThermostatDevice):
|
||||
"""Representation of a demo thermostat."""
|
||||
|
||||
|
@@ -68,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
class Thermostat(ThermostatDevice):
|
||||
"""A thermostat class for Ecobee."""
|
||||
|
||||
|
@@ -1,16 +1,16 @@
|
||||
"""
|
||||
Support for eq3 Bluetooth Smart thermostats.
|
||||
|
||||
Uses bluepy_devices library.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/thermostat.eq3btsmart/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.thermostat import ThermostatDevice
|
||||
from homeassistant.const import TEMP_CELCIUS
|
||||
from homeassistant.helpers.temperature import convert
|
||||
|
||||
REQUIREMENTS = ['bluepy_devices>=0.2.0']
|
||||
REQUIREMENTS = ['bluepy_devices==0.2.0']
|
||||
|
||||
CONF_MAC = 'mac'
|
||||
CONF_DEVICES = 'devices'
|
||||
@@ -31,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, import-error
|
||||
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
|
||||
class EQ3BTSmartThermostat(ThermostatDevice):
|
||||
"""Representation of a EQ3 Bluetooth Smart thermostat."""
|
||||
|
||||
|
@@ -41,7 +41,6 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the heat control thermostat."""
|
||||
name = config.get(CONF_NAME)
|
||||
@@ -55,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
min_temp, max_temp, target_temp)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
class HeatControl(ThermostatDevice):
|
||||
"""Representation of a HeatControl device."""
|
||||
|
||||
|
@@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class HeatmiserV3Thermostat(ThermostatDevice):
|
||||
"""Representation of a HeatmiserV3 thermostat."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
def __init__(self, heatmiser, device, name, serport):
|
||||
"""Initialize the thermostat."""
|
||||
self.heatmiser = heatmiser
|
||||
|
@@ -1,122 +1,34 @@
|
||||
"""
|
||||
Support for Homematic (HM-TC-IT-WM-W-EU, HM-CC-RT-DN) thermostats.
|
||||
Support for Homematic thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/thermostat.homematic/
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
from xmlrpc.client import ServerProxy
|
||||
from xmlrpc.client import Error
|
||||
from collections import namedtuple
|
||||
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.components.thermostat import ThermostatDevice
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.temperature import convert
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN
|
||||
|
||||
REQUIREMENTS = []
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ADDRESS = 'address'
|
||||
CONF_DEVICES = 'devices'
|
||||
CONF_ID = 'id'
|
||||
PROPERTY_SET_TEMPERATURE = 'SET_TEMPERATURE'
|
||||
PROPERTY_VALVE_STATE = 'VALVE_STATE'
|
||||
PROPERTY_ACTUAL_TEMPERATURE = 'ACTUAL_TEMPERATURE'
|
||||
PROPERTY_BATTERY_STATE = 'BATTERY_STATE'
|
||||
PROPERTY_LOWBAT = 'LOWBAT'
|
||||
PROPERTY_CONTROL_MODE = 'CONTROL_MODE'
|
||||
PROPERTY_BURST_MODE = 'BURST_RX'
|
||||
TYPE_HM_THERMOSTAT = 'HOMEMATIC_THERMOSTAT'
|
||||
TYPE_HM_WALLTHERMOSTAT = 'HOMEMATIC_WALLTHERMOSTAT'
|
||||
TYPE_MAX_THERMOSTAT = 'MAX_THERMOSTAT'
|
||||
|
||||
HomematicConfig = namedtuple('HomematicConfig',
|
||||
['device_type',
|
||||
'platform_type',
|
||||
'channel',
|
||||
'maint_channel'])
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the Homematic thermostat platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
HM_TYPE_MAPPING = {
|
||||
'HM-CC-RT-DN': HomematicConfig('HM-CC-RT-DN',
|
||||
TYPE_HM_THERMOSTAT,
|
||||
4, 4),
|
||||
'HM-CC-RT-DN-BoM': HomematicConfig('HM-CC-RT-DN-BoM',
|
||||
TYPE_HM_THERMOSTAT,
|
||||
4, 4),
|
||||
'HM-TC-IT-WM-W-EU': HomematicConfig('HM-TC-IT-WM-W-EU',
|
||||
TYPE_HM_WALLTHERMOSTAT,
|
||||
2, 2),
|
||||
'BC-RT-TRX-CyG': HomematicConfig('BC-RT-TRX-CyG',
|
||||
TYPE_MAX_THERMOSTAT,
|
||||
1, 0),
|
||||
'BC-RT-TRX-CyG-2': HomematicConfig('BC-RT-TRX-CyG-2',
|
||||
TYPE_MAX_THERMOSTAT,
|
||||
1, 0),
|
||||
'BC-RT-TRX-CyG-3': HomematicConfig('BC-RT-TRX-CyG-3',
|
||||
TYPE_MAX_THERMOSTAT,
|
||||
1, 0)
|
||||
}
|
||||
return homematic.setup_hmdevice_discovery_helper(HMThermostat,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Homematic thermostat."""
|
||||
devices = []
|
||||
try:
|
||||
address = config[CONF_ADDRESS]
|
||||
homegear = ServerProxy(address)
|
||||
|
||||
for name, device_cfg in config[CONF_DEVICES].items():
|
||||
# get device description to detect the type
|
||||
device_type = homegear.getDeviceDescription(
|
||||
device_cfg[CONF_ID] + ':-1')['TYPE']
|
||||
|
||||
if device_type in HM_TYPE_MAPPING.keys():
|
||||
devices.append(HomematicThermostat(
|
||||
HM_TYPE_MAPPING[device_type],
|
||||
address,
|
||||
device_cfg[CONF_ID],
|
||||
name))
|
||||
else:
|
||||
raise ValueError(
|
||||
"Device Type '{}' currently not supported".format(
|
||||
device_type))
|
||||
except socket.error:
|
||||
_LOGGER.exception("Connection error to homematic web service")
|
||||
return False
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class HomematicThermostat(ThermostatDevice):
|
||||
# pylint: disable=abstract-method
|
||||
class HMThermostat(homematic.HMDevice, ThermostatDevice):
|
||||
"""Representation of a Homematic thermostat."""
|
||||
|
||||
def __init__(self, hm_config, address, _id, name):
|
||||
"""Initialize the thermostat."""
|
||||
self._hm_config = hm_config
|
||||
self.address = address
|
||||
self._id = _id
|
||||
self._name = name
|
||||
self._full_device_name = '{}:{}'.format(self._id,
|
||||
self._hm_config.channel)
|
||||
self._maint_device_name = '{}:{}'.format(self._id,
|
||||
self._hm_config.maint_channel)
|
||||
self._current_temperature = None
|
||||
self._target_temperature = None
|
||||
self._valve = None
|
||||
self._battery = None
|
||||
self._mode = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Homematic device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
@@ -125,26 +37,22 @@ class HomematicThermostat(ThermostatDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
if not self.available:
|
||||
return None
|
||||
return self._data["ACTUAL_TEMPERATURE"]
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
"""Return the target temperature."""
|
||||
if not self.available:
|
||||
return None
|
||||
return self._data["SET_TEMPERATURE"]
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
device = ServerProxy(self.address)
|
||||
device.setValue(self._full_device_name,
|
||||
PROPERTY_SET_TEMPERATURE,
|
||||
temperature)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {"valve": self._valve,
|
||||
"battery": self._battery,
|
||||
"mode": self._mode}
|
||||
if not self.available:
|
||||
return None
|
||||
self._hmdevice.set_temperature(temperature)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
@@ -156,39 +64,27 @@ class HomematicThermostat(ThermostatDevice):
|
||||
"""Return the maximum temperature - 30.5 means on."""
|
||||
return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
try:
|
||||
device = ServerProxy(self.address)
|
||||
self._current_temperature = device.getValue(
|
||||
self._full_device_name,
|
||||
PROPERTY_ACTUAL_TEMPERATURE)
|
||||
self._target_temperature = device.getValue(
|
||||
self._full_device_name,
|
||||
PROPERTY_SET_TEMPERATURE)
|
||||
self._valve = device.getValue(
|
||||
self._full_device_name,
|
||||
PROPERTY_VALVE_STATE)
|
||||
self._mode = device.getValue(
|
||||
self._full_device_name,
|
||||
PROPERTY_CONTROL_MODE)
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the Homematic object as this HA type."""
|
||||
from pyhomematic.devicetypes.thermostats import HMThermostat\
|
||||
as pyHMThermostat
|
||||
|
||||
if self._hm_config.platform_type in [TYPE_HM_THERMOSTAT,
|
||||
TYPE_HM_WALLTHERMOSTAT]:
|
||||
self._battery = device.getValue(self._maint_device_name,
|
||||
PROPERTY_BATTERY_STATE)
|
||||
elif self._hm_config.platform_type == TYPE_MAX_THERMOSTAT:
|
||||
# emulate homematic battery voltage,
|
||||
# max reports lowbat if voltage < 2.2V
|
||||
# while homematic battery_state should
|
||||
# be between 1.5V and 4.6V
|
||||
lowbat = device.getValue(self._maint_device_name,
|
||||
PROPERTY_LOWBAT)
|
||||
if lowbat:
|
||||
self._battery = 1.5
|
||||
else:
|
||||
self._battery = 4.6
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
except Error:
|
||||
_LOGGER.exception("Did not receive any temperature data from the "
|
||||
"homematic API.")
|
||||
# Check if the Homematic device correct for this HA device
|
||||
if isinstance(self._hmdevice, pyHMThermostat):
|
||||
return True
|
||||
|
||||
_LOGGER.critical("This %s can't be use as thermostat", self._name)
|
||||
return False
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
super()._init_data_struct()
|
||||
|
||||
# Add state to data dict
|
||||
self._data.update({"CONTROL_MODE": STATE_UNKNOWN,
|
||||
"SET_TEMPERATURE": STATE_UNKNOWN,
|
||||
"ACTUAL_TEMPERATURE": STATE_UNKNOWN})
|
||||
|
@@ -49,7 +49,6 @@ def _setup_round(username, password, config, add_devices):
|
||||
|
||||
|
||||
# config will be used later
|
||||
# pylint: disable=unused-argument
|
||||
def _setup_us(username, password, config, add_devices):
|
||||
"""Setup user."""
|
||||
import somecomfort
|
||||
@@ -74,7 +73,6 @@ def _setup_us(username, password, config, add_devices):
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the honeywel thermostat."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
@@ -98,7 +96,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class RoundThermostat(ThermostatDevice):
|
||||
"""Representation of a Honeywell Round Connected thermostat."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-instance-attributes, abstract-method
|
||||
def __init__(self, device, zone_id, master, away_temp):
|
||||
"""Initialize the thermostat."""
|
||||
self.device = device
|
||||
@@ -182,6 +180,7 @@ class RoundThermostat(ThermostatDevice):
|
||||
self._is_dhw = False
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HoneywellUSThermostat(ThermostatDevice):
|
||||
"""Representation of a Honeywell US Thermostat."""
|
||||
|
||||
|
@@ -26,6 +26,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for structure, device in nest.devices()])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class NestThermostat(ThermostatDevice):
|
||||
"""Representation of a Nest thermostat."""
|
||||
|
||||
|
@@ -27,6 +27,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class ProliphixThermostat(ThermostatDevice):
|
||||
"""Representation a Proliphix thermostat."""
|
||||
|
||||
|
@@ -45,6 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(tstats)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class RadioThermostat(ThermostatDevice):
|
||||
"""Representation of a Radio Thermostat."""
|
||||
|
||||
|
@@ -58,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||
# pylint: disable=abstract-method
|
||||
class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
||||
"""Represents a HeatControl thermostat."""
|
||||
|
||||
@@ -80,7 +81,7 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.node == value.node:
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
|
||||
@@ -156,3 +157,4 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT).items():
|
||||
if int(value.data) != 0 and value.index == self._index:
|
||||
value.data = temperature
|
||||
break
|
||||
|
@@ -13,12 +13,13 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pyvera==0.2.10']
|
||||
REQUIREMENTS = ['pyvera==0.2.12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'vera'
|
||||
|
||||
|
||||
VERA_CONTROLLER = None
|
||||
|
||||
CONF_EXCLUDE = 'exclude'
|
||||
@@ -33,6 +34,7 @@ DEVICE_CATEGORIES = {
|
||||
'Switch': 'switch',
|
||||
'Armable Sensor': 'switch',
|
||||
'On/Off Switch': 'switch',
|
||||
'Doorlock': 'lock',
|
||||
# 'Window Covering': NOT SUPPORTED YET
|
||||
}
|
||||
|
||||
@@ -91,7 +93,7 @@ def setup(hass, base_config):
|
||||
dev_type = 'light'
|
||||
VERA_DEVICES[dev_type].append(device)
|
||||
|
||||
for component in 'binary_sensor', 'sensor', 'light', 'switch':
|
||||
for component in 'binary_sensor', 'sensor', 'light', 'switch', 'lock':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, base_config)
|
||||
|
||||
return True
|
||||
|
@@ -5,13 +5,17 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/wink/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DOMAIN = "wink"
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
|
||||
|
||||
SUBSCRIPTION_HANDLER = None
|
||||
CHANNELS = []
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@@ -22,7 +26,11 @@ def setup(hass, config):
|
||||
return False
|
||||
|
||||
import pywink
|
||||
from pubnub import Pubnub
|
||||
pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN])
|
||||
global SUBSCRIPTION_HANDLER
|
||||
SUBSCRIPTION_HANDLER = Pubnub("N/A", pywink.get_subscription_key())
|
||||
SUBSCRIPTION_HANDLER.set_heartbeat(120)
|
||||
|
||||
# Load components for the devices in the Wink that we support
|
||||
for component_name, func_exists in (
|
||||
@@ -41,13 +49,33 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class WinkToggleDevice(ToggleEntity):
|
||||
"""Represents a Wink toggle (switch) device."""
|
||||
class WinkDevice(Entity):
|
||||
"""Represents a base Wink device."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the Wink device."""
|
||||
from pubnub import Pubnub
|
||||
self.wink = wink
|
||||
self._battery = self.wink.battery_level
|
||||
if self.wink.pubnub_channel in CHANNELS:
|
||||
pubnub = Pubnub("N/A", self.wink.pubnub_key)
|
||||
pubnub.set_heartbeat(120)
|
||||
pubnub.subscribe(self.wink.pubnub_channel,
|
||||
self._pubnub_update,
|
||||
error=self._pubnub_error)
|
||||
else:
|
||||
CHANNELS.append(self.wink.pubnub_channel)
|
||||
SUBSCRIPTION_HANDLER.subscribe(self.wink.pubnub_channel,
|
||||
self._pubnub_update,
|
||||
error=self._pubnub_error)
|
||||
|
||||
def _pubnub_update(self, message, channel):
|
||||
self.wink.pubnub_update(json.loads(message))
|
||||
self.update_ha_state()
|
||||
|
||||
def _pubnub_error(self, message):
|
||||
logging.getLogger(__name__).error(
|
||||
"Error on pubnub update for " + self.wink.name())
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -59,28 +87,20 @@ class WinkToggleDevice(ToggleEntity):
|
||||
"""Return the name of the device."""
|
||||
return self.wink.name()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
self.wink.set_state(True)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the device off."""
|
||||
self.wink.set_state(False)
|
||||
|
||||
def update(self):
|
||||
"""Update state of the device."""
|
||||
self.wink.update_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Only poll if we are not subscribed to pubnub."""
|
||||
return self.wink.pubnub_channel is None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
|
@@ -39,16 +39,41 @@ SERVICE_TEST_NETWORK = "test_network"
|
||||
|
||||
EVENT_SCENE_ACTIVATED = "zwave.scene_activated"
|
||||
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 38
|
||||
COMMAND_CLASS_DOOR_LOCK = 98
|
||||
COMMAND_CLASS_WHATEVER = None
|
||||
COMMAND_CLASS_SENSOR_MULTILEVEL = 49
|
||||
COMMAND_CLASS_COLOR = 51
|
||||
COMMAND_CLASS_METER = 50
|
||||
COMMAND_CLASS_ALARM = 113
|
||||
COMMAND_CLASS_SWITCH_BINARY = 37
|
||||
COMMAND_CLASS_SENSOR_BINARY = 48
|
||||
COMMAND_CLASS_SENSOR_MULTILEVEL = 49
|
||||
COMMAND_CLASS_METER = 50
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 38
|
||||
COMMAND_CLASS_DOOR_LOCK = 98
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT = 67
|
||||
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68
|
||||
COMMAND_CLASS_BATTERY = 128
|
||||
COMMAND_CLASS_ALARM = 113 # 0x71
|
||||
COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43
|
||||
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 # 0x44
|
||||
|
||||
GENERIC_COMMAND_CLASS_WHATEVER = None
|
||||
GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH = 17
|
||||
GENERIC_COMMAND_CLASS_BINARY_SWITCH = 16
|
||||
GENERIC_COMMAND_CLASS_ENTRY_CONTROL = 64
|
||||
GENERIC_COMMAND_CLASS_BINARY_SENSOR = 32
|
||||
GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR = 33
|
||||
GENERIC_COMMAND_CLASS_METER = 49
|
||||
GENERIC_COMMAND_CLASS_ALARM_SENSOR = 161
|
||||
GENERIC_COMMAND_CLASS_THERMOSTAT = 8
|
||||
|
||||
SPECIFIC_DEVICE_CLASS_WHATEVER = None
|
||||
SPECIFIC_DEVICE_CLASS_NOT_USED = 0
|
||||
SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH = 1
|
||||
SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK = 2
|
||||
SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR = 3
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_KEYPAD_DOOR_LOCK = 3
|
||||
SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE = 4
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_DOOR = 5
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A = 5
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B = 6
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_BARRIER_ADD_ON = 7
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C = 7
|
||||
|
||||
GENRE_WHATEVER = None
|
||||
GENRE_USER = "User"
|
||||
@@ -60,38 +85,71 @@ TYPE_DECIMAL = "Decimal"
|
||||
|
||||
|
||||
# List of tuple (DOMAIN, discovered service, supported command classes,
|
||||
# value type).
|
||||
# value type, genre type, specific device class).
|
||||
DISCOVERY_COMPONENTS = [
|
||||
('sensor',
|
||||
[GENERIC_COMMAND_CLASS_WHATEVER],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
COMMAND_CLASS_METER,
|
||||
COMMAND_CLASS_ALARM],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_USER),
|
||||
('light',
|
||||
[GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH],
|
||||
[SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH,
|
||||
SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE],
|
||||
[COMMAND_CLASS_SWITCH_MULTILEVEL],
|
||||
TYPE_BYTE,
|
||||
GENRE_USER),
|
||||
('switch',
|
||||
[GENERIC_COMMAND_CLASS_BINARY_SWITCH],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_SWITCH_BINARY],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER),
|
||||
('binary_sensor',
|
||||
[GENERIC_COMMAND_CLASS_BINARY_SENSOR,
|
||||
GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_SENSOR_BINARY],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER),
|
||||
('thermostat',
|
||||
[GENERIC_COMMAND_CLASS_THERMOSTAT],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_THERMOSTAT_SETPOINT],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_WHATEVER),
|
||||
('hvac',
|
||||
[GENERIC_COMMAND_CLASS_THERMOSTAT],
|
||||
[SPECIFIC_DEVICE_CLASS_WHATEVER],
|
||||
[COMMAND_CLASS_THERMOSTAT_FAN_MODE],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_WHATEVER),
|
||||
('lock',
|
||||
[GENERIC_COMMAND_CLASS_ENTRY_CONTROL],
|
||||
[SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK,
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_KEYPAD_DOOR_LOCK],
|
||||
[COMMAND_CLASS_DOOR_LOCK],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER),
|
||||
('rollershutter',
|
||||
[GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH],
|
||||
[SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A,
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B,
|
||||
SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C,
|
||||
SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR],
|
||||
[COMMAND_CLASS_WHATEVER],
|
||||
TYPE_WHATEVER,
|
||||
GENRE_USER),
|
||||
('garage_door',
|
||||
[GENERIC_COMMAND_CLASS_ENTRY_CONTROL],
|
||||
[SPECIFIC_DEVICE_CLASS_SECURE_BARRIER_ADD_ON,
|
||||
SPECIFIC_DEVICE_CLASS_SECURE_DOOR],
|
||||
[COMMAND_CLASS_SWITCH_BINARY],
|
||||
TYPE_BOOL,
|
||||
GENRE_USER)
|
||||
]
|
||||
|
||||
|
||||
@@ -220,18 +278,49 @@ def setup(hass, config):
|
||||
def value_added(node, value):
|
||||
"""Called when a value is added to a node on the network."""
|
||||
for (component,
|
||||
command_ids,
|
||||
generic_device_class,
|
||||
specific_device_class,
|
||||
command_class,
|
||||
value_type,
|
||||
value_genre) in DISCOVERY_COMPONENTS:
|
||||
|
||||
if value.command_class not in command_ids:
|
||||
_LOGGER.debug("Component=%s Node_id=%s query start",
|
||||
component, node.node_id)
|
||||
if node.generic not in generic_device_class and \
|
||||
None not in generic_device_class:
|
||||
_LOGGER.debug("node.generic %s not None and in \
|
||||
generic_device_class %s",
|
||||
node.generic, generic_device_class)
|
||||
continue
|
||||
if value_type is not None and value_type != value.type:
|
||||
if node.specific not in specific_device_class and \
|
||||
None not in specific_device_class:
|
||||
_LOGGER.debug("node.specific %s is not None and in \
|
||||
specific_device_class %s", node.specific,
|
||||
specific_device_class)
|
||||
continue
|
||||
if value_genre is not None and value_genre != value.genre:
|
||||
if value.command_class not in command_class and \
|
||||
None not in command_class:
|
||||
_LOGGER.debug("value.command_class %s is not None \
|
||||
and in command_class %s",
|
||||
value.command_class, command_class)
|
||||
continue
|
||||
if value_type != value.type and value_type is not None:
|
||||
_LOGGER.debug("value.type %s != value_type %s",
|
||||
value.type, value_type)
|
||||
continue
|
||||
if value_genre != value.genre and value_genre is not None:
|
||||
_LOGGER.debug("value.genre %s != value_genre %s",
|
||||
value.genre, value_genre)
|
||||
continue
|
||||
|
||||
# Configure node
|
||||
_LOGGER.debug("Adding Node_id=%s Generic_command_class=%s, \
|
||||
Specific_command_class=%s, \
|
||||
Command_class=%s, Value type=%s, \
|
||||
Genre=%s", node.node_id,
|
||||
node.generic, node.specific,
|
||||
value.command_class, value.type,
|
||||
value.genre)
|
||||
name = "{}.{}".format(component, _object_id(value))
|
||||
|
||||
node_config = customize.get(name, {})
|
||||
|
@@ -1,31 +1,35 @@
|
||||
"""Module to help with parsing and generating configuration files."""
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from types import MappingProxyType
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.util.location as loc_util
|
||||
from homeassistant.const import (
|
||||
CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT,
|
||||
CONF_TIME_ZONE, CONF_CUSTOMIZE)
|
||||
CONF_TIME_ZONE, CONF_CUSTOMIZE, CONF_ELEVATION, TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS, __version__)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.yaml import load_yaml
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import valid_entity_id
|
||||
from homeassistant.helpers.entity import valid_entity_id, set_customize
|
||||
from homeassistant.util import dt as date_util, location as loc_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
YAML_CONFIG_FILE = 'configuration.yaml'
|
||||
VERSION_FILE = '.HA_VERSION'
|
||||
CONFIG_DIR_NAME = '.homeassistant'
|
||||
|
||||
DEFAULT_CONFIG = (
|
||||
# Tuples (attribute, default, auto detect property, description)
|
||||
(CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is '
|
||||
'running'),
|
||||
(CONF_LATITUDE, None, 'latitude', 'Location required to calculate the time'
|
||||
(CONF_LATITUDE, 0, 'latitude', 'Location required to calculate the time'
|
||||
' the sun rises and sets'),
|
||||
(CONF_LONGITUDE, None, 'longitude', None),
|
||||
(CONF_LONGITUDE, 0, 'longitude', None),
|
||||
(CONF_ELEVATION, 0, None, 'Impacts weather/sunrise data'),
|
||||
(CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celsius, F for Fahrenheit'),
|
||||
(CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki'
|
||||
'pedia.org/wiki/List_of_tz_database_time_zones'),
|
||||
@@ -39,7 +43,7 @@ DEFAULT_COMPONENTS = {
|
||||
'history:': 'Enables support for tracking state changes over time.',
|
||||
'logbook:': 'View all events in a logbook',
|
||||
'sun:': 'Track the sun',
|
||||
'sensor:\n platform: yr': 'Prediction of weather',
|
||||
'sensor:\n platform: yr': 'Weather Prediction',
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +65,7 @@ CORE_CONFIG_SCHEMA = vol.Schema({
|
||||
CONF_NAME: vol.Coerce(str),
|
||||
CONF_LATITUDE: cv.latitude,
|
||||
CONF_LONGITUDE: cv.longitude,
|
||||
CONF_ELEVATION: vol.Coerce(float),
|
||||
CONF_TEMPERATURE_UNIT: cv.temperature_unit,
|
||||
CONF_TIME_ZONE: cv.time_zone,
|
||||
vol.Required(CONF_CUSTOMIZE,
|
||||
@@ -97,6 +102,7 @@ def create_default_config(config_dir, detect_location=True):
|
||||
Return path to new config file if success, None if failed.
|
||||
"""
|
||||
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
|
||||
version_path = os.path.join(config_dir, VERSION_FILE)
|
||||
|
||||
info = {attr: default for attr, default, _, _ in DEFAULT_CONFIG}
|
||||
|
||||
@@ -111,6 +117,10 @@ def create_default_config(config_dir, detect_location=True):
|
||||
continue
|
||||
info[attr] = getattr(location_info, prop) or default
|
||||
|
||||
if location_info.latitude and location_info.longitude:
|
||||
info[CONF_ELEVATION] = loc_util.elevation(location_info.latitude,
|
||||
location_info.longitude)
|
||||
|
||||
# Writing files with YAML does not create the most human readable results
|
||||
# So we're hard coding a YAML template.
|
||||
try:
|
||||
@@ -130,6 +140,9 @@ def create_default_config(config_dir, detect_location=True):
|
||||
config_file.write("# {}\n".format(description))
|
||||
config_file.write("{}\n\n".format(component))
|
||||
|
||||
with open(version_path, 'wt') as version_file:
|
||||
version_file.write(__version__)
|
||||
|
||||
return config_path
|
||||
|
||||
except IOError:
|
||||
@@ -149,9 +162,118 @@ def load_yaml_config_file(config_path):
|
||||
conf_dict = load_yaml(config_path)
|
||||
|
||||
if not isinstance(conf_dict, dict):
|
||||
_LOGGER.error(
|
||||
'The configuration file %s does not contain a dictionary',
|
||||
msg = 'The configuration file {} does not contain a dictionary'.format(
|
||||
os.path.basename(config_path))
|
||||
raise HomeAssistantError()
|
||||
_LOGGER.error(msg)
|
||||
raise HomeAssistantError(msg)
|
||||
|
||||
return conf_dict
|
||||
|
||||
|
||||
def process_ha_config_upgrade(hass):
|
||||
"""Upgrade config if necessary."""
|
||||
version_path = hass.config.path(VERSION_FILE)
|
||||
|
||||
try:
|
||||
with open(version_path, 'rt') as inp:
|
||||
conf_version = inp.readline().strip()
|
||||
except FileNotFoundError:
|
||||
# Last version to not have this file
|
||||
conf_version = '0.7.7'
|
||||
|
||||
if conf_version == __version__:
|
||||
return
|
||||
|
||||
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
|
||||
__version__)
|
||||
|
||||
lib_path = hass.config.path('deps')
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
with open(version_path, 'wt') as outp:
|
||||
outp.write(__version__)
|
||||
|
||||
|
||||
def process_ha_core_config(hass, config):
|
||||
"""Process the [homeassistant] section from the config."""
|
||||
# pylint: disable=too-many-branches
|
||||
config = CORE_CONFIG_SCHEMA(config)
|
||||
hac = hass.config
|
||||
|
||||
def set_time_zone(time_zone_str):
|
||||
"""Helper method to set time zone."""
|
||||
if time_zone_str is None:
|
||||
return
|
||||
|
||||
time_zone = date_util.get_time_zone(time_zone_str)
|
||||
|
||||
if time_zone:
|
||||
hac.time_zone = time_zone
|
||||
date_util.set_default_time_zone(time_zone)
|
||||
else:
|
||||
_LOGGER.error('Received invalid time zone %s', time_zone_str)
|
||||
|
||||
for key, attr in ((CONF_LATITUDE, 'latitude'),
|
||||
(CONF_LONGITUDE, 'longitude'),
|
||||
(CONF_NAME, 'location_name'),
|
||||
(CONF_ELEVATION, 'elevation')):
|
||||
if key in config:
|
||||
setattr(hac, attr, config[key])
|
||||
|
||||
if CONF_TIME_ZONE in config:
|
||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||
|
||||
set_customize(config.get(CONF_CUSTOMIZE) or {})
|
||||
|
||||
if CONF_TEMPERATURE_UNIT in config:
|
||||
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
|
||||
|
||||
# Shortcut if no auto-detection necessary
|
||||
if None not in (hac.latitude, hac.longitude, hac.temperature_unit,
|
||||
hac.time_zone, hac.elevation):
|
||||
return
|
||||
|
||||
discovered = []
|
||||
|
||||
# If we miss some of the needed values, auto detect them
|
||||
if None in (hac.latitude, hac.longitude, hac.temperature_unit,
|
||||
hac.time_zone):
|
||||
info = loc_util.detect_location_info()
|
||||
|
||||
if info is None:
|
||||
_LOGGER.error('Could not detect location information')
|
||||
return
|
||||
|
||||
if hac.latitude is None and hac.longitude is None:
|
||||
hac.latitude = info.latitude
|
||||
hac.longitude = info.longitude
|
||||
discovered.append(('latitude', hac.latitude))
|
||||
discovered.append(('longitude', hac.longitude))
|
||||
|
||||
if hac.temperature_unit is None:
|
||||
if info.use_fahrenheit:
|
||||
hac.temperature_unit = TEMP_FAHRENHEIT
|
||||
discovered.append(('temperature_unit', 'F'))
|
||||
else:
|
||||
hac.temperature_unit = TEMP_CELSIUS
|
||||
discovered.append(('temperature_unit', 'C'))
|
||||
|
||||
if hac.location_name is None:
|
||||
hac.location_name = info.city
|
||||
discovered.append(('name', info.city))
|
||||
|
||||
if hac.time_zone is None:
|
||||
set_time_zone(info.time_zone)
|
||||
discovered.append(('time_zone', info.time_zone))
|
||||
|
||||
if hac.elevation is None and hac.latitude is not None and \
|
||||
hac.longitude is not None:
|
||||
elevation = loc_util.elevation(hac.latitude, hac.longitude)
|
||||
hac.elevation = elevation
|
||||
discovered.append(('elevation', elevation))
|
||||
|
||||
if discovered:
|
||||
_LOGGER.warning(
|
||||
'Incomplete core config. Auto detected %s',
|
||||
', '.join('{}: {}'.format(key, val) for key, val in discovered))
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# coding: utf-8
|
||||
"""Constants used by Home Assistant components."""
|
||||
|
||||
__version__ = "0.22.1"
|
||||
__version__ = "0.23.0"
|
||||
REQUIRED_PYTHON_VER = (3, 4)
|
||||
|
||||
PLATFORM_FORMAT = '{}.{}'
|
||||
|
@@ -49,6 +49,19 @@ MIN_WORKER_THREAD = 2
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CoreState(enum.Enum):
|
||||
"""Represent the current state of Home Assistant."""
|
||||
|
||||
not_running = "NOT_RUNNING"
|
||||
starting = "STARTING"
|
||||
running = "RUNNING"
|
||||
stopping = "STOPPING"
|
||||
|
||||
def __str__(self):
|
||||
"""Return the event."""
|
||||
return self.value
|
||||
|
||||
|
||||
class HomeAssistant(object):
|
||||
"""Root object of the Home Assistant home automation."""
|
||||
|
||||
@@ -59,14 +72,23 @@ class HomeAssistant(object):
|
||||
self.services = ServiceRegistry(self.bus, pool)
|
||||
self.states = StateMachine(self.bus)
|
||||
self.config = Config()
|
||||
self.state = CoreState.not_running
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
"""Return if Home Assistant is running."""
|
||||
return self.state == CoreState.running
|
||||
|
||||
def start(self):
|
||||
"""Start home assistant."""
|
||||
_LOGGER.info(
|
||||
"Starting Home Assistant (%d threads)", self.pool.worker_count)
|
||||
self.state = CoreState.starting
|
||||
|
||||
create_timer(self)
|
||||
self.bus.fire(EVENT_HOMEASSISTANT_START)
|
||||
self.pool.block_till_done()
|
||||
self.state = CoreState.running
|
||||
|
||||
def block_till_stopped(self):
|
||||
"""Register service homeassistant/stop and will block until called."""
|
||||
@@ -113,8 +135,10 @@ class HomeAssistant(object):
|
||||
def stop(self):
|
||||
"""Stop Home Assistant and shuts down all threads."""
|
||||
_LOGGER.info("Stopping")
|
||||
self.state = CoreState.stopping
|
||||
self.bus.fire(EVENT_HOMEASSISTANT_STOP)
|
||||
self.pool.stop()
|
||||
self.state = CoreState.not_running
|
||||
|
||||
|
||||
class JobPriority(util.OrderedEnum):
|
||||
@@ -456,7 +480,7 @@ class StateMachine(object):
|
||||
|
||||
return True
|
||||
|
||||
def set(self, entity_id, new_state, attributes=None):
|
||||
def set(self, entity_id, new_state, attributes=None, force_update=False):
|
||||
"""Set the state of an entity, add entity if it does not exist.
|
||||
|
||||
Attributes is an optional dict to specify attributes of this state.
|
||||
@@ -472,7 +496,8 @@ class StateMachine(object):
|
||||
old_state = self._states.get(entity_id)
|
||||
|
||||
is_existing = old_state is not None
|
||||
same_state = is_existing and old_state.state == new_state
|
||||
same_state = (is_existing and old_state.state == new_state and
|
||||
not force_update)
|
||||
same_attr = is_existing and old_state.attributes == attributes
|
||||
|
||||
if same_state and same_attr:
|
||||
@@ -680,6 +705,7 @@ class Config(object):
|
||||
"""Initialize a new config object."""
|
||||
self.latitude = None
|
||||
self.longitude = None
|
||||
self.elevation = None
|
||||
self.temperature_unit = None
|
||||
self.location_name = None
|
||||
self.time_zone = None
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"""An abstract class for entities."""
|
||||
import logging
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON,
|
||||
@@ -10,8 +10,10 @@ from homeassistant.const import (
|
||||
from homeassistant.exceptions import NoEntitySpecifiedError
|
||||
from homeassistant.util import ensure_unique_string, slugify
|
||||
|
||||
# Dict mapping entity_id to a boolean that overwrites the hidden property
|
||||
_OVERWRITE = defaultdict(dict)
|
||||
# Entity attributes that we will overwrite
|
||||
_OVERWRITE = {}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Pattern for validating entity IDs (format: <domain>.<entity>)
|
||||
ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$")
|
||||
@@ -22,7 +24,7 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
|
||||
name = (name or DEVICE_DEFAULT_NAME).lower()
|
||||
if current_ids is None:
|
||||
if hass is None:
|
||||
raise RuntimeError("Missing required parameter currentids or hass")
|
||||
raise ValueError("Missing required parameter currentids or hass")
|
||||
|
||||
current_ids = hass.states.entity_ids()
|
||||
|
||||
@@ -30,6 +32,13 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
|
||||
entity_id_format.format(slugify(name)), current_ids)
|
||||
|
||||
|
||||
def set_customize(customize):
|
||||
"""Overwrite all current customize settings."""
|
||||
global _OVERWRITE
|
||||
|
||||
_OVERWRITE = {key.lower(): val for key, val in customize.items()}
|
||||
|
||||
|
||||
def split_entity_id(entity_id):
|
||||
"""Split a state entity_id into domain, object_id."""
|
||||
return entity_id.split(".", 1)
|
||||
@@ -116,6 +125,15 @@ class Entity(object):
|
||||
"""Return True if unable to access real state of the entity."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def force_update(self):
|
||||
"""Return True if state updates should be forced.
|
||||
|
||||
If True, a state change will be triggered anytime the state property is
|
||||
updated, not just when the value changes.
|
||||
"""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
pass
|
||||
@@ -181,7 +199,8 @@ class Entity(object):
|
||||
state, attr[ATTR_UNIT_OF_MEASUREMENT])
|
||||
state = str(state)
|
||||
|
||||
return self.hass.states.set(self.entity_id, state, attr)
|
||||
return self.hass.states.set(
|
||||
self.entity_id, state, attr, self.force_update)
|
||||
|
||||
def _attr_setter(self, name, typ, attr, attrs):
|
||||
"""Helper method to populate attributes based on properties."""
|
||||
@@ -207,20 +226,6 @@ class Entity(object):
|
||||
"""Return the representation."""
|
||||
return "<Entity {}: {}>".format(self.name, self.state)
|
||||
|
||||
@staticmethod
|
||||
def overwrite_attribute(entity_id, attrs, vals):
|
||||
"""Overwrite any attribute of an entity.
|
||||
|
||||
This function should receive a list of attributes and a
|
||||
list of values. Set attribute to None to remove any overwritten
|
||||
value in place.
|
||||
"""
|
||||
for attr, val in zip(attrs, vals):
|
||||
if val is None:
|
||||
_OVERWRITE[entity_id.lower()].pop(attr, None)
|
||||
else:
|
||||
_OVERWRITE[entity_id.lower()][attr] = val
|
||||
|
||||
|
||||
class ToggleEntity(Entity):
|
||||
"""An abstract class for entities that can be turned on and off."""
|
||||
@@ -234,15 +239,15 @@ class ToggleEntity(Entity):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if entity is on."""
|
||||
return False
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the entity on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def toggle(self, **kwargs):
|
||||
"""Toggle the entity off."""
|
||||
|
@@ -11,6 +11,7 @@ from datetime import datetime
|
||||
import enum
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import threading
|
||||
import urllib.parse
|
||||
|
||||
@@ -123,6 +124,7 @@ class HomeAssistant(ha.HomeAssistant):
|
||||
self.services = ha.ServiceRegistry(self.bus, pool)
|
||||
self.states = StateMachine(self.bus, self.remote_api)
|
||||
self.config = ha.Config()
|
||||
self.state = ha.CoreState.not_running
|
||||
|
||||
self.config.api = local_api
|
||||
|
||||
@@ -134,17 +136,20 @@ class HomeAssistant(ha.HomeAssistant):
|
||||
raise HomeAssistantError(
|
||||
'Unable to setup local API to receive events')
|
||||
|
||||
self.state = ha.CoreState.starting
|
||||
ha.create_timer(self)
|
||||
|
||||
self.bus.fire(ha.EVENT_HOMEASSISTANT_START,
|
||||
origin=ha.EventOrigin.remote)
|
||||
|
||||
# Give eventlet time to startup
|
||||
import eventlet
|
||||
eventlet.sleep(0.1)
|
||||
# Ensure local HTTP is started
|
||||
self.pool.block_till_done()
|
||||
self.state = ha.CoreState.running
|
||||
time.sleep(0.05)
|
||||
|
||||
# Setup that events from remote_api get forwarded to local_api
|
||||
# Do this after we fire START, otherwise HTTP is not started
|
||||
# Do this after we are running, otherwise HTTP is not started
|
||||
# or requests are blocked
|
||||
if not connect_remote_events(self.remote_api, self.config.api):
|
||||
raise HomeAssistantError((
|
||||
'Could not setup event forwarding from api {} to '
|
||||
@@ -153,6 +158,7 @@ class HomeAssistant(ha.HomeAssistant):
|
||||
def stop(self):
|
||||
"""Stop Home Assistant and shuts down all threads."""
|
||||
_LOGGER.info("Stopping")
|
||||
self.state = ha.CoreState.stopping
|
||||
|
||||
self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP,
|
||||
origin=ha.EventOrigin.remote)
|
||||
@@ -161,6 +167,7 @@ class HomeAssistant(ha.HomeAssistant):
|
||||
|
||||
# Disconnect master event forwarding
|
||||
disconnect_remote_events(self.remote_api, self.config.api)
|
||||
self.state = ha.CoreState.not_running
|
||||
|
||||
|
||||
class EventBus(ha.EventBus):
|
||||
@@ -259,9 +266,9 @@ class StateMachine(ha.StateMachine):
|
||||
"""
|
||||
return remove_state(self._api, entity_id)
|
||||
|
||||
def set(self, entity_id, new_state, attributes=None):
|
||||
def set(self, entity_id, new_state, attributes=None, force_update=False):
|
||||
"""Call set_state on remote API."""
|
||||
set_state(self._api, entity_id, new_state, attributes)
|
||||
set_state(self._api, entity_id, new_state, attributes, force_update)
|
||||
|
||||
def mirror(self):
|
||||
"""Discard current data and mirrors the remote state machine."""
|
||||
@@ -450,7 +457,7 @@ def remove_state(api, entity_id):
|
||||
return False
|
||||
|
||||
|
||||
def set_state(api, entity_id, new_state, attributes=None):
|
||||
def set_state(api, entity_id, new_state, attributes=None, force_update=False):
|
||||
"""Tell API to update state for entity_id.
|
||||
|
||||
Return True if success.
|
||||
@@ -458,7 +465,8 @@ def set_state(api, entity_id, new_state, attributes=None):
|
||||
attributes = attributes or {}
|
||||
|
||||
data = {'state': new_state,
|
||||
'attributes': attributes}
|
||||
'attributes': attributes,
|
||||
'force_update': force_update}
|
||||
|
||||
try:
|
||||
req = api(METHOD_POST,
|
||||
|
@@ -8,7 +8,8 @@ import math
|
||||
import requests
|
||||
|
||||
ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json'
|
||||
DATA_SOURCE = ['https://freegeoip.io/json/', 'http://ip-api.com/json']
|
||||
FREEGEO_API = 'https://freegeoip.io/json/'
|
||||
IP_API = 'http://ip-api.com/json'
|
||||
|
||||
# Constants from https://github.com/maurycyp/vincenty
|
||||
# Earth ellipsoid according to WGS 84
|
||||
@@ -32,30 +33,13 @@ LocationInfo = collections.namedtuple(
|
||||
|
||||
def detect_location_info():
|
||||
"""Detect location information."""
|
||||
success = None
|
||||
data = _get_freegeoip()
|
||||
|
||||
for source in DATA_SOURCE:
|
||||
try:
|
||||
raw_info = requests.get(source, timeout=5).json()
|
||||
success = source
|
||||
break
|
||||
except (requests.RequestException, ValueError):
|
||||
success = False
|
||||
if data is None:
|
||||
data = _get_ip_api()
|
||||
|
||||
if success is False:
|
||||
if data is None:
|
||||
return None
|
||||
else:
|
||||
data = {key: raw_info.get(key) for key in LocationInfo._fields}
|
||||
if success is DATA_SOURCE[1]:
|
||||
data['ip'] = raw_info.get('query')
|
||||
data['country_code'] = raw_info.get('countryCode')
|
||||
data['country_name'] = raw_info.get('country')
|
||||
data['region_code'] = raw_info.get('region')
|
||||
data['region_name'] = raw_info.get('regionName')
|
||||
data['zip_code'] = raw_info.get('zip')
|
||||
data['time_zone'] = raw_info.get('timezone')
|
||||
data['latitude'] = raw_info.get('lat')
|
||||
data['longitude'] = raw_info.get('lon')
|
||||
|
||||
# From Wikipedia: Fahrenheit is used in the Bahamas, Belize,
|
||||
# the Cayman Islands, Palau, and the United States and associated
|
||||
@@ -73,11 +57,16 @@ def distance(lat1, lon1, lat2, lon2):
|
||||
|
||||
def elevation(latitude, longitude):
|
||||
"""Return elevation for given latitude and longitude."""
|
||||
req = requests.get(ELEVATION_URL,
|
||||
params={'locations': '{},{}'.format(latitude,
|
||||
longitude),
|
||||
'sensor': 'false'},
|
||||
try:
|
||||
req = requests.get(
|
||||
ELEVATION_URL,
|
||||
params={
|
||||
'locations': '{},{}'.format(latitude, longitude),
|
||||
'sensor': 'false',
|
||||
},
|
||||
timeout=10)
|
||||
except requests.RequestException:
|
||||
return 0
|
||||
|
||||
if req.status_code != 200:
|
||||
return 0
|
||||
@@ -157,3 +146,45 @@ def vincenty(point1, point2, miles=False):
|
||||
s *= MILES_PER_KILOMETER # kilometers to miles
|
||||
|
||||
return round(s, 6)
|
||||
|
||||
|
||||
def _get_freegeoip():
|
||||
"""Query freegeoip.io for location data."""
|
||||
try:
|
||||
raw_info = requests.get(FREEGEO_API, timeout=5).json()
|
||||
except (requests.RequestException, ValueError):
|
||||
return None
|
||||
|
||||
return {
|
||||
'ip': raw_info.get('ip'),
|
||||
'country_code': raw_info.get('country_code'),
|
||||
'country_name': raw_info.get('country_name'),
|
||||
'region_code': raw_info.get('region_code'),
|
||||
'region_name': raw_info.get('region_name'),
|
||||
'city': raw_info.get('city'),
|
||||
'zip_code': raw_info.get('zip_code'),
|
||||
'time_zone': raw_info.get('time_zone'),
|
||||
'latitude': raw_info.get('latitude'),
|
||||
'longitude': raw_info.get('longitude'),
|
||||
}
|
||||
|
||||
|
||||
def _get_ip_api():
|
||||
"""Query ip-api.com for location data."""
|
||||
try:
|
||||
raw_info = requests.get(IP_API, timeout=5).json()
|
||||
except (requests.RequestException, ValueError):
|
||||
return None
|
||||
|
||||
return {
|
||||
'ip': raw_info.get('query'),
|
||||
'country_code': raw_info.get('countryCode'),
|
||||
'country_name': raw_info.get('country'),
|
||||
'region_code': raw_info.get('region'),
|
||||
'region_name': raw_info.get('regionName'),
|
||||
'city': raw_info.get('city'),
|
||||
'zip_code': raw_info.get('zip'),
|
||||
'time_zone': raw_info.get('timezone'),
|
||||
'latitude': raw_info.get('lat'),
|
||||
'longitude': raw_info.get('lon'),
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user