mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 05:37:44 +00:00
commit
6bc504bfcc
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:
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
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.
@ -1 +1 @@
|
||||
Subproject commit 168706fdb192219d8074d6709c0ce686180d1c8a
|
||||
Subproject commit 1e1a3a1c845713508d21d7c1cb87a7ecee6222aa
|
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,7 +47,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveDimmer(value)])
|
||||
|
||||
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)])
|
||||
|
||||
|
||||
def brightness_state(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.
|
||||
example: 'media_player.living_room_sonos'
|
||||
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_arrival': 0})
|
||||
# IMHO only delay_departure is usefull
|
||||
delay = con.get('delay', {'delay_departure': 0,
|
||||
'delay_arrival': 0})
|
||||
# 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,19 +11,29 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||
'co2': ['CO2', 'ppm', 'mdi:cloud'],
|
||||
'pressure': ['Pressure', 'mbar', 'mdi:gauge'],
|
||||
'noise': ['Noise', 'dB', 'mdi:volume-high'],
|
||||
'humidity': ['Humidity', '%', 'mdi:water-percent'],
|
||||
'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'],
|
||||
'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||
'co2': ['CO2', 'ppm', 'mdi:cloud'],
|
||||
'pressure': ['Pressure', 'mbar', 'mdi:gauge'],
|
||||
'noise': ['Noise', 'dB', 'mdi:volume-high'],
|
||||
'humidity': ['Humidity', '%', 'mdi:water-percent'],
|
||||
'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,14 +91,27 @@ class OneWire(Entity):
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from the device."""
|
||||
lines = self._read_temp_raw()
|
||||
while lines[0].strip()[-3:] != 'YES':
|
||||
time.sleep(0.2)
|
||||
temp = -99
|
||||
if self._device_file.startswith('/sys/bus/w1/devices'):
|
||||
lines = self._read_temp_raw()
|
||||
equals_pos = lines[1].find('t=')
|
||||
if equals_pos != -1:
|
||||
temp_string = lines[1][equals_pos+2:]
|
||||
temp = round(float(temp_string) / 1000.0, 1)
|
||||
if temp < -55 or temp > 125:
|
||||
return
|
||||
self._state = temp
|
||||
while lines[0].strip()[-3:] != 'YES':
|
||||
time.sleep(0.2)
|
||||
lines = self._read_temp_raw()
|
||||
equals_pos = lines[1].find('t=')
|
||||
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'},
|
||||
timeout=10)
|
||||
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
Loading…
x
Reference in New Issue
Block a user