Merge pull request #2952 from home-assistant/dev

0.27
This commit is contained in:
Robbie Trencheny 2016-08-27 20:27:52 -07:00 committed by GitHub
commit 7b2f0e709b
274 changed files with 14372 additions and 2054 deletions

View File

@ -101,18 +101,29 @@ omit =
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/ffmpeg.py
homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py homeassistant/components/browser.py
homeassistant/components/camera/bloomsky.py homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
homeassistant/components/camera/generic.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/rpi_camera.py
homeassistant/components/climate/eq3btsmart.py
homeassistant/components/climate/heatmiser.py
homeassistant/components/climate/homematic.py
homeassistant/components/climate/knx.py
homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py
homeassistant/components/cover/wink.py
homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/bluetooth_tracker.py homeassistant/components/device_tracker/bluetooth_tracker.py
homeassistant/components/device_tracker/bluetooth_le_tracker.py
homeassistant/components/device_tracker/bt_home_hub_5.py homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
@ -173,8 +184,10 @@ omit =
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py
homeassistant/components/notify/instapush.py homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/message_bird.py homeassistant/components/notify/message_bird.py
homeassistant/components/notify/nma.py homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
@ -202,13 +215,17 @@ omit =
homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/forecast.py homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gpsd.py homeassistant/components/sensor/gpsd.py
homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/mhz19.py
homeassistant/components/sensor/mqtt_room.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nzbget.py homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/ohmconnect.py homeassistant/components/sensor/ohmconnect.py

View File

@ -19,6 +19,7 @@ import homeassistant.config as conf_util
import homeassistant.core as core import homeassistant.core as core
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.util.package as pkg_util import homeassistant.util.package as pkg_util
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import ( from homeassistant.helpers import (
@ -103,7 +104,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
try: try:
config = component.CONFIG_SCHEMA(config) config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex: except vol.MultipleInvalid as ex:
_log_exception(ex, domain, config) log_exception(ex, domain, config)
return False return False
elif hasattr(component, 'PLATFORM_SCHEMA'): elif hasattr(component, 'PLATFORM_SCHEMA'):
@ -113,7 +114,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
try: try:
p_validated = component.PLATFORM_SCHEMA(p_config) p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex: except vol.MultipleInvalid as ex:
_log_exception(ex, domain, p_config) log_exception(ex, domain, p_config)
return False return False
# Not all platform components follow same pattern for platforms # Not all platform components follow same pattern for platforms
@ -134,8 +135,8 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
try: try:
p_validated = platform.PLATFORM_SCHEMA(p_validated) p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex: except vol.MultipleInvalid as ex:
_log_exception(ex, '{}.{}'.format(domain, p_name), log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated) p_validated)
return False return False
platforms.append(p_validated) platforms.append(p_validated)
@ -239,7 +240,7 @@ def from_config_dict(config: Dict[str, Any],
try: try:
conf_util.process_ha_core_config(hass, core_config) conf_util.process_ha_core_config(hass, core_config)
except vol.Invalid as ex: except vol.Invalid as ex:
_log_exception(ex, 'homeassistant', core_config) log_exception(ex, 'homeassistant', core_config)
return None return None
conf_util.process_ha_config_upgrade(hass) conf_util.process_ha_config_upgrade(hass)
@ -308,6 +309,8 @@ def from_config_file(config_path: str,
config_dict = conf_util.load_yaml_config_file(config_path) config_dict = conf_util.load_yaml_config_file(config_path)
except HomeAssistantError: except HomeAssistantError:
return None return None
finally:
clear_secret_cache()
return from_config_dict(config_dict, hass, enable_log=False, return from_config_dict(config_dict, hass, enable_log=False,
skip_pip=skip_pip) skip_pip=skip_pip)
@ -371,7 +374,7 @@ def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
loader.prepare(hass) loader.prepare(hass)
def _log_exception(ex, domain, config): def log_exception(ex, domain, config):
"""Generate log exception for config validation.""" """Generate log exception for config validation."""
message = 'Invalid config for [{}]: '.format(domain) message = 'Invalid config for [{}]: '.format(domain)
if 'extra keys not allowed' in ex.error_message: if 'extra keys not allowed' in ex.error_message:

View File

@ -80,7 +80,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
if not self._validate_code(code, 'arming home'): if not self._validate_code(code, 'disarming home'):
return return
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
# Open another session to alarm.com to fire off the command # Open another session to alarm.com to fire off the command

View File

@ -10,5 +10,5 @@ import homeassistant.components.alarm_control_panel.manual as manual
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo alarm control panel platform.""" """Setup the Demo alarm control panel platform."""
add_devices([ add_devices([
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10), manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False),
]) ])

View File

@ -7,28 +7,46 @@ https://home-assistant.io/components/alarm_control_panel.manual/
import datetime import datetime
import logging import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME,
CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
_LOGGER = logging.getLogger(__name__)
DEFAULT_ALARM_NAME = 'HA Alarm' DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_PENDING_TIME = 60 DEFAULT_PENDING_TIME = 60
DEFAULT_TRIGGER_TIME = 120 DEFAULT_TRIGGER_TIME = 120
DEFAULT_DISARM_AFTER_TRIGGER = False
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'manual',
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
vol.Optional(CONF_CODE): cv.string,
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
})
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the manual alarm platform.""" """Setup the manual alarm platform."""
add_devices([ManualAlarm( add_devices([ManualAlarm(
hass, hass,
config.get('name', DEFAULT_ALARM_NAME), config[CONF_NAME],
config.get('code'), config.get(CONF_CODE),
config.get('pending_time', DEFAULT_PENDING_TIME), config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME),
config.get('trigger_time', DEFAULT_TRIGGER_TIME), config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME),
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER)
)]) )])
@ -40,10 +58,12 @@ class ManualAlarm(alarm.AlarmControlPanel):
When armed, will be pending for 'pending_time', after that armed. When armed, will be pending for 'pending_time', after that armed.
When triggered, will be pending for 'trigger_time'. After that will be When triggered, will be pending for 'trigger_time'. After that will be
triggered for 'trigger_time', after that we return to disarmed. triggered for 'trigger_time', after that we return to the previous state
or disarm if `disarm_after_trigger` is true.
""" """
def __init__(self, hass, name, code, pending_time, trigger_time): def __init__(self, hass, name, code, pending_time,
trigger_time, disarm_after_trigger):
"""Initalize the manual alarm panel.""" """Initalize the manual alarm panel."""
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._hass = hass self._hass = hass
@ -51,6 +71,8 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._code = str(code) if code else None self._code = str(code) if code else None
self._pending_time = datetime.timedelta(seconds=pending_time) self._pending_time = datetime.timedelta(seconds=pending_time)
self._trigger_time = datetime.timedelta(seconds=trigger_time) self._trigger_time = datetime.timedelta(seconds=trigger_time)
self._disarm_after_trigger = disarm_after_trigger
self._pre_trigger_state = self._state
self._state_ts = None self._state_ts = None
@property @property
@ -77,7 +99,10 @@ class ManualAlarm(alarm.AlarmControlPanel):
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
elif (self._state_ts + self._pending_time + elif (self._state_ts + self._pending_time +
self._trigger_time) < dt_util.utcnow(): self._trigger_time) < dt_util.utcnow():
return STATE_ALARM_DISARMED if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
else:
return self._pre_trigger_state
return self._state return self._state
@ -125,6 +150,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
def alarm_trigger(self, code=None): def alarm_trigger(self, code=None):
"""Send alarm trigger command. No code needed.""" """Send alarm trigger command. No code needed."""
self._pre_trigger_state = self._state
self._state = STATE_ALARM_TRIGGERED self._state = STATE_ALARM_TRIGGERED
self._state_ts = dt_util.utcnow() self._state_ts = dt_util.utcnow()
self.update_ha_state() self.update_ha_state()

View File

@ -11,17 +11,17 @@ from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script from homeassistant.helpers import template, script
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
DOMAIN = 'alexa'
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
API_ENDPOINT = '/api/alexa' API_ENDPOINT = '/api/alexa'
CONF_INTENTS = 'intents'
CONF_CARD = 'card'
CONF_SPEECH = 'speech'
CONF_ACTION = 'action' CONF_ACTION = 'action'
CONF_CARD = 'card'
CONF_INTENTS = 'intents'
CONF_SPEECH = 'speech'
DOMAIN = 'alexa'
DEPENDENCIES = ['http']
def setup(hass, config): def setup(hass, config):

View File

@ -6,27 +6,34 @@ https://home-assistant.io/components/arduino/
""" """
import logging import logging
import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import validate_config from homeassistant.const import CONF_PORT
import homeassistant.helpers.config_validation as cv
DOMAIN = "arduino"
REQUIREMENTS = ['PyMata==2.12'] REQUIREMENTS = ['PyMata==2.12']
BOARD = None
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
BOARD = None
DOMAIN = 'arduino'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PORT): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config): def setup(hass, config):
"""Setup the Arduino component.""" """Setup the Arduino component."""
if not validate_config(config,
{DOMAIN: ['port']},
_LOGGER):
return False
import serial import serial
global BOARD global BOARD
try: try:
BOARD = ArduinoBoard(config[DOMAIN]['port']) BOARD = ArduinoBoard(config[DOMAIN][CONF_PORT])
except (serial.serialutil.SerialException, FileNotFoundError): except (serial.serialutil.SerialException, FileNotFoundError):
_LOGGER.exception("Your port is not accessible.") _LOGGER.exception("Your port is not accessible.")
return False return False

View File

@ -6,6 +6,8 @@ https://home-assistant.io/components/binary_sensor/
""" """
import logging import logging
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_ON, STATE_OFF) from homeassistant.const import (STATE_ON, STATE_OFF)
@ -33,6 +35,8 @@ SENSOR_CLASSES = [
'vibration', # On means vibration detected, Off means no vibration 'vibration', # On means vibration detected, Off means no vibration
] ]
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES))
def setup(hass, config): def setup(hass, config):
"""Track states and offer events for binary sensors.""" """Track states and offer events for binary sensors."""

View File

@ -0,0 +1,72 @@
"""
Support for Ecobee sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.ecobee/
"""
from homeassistant.components import ecobee
from homeassistant.components.binary_sensor import BinarySensorDevice
DEPENDENCIES = ['ecobee']
ECOBEE_CONFIG_FILE = 'ecobee.conf'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Ecobee sensors."""
if discovery_info is None:
return
data = ecobee.NETWORK
dev = list()
for index in range(len(data.ecobee.thermostats)):
for sensor in data.ecobee.get_remote_sensors(index):
for item in sensor['capability']:
if item['type'] != 'occupancy':
continue
dev.append(EcobeeBinarySensor(sensor['name'], index))
add_devices(dev)
class EcobeeBinarySensor(BinarySensorDevice):
"""Representation of an Ecobee sensor."""
def __init__(self, sensor_name, sensor_index):
"""Initialize the sensor."""
self._name = sensor_name + ' Occupancy'
self.sensor_name = sensor_name
self.index = sensor_index
self._state = None
self._sensor_class = 'motion'
self.update()
@property
def name(self):
"""Return the name of the Ecobee sensor."""
return self._name.rstrip()
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state == 'true'
@property
def unique_id(self):
"""Return the unique ID of this sensor."""
return "binary_sensor_ecobee_{}_{}".format(self._name, self.index)
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return self._sensor_class
def update(self):
"""Get the latest state of the sensor."""
data = ecobee.NETWORK
data.update()
for sensor in data.ecobee.get_remote_sensors(self.index):
for item in sensor['capability']:
if (item['type'] == 'occupancy' and
self.sensor_name == sensor['name']):
self._state = item['value']

View File

@ -4,27 +4,41 @@ Support for EnOcean binary sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.enocean/ https://home-assistant.io/components/binary_sensor.enocean/
""" """
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
from homeassistant.components import enocean from homeassistant.components import enocean
from homeassistant.const import CONF_NAME from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SENSOR_CLASS)
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ["enocean"] _LOGGER = logging.getLogger(__name__)
CONF_ID = "id" DEPENDENCIES = ['enocean']
DEFAULT_NAME = 'EnOcean binary sensor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Binary Sensor platform fo EnOcean.""" """Setup the Binary Sensor platform fo EnOcean."""
dev_id = config.get(CONF_ID, None) dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME, "EnOcean binary sensor") devname = config.get(CONF_NAME)
add_devices([EnOceanBinarySensor(dev_id, devname)]) sensor_class = config.get(CONF_SENSOR_CLASS)
add_devices([EnOceanBinarySensor(dev_id, devname, sensor_class)])
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
"""Representation of EnOcean binary sensors such as wall switches.""" """Representation of EnOcean binary sensors such as wall switches."""
def __init__(self, dev_id, devname): def __init__(self, dev_id, devname, sensor_class):
"""Initialize the EnOcean binary sensor.""" """Initialize the EnOcean binary sensor."""
enocean.EnOceanDevice.__init__(self) enocean.EnOceanDevice.__init__(self)
self.stype = "listener" self.stype = "listener"
@ -32,12 +46,18 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
self.which = -1 self.which = -1
self.onoff = -1 self.onoff = -1
self.devname = devname self.devname = devname
self._sensor_class = sensor_class
@property @property
def name(self): def name(self):
"""The default name for the binary sensor.""" """The default name for the binary sensor."""
return self.devname return self.devname
@property
def sensor_class(self):
"""Return the class of this sensor."""
return self._sensor_class
def value_changed(self, value, value2): def value_changed(self, value, value2):
"""Fire an event with the data that have changed. """Fire an event with the data that have changed.

View File

@ -0,0 +1,215 @@
"""
Provides a binary sensor which is a collection of ffmpeg tools.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ffmpeg/
"""
import logging
from os import path
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (BinarySensorDevice,
PLATFORM_SCHEMA, DOMAIN)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME,
ATTR_ENTITY_ID)
REQUIREMENTS = ["ha-ffmpeg==0.8"]
SERVICE_RESTART = 'ffmpeg_restart'
FFMPEG_SENSOR_NOISE = 'noise'
FFMPEG_SENSOR_MOTION = 'motion'
MAP_FFMPEG_BIN = [
FFMPEG_SENSOR_NOISE,
FFMPEG_SENSOR_MOTION
]
CONF_TOOL = 'tool'
CONF_INPUT = 'input'
CONF_FFMPEG_BIN = 'ffmpeg_bin'
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
CONF_OUTPUT = 'output'
CONF_PEAK = 'peak'
CONF_DURATION = 'duration'
CONF_RESET = 'reset'
CONF_CHANGES = 'changes'
CONF_REPEAT = 'repeat'
CONF_REPEAT_TIME = 'repeat_time'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN),
vol.Required(CONF_INPUT): cv.string,
vol.Optional(CONF_FFMPEG_BIN, default="ffmpeg"): cv.string,
vol.Optional(CONF_NAME, default="FFmpeg"): cv.string,
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
vol.Optional(CONF_OUTPUT): cv.string,
vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int),
vol.Optional(CONF_DURATION, default=1):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_RESET, default=10):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_CHANGES, default=10):
vol.All(vol.Coerce(float), vol.Range(min=0, max=99)),
vol.Optional(CONF_REPEAT, default=0):
vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.Optional(CONF_REPEAT_TIME, default=0):
vol.All(vol.Coerce(int), vol.Range(min=0)),
})
SERVICE_RESTART_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
# list of all ffmpeg sensors
DEVICES = []
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the binary sensor."""
from haffmpeg import SensorNoise, SensorMotion
if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE:
entity = FFmpegNoise(SensorNoise, config)
else:
entity = FFmpegMotion(SensorMotion, config)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, entity.shutdown_ffmpeg)
# add to system
add_entities([entity])
DEVICES.append(entity)
# exists service?
if hass.services.has_service(DOMAIN, SERVICE_RESTART):
return True
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
# register service
def _service_handle_restart(service):
"""Handle service binary_sensor.ffmpeg_restart."""
entity_ids = service.data.get('entity_id')
if entity_ids:
_devices = [device for device in DEVICES
if device.entity_id in entity_ids]
else:
_devices = DEVICES
for device in _devices:
device.reset_ffmpeg()
hass.services.register(DOMAIN, SERVICE_RESTART,
_service_handle_restart,
descriptions.get(SERVICE_RESTART),
schema=SERVICE_RESTART_SCHEMA)
return True
class FFmpegBinarySensor(BinarySensorDevice):
"""A binary sensor which use ffmpeg for noise detection."""
def __init__(self, ffobj, config):
"""Constructor for binary sensor noise detection."""
self._state = False
self._config = config
self._name = config.get(CONF_NAME)
self._ffmpeg = ffobj(config.get(CONF_FFMPEG_BIN), self._callback)
self._start_ffmpeg(config)
def _callback(self, state):
"""HA-FFmpeg callback for noise detection."""
self._state = state
self.update_ha_state()
def _start_ffmpeg(self, config):
"""Start a FFmpeg instance."""
raise NotImplementedError
def shutdown_ffmpeg(self, event):
"""For STOP event to shutdown ffmpeg."""
self._ffmpeg.close()
def reset_ffmpeg(self):
"""Restart ffmpeg with new config."""
self._ffmpeg.close()
self._start_ffmpeg(self._config)
@property
def is_on(self):
"""True if the binary sensor is on."""
return self._state
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return False
@property
def name(self):
"""Return the name of the entity."""
return self._name
@property
def available(self):
"""Return True if entity is available."""
return self._ffmpeg.is_running
class FFmpegNoise(FFmpegBinarySensor):
"""A binary sensor which use ffmpeg for noise detection."""
def _start_ffmpeg(self, config):
"""Start a FFmpeg instance."""
# init config
self._ffmpeg.set_options(
time_duration=config.get(CONF_DURATION),
time_reset=config.get(CONF_RESET),
peak=config.get(CONF_PEAK),
)
# run
self._ffmpeg.open_sensor(
input_source=config.get(CONF_INPUT),
output_dest=config.get(CONF_OUTPUT),
extra_cmd=config.get(CONF_EXTRA_ARGUMENTS),
)
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return "sound"
class FFmpegMotion(FFmpegBinarySensor):
"""A binary sensor which use ffmpeg for noise detection."""
def _start_ffmpeg(self, config):
"""Start a FFmpeg instance."""
# init config
self._ffmpeg.set_options(
time_reset=config.get(CONF_RESET),
time_repeat=config.get(CONF_REPEAT_TIME),
repeat=config.get(CONF_REPEAT),
changes=config.get(CONF_CHANGES),
)
# run
self._ffmpeg.open_sensor(
input_source=config.get(CONF_INPUT),
extra_cmd=config.get(CONF_EXTRA_ARGUMENTS),
)
@property
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return "motion"

View File

@ -20,7 +20,9 @@ SENSOR_TYPES_CLASS = {
"SmokeV2": "smoke", "SmokeV2": "smoke",
"Motion": "motion", "Motion": "motion",
"MotionV2": "motion", "MotionV2": "motion",
"RemoteMotion": None "RemoteMotion": None,
"WeatherSensor": None,
"TiltSensor": None,
} }

View File

@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pres.S_MOTION: [set_req.V_TRIPPED], pres.S_MOTION: [set_req.V_TRIPPED],
pres.S_SMOKE: [set_req.V_TRIPPED], pres.S_SMOKE: [set_req.V_TRIPPED],
} }
if float(gateway.version) >= 1.5: if float(gateway.protocol_version) >= 1.5:
map_sv_types.update({ map_sv_types.update({
pres.S_SPRINKLER: [set_req.V_TRIPPED], pres.S_SPRINKLER: [set_req.V_TRIPPED],
pres.S_WATER_LEAK: [set_req.V_TRIPPED], pres.S_WATER_LEAK: [set_req.V_TRIPPED],
@ -66,7 +66,7 @@ class MySensorsBinarySensor(
pres.S_MOTION: 'motion', pres.S_MOTION: 'motion',
pres.S_SMOKE: 'smoke', pres.S_SMOKE: 'smoke',
} }
if float(self.gateway.version) >= 1.5: if float(self.gateway.protocol_version) >= 1.5:
class_map.update({ class_map.update({
pres.S_SPRINKLER: 'sprinkler', pres.S_SPRINKLER: 'sprinkler',
pres.S_WATER_LEAK: 'leak', pres.S_WATER_LEAK: 'leak',

View File

@ -6,30 +6,42 @@ https://home-assistant.io/components/binary_sensor.rest/
""" """
import logging import logging
from homeassistant.components.binary_sensor import (BinarySensorDevice, import voluptuous as vol
SENSOR_CLASSES)
from homeassistant.components.binary_sensor import (
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
from homeassistant.components.sensor.rest import RestData from homeassistant.components.sensor.rest import RestData
from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.const import (
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
CONF_SENSOR_CLASS)
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
DEFAULT_METHOD = 'GET'
DEFAULT_NAME = 'REST Binary Sensor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
})
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'REST Binary Sensor'
DEFAULT_METHOD = 'GET'
# pylint: disable=unused-variable # pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the REST binary sensor.""" """Setup the REST binary sensor."""
resource = config.get('resource', None) name = config.get(CONF_NAME)
method = config.get('method', DEFAULT_METHOD) resource = config.get(CONF_RESOURCE)
payload = config.get('payload', None) method = config.get(CONF_METHOD)
payload = config.get(CONF_PAYLOAD)
verify_ssl = config.get('verify_ssl', True) verify_ssl = config.get('verify_ssl', True)
sensor_class = config.get(CONF_SENSOR_CLASS)
sensor_class = config.get('sensor_class') value_template = config.get(CONF_VALUE_TEMPLATE)
if sensor_class not in SENSOR_CLASSES:
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
sensor_class = None
rest = RestData(method, resource, payload, verify_ssl) rest = RestData(method, resource, payload, verify_ssl)
rest.update() rest.update()
@ -39,11 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False return False
add_devices([RestBinarySensor( add_devices([RestBinarySensor(
hass, hass, rest, name, sensor_class, value_template)])
rest,
config.get('name', DEFAULT_NAME),
sensor_class,
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments

View File

@ -0,0 +1,9 @@
# Describes the format for available binary_sensor services
ffmpeg_restart:
description: Send a restart command to a ffmpeg based sensor (party mode).
fields:
entity_id:
description: Name(s) of entites that will restart. Platform dependent.
example: 'binary_sensor.ffmpeg_noise'

View File

@ -5,55 +5,47 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.template/ https://home-assistant.io/components/binary_sensor.template/
""" """
import logging import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (BinarySensorDevice, from homeassistant.components.binary_sensor import (BinarySensorDevice,
ENTITY_ID_FORMAT, ENTITY_ID_FORMAT,
SENSOR_CLASSES) PLATFORM_SCHEMA,
from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, SENSOR_CLASSES_SCHEMA)
ATTR_ENTITY_ID, MATCH_ALL)
from homeassistant.const import (ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, MATCH_ALL,
CONF_VALUE_TEMPLATE, CONF_SENSOR_CLASS)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
from homeassistant.util import slugify
CONF_SENSORS = 'sensors' CONF_SENSORS = 'sensors'
SENSOR_SCHEMA = vol.Schema({
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID, default=MATCH_ALL): cv.entity_ids,
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
})
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup template binary sensors.""" """Setup template binary sensors."""
sensors = [] sensors = []
if config.get(CONF_SENSORS) is None:
_LOGGER.error('Missing configuration data for binary_sensor platform')
return False
for device, device_config in config[CONF_SENSORS].items(): for device, device_config in config[CONF_SENSORS].items():
if device != slugify(device): value_template = device_config[CONF_VALUE_TEMPLATE]
_LOGGER.error('Found invalid key for binary_sensor.template: %s. ' entity_ids = device_config[ATTR_ENTITY_ID]
'Use %s instead', device, slugify(device))
continue
if not isinstance(device_config, dict):
_LOGGER.error('Missing configuration data for binary_sensor %s',
device)
continue
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
sensor_class = device_config.get('sensor_class') sensor_class = device_config.get(CONF_SENSOR_CLASS)
value_template = device_config.get(CONF_VALUE_TEMPLATE)
if sensor_class not in SENSOR_CLASSES:
_LOGGER.error('Sensor class is not valid')
continue
if value_template is None:
_LOGGER.error(
'Missing %s for sensor %s', CONF_VALUE_TEMPLATE, device)
continue
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
sensors.append( sensors.append(
BinarySensorTemplate( BinarySensorTemplate(

View File

@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component from homeassistant.loader import get_component
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] REQUIREMENTS = ['python-wink==0.7.13', 'pubnub==3.8.2']
# These are the available sensors mapped to binary_sensor class # These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = { SENSOR_TYPES = {

View File

@ -11,15 +11,15 @@ import requests
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.loader import get_component from homeassistant.loader import get_component
DEPENDENCIES = ["bloomsky"] DEPENDENCIES = ['bloomsky']
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup access to BloomSky cameras.""" """Setup access to BloomSky cameras."""
bloomsky = get_component('bloomsky') bloomsky = get_component('bloomsky')
for device in bloomsky.BLOOMSKY.devices.values(): for device in bloomsky.BLOOMSKY.devices.values():
add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
class BloomSkyCamera(Camera): class BloomSkyCamera(Camera):
@ -28,8 +28,8 @@ class BloomSkyCamera(Camera):
def __init__(self, bs, device): def __init__(self, bs, device):
"""Setup for access to the BloomSky camera images.""" """Setup for access to the BloomSky camera images."""
super(BloomSkyCamera, self).__init__() super(BloomSkyCamera, self).__init__()
self._name = device["DeviceName"] self._name = device['DeviceName']
self._id = device["DeviceID"] self._id = device['DeviceID']
self._bloomsky = bs self._bloomsky = bs
self._url = "" self._url = ""
self._last_url = "" self._last_url = ""
@ -42,7 +42,7 @@ class BloomSkyCamera(Camera):
def camera_image(self): def camera_image(self):
"""Update the camera's image if it has changed.""" """Update the camera's image if it has changed."""
try: try:
self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"] self._url = self._bloomsky.devices[self._id]['Data']['ImageURL']
self._bloomsky.refresh_devices() self._bloomsky.refresh_devices()
# If the URL hasn't changed then the image hasn't changed. # If the URL hasn't changed then the image hasn't changed.
if self._url != self._last_url: if self._url != self._last_url:

View File

@ -9,31 +9,33 @@ from contextlib import closing
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import Camera from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.components.camera.mjpeg import extract_image_from_mjpeg from homeassistant.components.camera.mjpeg import extract_image_from_mjpeg
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.const import CONF_NAME
REQUIREMENTS = ["ha-ffmpeg==0.4"] REQUIREMENTS = ['ha-ffmpeg==0.8']
_LOGGER = logging.getLogger(__name__)
CONF_INPUT = 'input' CONF_INPUT = 'input'
CONF_FFMPEG_BIN = 'ffmpeg_bin' CONF_FFMPEG_BIN = 'ffmpeg_bin'
CONF_EXTRA_ARGUMENTS = 'extra_arguments' CONF_EXTRA_ARGUMENTS = 'extra_arguments'
PLATFORM_SCHEMA = vol.Schema({ DEFAULT_BINARY = 'ffmpeg'
vol.Required(CONF_PLATFORM): "ffmpeg", DEFAULT_NAME = 'FFmpeg'
vol.Optional(CONF_NAME, default="FFmpeg"): cv.string,
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_INPUT): cv.string, vol.Required(CONF_INPUT): cv.string,
vol.Optional(CONF_FFMPEG_BIN, default="ffmpeg"): cv.string,
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup a FFmpeg Camera.""" """Setup a FFmpeg Camera."""
add_devices_callback([FFmpegCamera(config)]) add_devices([FFmpegCamera(config)])
class FFmpegCamera(Camera): class FFmpegCamera(Camera):

View File

@ -7,21 +7,33 @@ https://home-assistant.io/components/camera.foscam/
import logging import logging
import requests import requests
import voluptuous as vol
from homeassistant.components.camera import DOMAIN, Camera from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.helpers import validate_config from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_IP = 'ip'
DEFAULT_NAME = 'Foscam Camera'
DEFAULT_PORT = 88
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a Foscam IP Camera.""" """Setup a Foscam IP Camera."""
if not validate_config({DOMAIN: config}, add_devices([FoscamCamera(config)])
{DOMAIN: ['username', 'password', 'ip']}, _LOGGER):
return None
add_devices_callback([FoscamCamera(config)])
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
@ -32,16 +44,16 @@ class FoscamCamera(Camera):
"""Initialize a Foscam camera.""" """Initialize a Foscam camera."""
super(FoscamCamera, self).__init__() super(FoscamCamera, self).__init__()
ip_address = device_info.get('ip') ip_address = device_info.get(CONF_IP)
port = device_info.get('port', 88) port = device_info.get(CONF_PORT)
self._base_url = 'http://' + ip_address + ':' + str(port) + '/' self._base_url = 'http://{}:{}/'.format(ip_address, port)
self._username = device_info.get('username') self._username = device_info.get(CONF_USERNAME)
self._password = device_info.get('password') self._password = device_info.get(CONF_PASSWORD)
self._snap_picture_url = self._base_url \ self._snap_picture_url = self._base_url \
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \ + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \
+ self._username + '&pwd=' + self._password + self._username + '&pwd=' + self._password
self._name = device_info.get('name', 'Foscam Camera') self._name = device_info.get(CONF_NAME)
_LOGGER.info('Using the following URL for %s: %s', _LOGGER.info('Using the following URL for %s: %s',
self._name, self._snap_picture_url) self._name, self._snap_picture_url)

View File

@ -7,22 +7,38 @@ https://home-assistant.io/components/camera.generic/
import logging import logging
import requests import requests
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol
from homeassistant.components.camera import DOMAIN, Camera from homeassistant.const import (
from homeassistant.helpers import validate_config CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers import config_validation as cv, template
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change'
CONF_STILL_IMAGE_URL = 'still_image_url'
DEFAULT_NAME = 'Generic Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STILL_IMAGE_URL): vol.Any(cv.url, cv.template),
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a generic IP Camera.""" """Setup a generic IP Camera."""
if not validate_config({DOMAIN: config}, {DOMAIN: ['still_image_url']}, add_devices([GenericCamera(config)])
_LOGGER):
return None
add_devices_callback([GenericCamera(config)])
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
@ -32,30 +48,47 @@ class GenericCamera(Camera):
def __init__(self, device_info): def __init__(self, device_info):
"""Initialize a generic camera.""" """Initialize a generic camera."""
super().__init__() super().__init__()
self._name = device_info.get('name', 'Generic Camera') self._name = device_info.get(CONF_NAME)
self._username = device_info.get('username') self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._password = device_info.get('password') self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
self._still_image_url = device_info['still_image_url']
username = device_info.get(CONF_USERNAME)
password = device_info.get(CONF_PASSWORD)
if username and password:
if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION:
self._auth = HTTPDigestAuth(username, password)
else:
self._auth = HTTPBasicAuth(username, password)
else:
self._auth = None
self._last_url = None
self._last_image = None
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
if self._username and self._password: try:
try: url = template.render(self.hass, self._still_image_url)
response = requests.get( except TemplateError as err:
self._still_image_url, _LOGGER.error('Error parsing template %s: %s',
auth=HTTPBasicAuth(self._username, self._password), self._still_image_url, err)
timeout=10) return self._last_image
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
else:
try:
response = requests.get(self._still_image_url, timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
return response.content if url == self._last_url and self._limit_refetch:
return self._last_image
kwargs = {'timeout': 10, 'auth': self._auth}
try:
response = requests.get(url, **kwargs)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
self._last_url = url
self._last_image = response.content
return self._last_image
@property @property
def name(self): def name(self):

View File

@ -1,50 +1,55 @@
"""Camera that loads a picture from a local file.""" """
Camera that loads a picture from a local file.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.local_file/
"""
import logging import logging
import os import os
from homeassistant.components.camera import Camera import voluptuous as vol
from homeassistant.const import CONF_NAME
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_FILE_PATH = 'file_path'
DEFAULT_NAME = 'Local File'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FILE_PATH): cv.isfile,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Camera.""" """Setup the Camera."""
# check for missing required configuration variable file_path = config[CONF_FILE_PATH]
if config.get("file_path") is None:
_LOGGER.error("Missing required variable: file_path")
return False
setup_config = (
{
"name": config.get("name", "Local File"),
"file_path": config.get("file_path")
}
)
# check filepath given is readable # check filepath given is readable
if not os.access(setup_config["file_path"], os.R_OK): if not os.access(file_path, os.R_OK):
_LOGGER.error("file path is not readable") _LOGGER.error("file path is not readable")
return False return False
add_devices([ add_devices([LocalFile(config[CONF_NAME], file_path)])
LocalFile(setup_config)
])
class LocalFile(Camera): class LocalFile(Camera):
"""Local camera.""" """Local camera."""
def __init__(self, device_info): def __init__(self, name, file_path):
"""Initialize Local File Camera component.""" """Initialize Local File Camera component."""
super().__init__() super().__init__()
self._name = device_info["name"] self._name = name
self._config = device_info self._file_path = file_path
def camera_image(self): def camera_image(self):
"""Return image response.""" """Return image response."""
with open(self._config["file_path"], 'rb') as file: with open(self._file_path, 'rb') as file:
return file.read() return file.read()
@property @property

View File

@ -8,24 +8,36 @@ import logging
from contextlib import closing from contextlib import closing
import requests import requests
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol
from homeassistant.components.camera import DOMAIN, Camera from homeassistant.const import (
from homeassistant.helpers import validate_config CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
CONTENT_TYPE_HEADER = 'Content-Type' from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_MJPEG_URL = 'mjpeg_url'
CONTENT_TYPE_HEADER = 'Content-Type'
DEFAULT_NAME = 'Mjpeg Camera'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MJPEG_URL): cv.url,
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a MJPEG IP Camera.""" """Setup a MJPEG IP Camera."""
if not validate_config({DOMAIN: config}, {DOMAIN: ['mjpeg_url']}, add_devices([MjpegCamera(config)])
_LOGGER):
return None
add_devices_callback([MjpegCamera(config)])
def extract_image_from_mjpeg(stream): def extract_image_from_mjpeg(stream):
@ -47,17 +59,21 @@ class MjpegCamera(Camera):
def __init__(self, device_info): def __init__(self, device_info):
"""Initialize a MJPEG camera.""" """Initialize a MJPEG camera."""
super().__init__() super().__init__()
self._name = device_info.get('name', 'Mjpeg Camera') self._name = device_info.get(CONF_NAME)
self._username = device_info.get('username') self._authentication = device_info.get(CONF_AUTHENTICATION)
self._password = device_info.get('password') self._username = device_info.get(CONF_USERNAME)
self._mjpeg_url = device_info['mjpeg_url'] self._password = device_info.get(CONF_PASSWORD)
self._mjpeg_url = device_info[CONF_MJPEG_URL]
def camera_stream(self): def camera_stream(self):
"""Return a MJPEG stream image response directly from the camera.""" """Return a MJPEG stream image response directly from the camera."""
if self._username and self._password: if self._username and self._password:
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
auth = HTTPDigestAuth(self._username, self._password)
else:
auth = HTTPBasicAuth(self._username, self._password)
return requests.get(self._mjpeg_url, return requests.get(self._mjpeg_url,
auth=HTTPBasicAuth(self._username, auth=auth,
self._password),
stream=True, timeout=10) stream=True, timeout=10)
else: else:
return requests.get(self._mjpeg_url, stream=True, timeout=10) return requests.get(self._mjpeg_url, stream=True, timeout=10)

View File

@ -6,34 +6,43 @@ https://home-assistant.io/components/camera.netatmo/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
import requests import requests
import voluptuous as vol
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.components.camera import Camera
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ["netatmo"] DEPENDENCIES = ['netatmo']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_HOME = 'home' CONF_HOME = 'home'
ATTR_CAMERAS = 'cameras' CONF_CAMERAS = 'cameras'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME): cv.string,
vol.Optional(CONF_CAMERAS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup access to Netatmo Welcome cameras.""" """Setup access to Netatmo Welcome cameras."""
netatmo = get_component('netatmo') netatmo = get_component('netatmo')
home = config.get(CONF_HOME, None) home = config.get(CONF_HOME)
data = WelcomeData(netatmo.NETATMO_AUTH, home) data = WelcomeData(netatmo.NETATMO_AUTH, home)
for camera_name in data.get_camera_names(): for camera_name in data.get_camera_names():
if ATTR_CAMERAS in config: if CONF_CAMERAS in config:
if camera_name not in config[ATTR_CAMERAS]: if camera_name not in config[CONF_CAMERAS]:
continue continue
add_devices_callback([WelcomeCamera(data, camera_name, home)]) add_devices([WelcomeCamera(data, camera_name, home)])
class WelcomeCamera(Camera): class WelcomeCamera(Camera):

View File

@ -9,41 +9,77 @@ import subprocess
import logging import logging
import shutil import shutil
from homeassistant.components.camera import Camera import voluptuous as vol
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, CONF_FILE_PATH)
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_HORIZONTAL_FLIP = 'horizontal_flip'
CONF_IMAGE_HEIGHT = 'image_height'
CONF_IMAGE_QUALITY = 'image_quality'
CONF_IMAGE_ROTATION = 'image_rotation'
CONF_IMAGE_WIDTH = 'image_width'
CONF_TIMELAPSE = 'timelapse'
CONF_VERTICAL_FLIP = 'vertical_flip'
DEFAULT_HORIZONTAL_FLIP = 0
DEFAULT_IMAGE_HEIGHT = 480
DEFAULT_IMAGE_QUALITIY = 7
DEFAULT_IMAGE_ROTATION = 0
DEFAULT_IMAGE_WIDTH = 640
DEFAULT_NAME = 'Raspberry Pi Camera'
DEFAULT_TIMELAPSE = 1000
DEFAULT_VERTICAL_FLIP = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FILE_PATH): cv.isfile,
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP):
vol.Coerce(int),
vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY):
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
vol.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION):
vol.All(vol.Coerce(int), vol.Range(min=0, max=359)),
vol.Optional(CONF_IMAGE_WIDTH, default=DEFAULT_IMAGE_WIDTH):
vol.Coerce(int),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TIMELAPSE, default=1000): vol.Coerce(int),
vol.Optional(CONF_VERTICAL_FLIP, default=DEFAULT_VERTICAL_FLIP):
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Raspberry Camera.""" """Setup the Raspberry Camera."""
if shutil.which("raspistill") is None: if shutil.which("raspistill") is None:
_LOGGER.error("Error: raspistill not found") _LOGGER.error("'raspistill' was not found")
return False return False
setup_config = ( setup_config = (
{ {
"name": config.get("name", "Raspberry Pi Camera"), CONF_NAME: config.get(CONF_NAME),
"image_width": int(config.get("image_width", "640")), CONF_IMAGE_WIDTH: config.get(CONF_IMAGE_WIDTH),
"image_height": int(config.get("image_height", "480")), CONF_IMAGE_HEIGHT: config.get(CONF_IMAGE_HEIGHT),
"image_quality": int(config.get("image_quality", "7")), CONF_IMAGE_QUALITY: config.get(CONF_IMAGE_QUALITY),
"image_rotation": int(config.get("image_rotation", "0")), CONF_IMAGE_ROTATION: config.get(CONF_IMAGE_ROTATION),
"timelapse": int(config.get("timelapse", "2000")), CONF_TIMELAPSE: config.get(CONF_TIMELAPSE),
"horizontal_flip": int(config.get("horizontal_flip", "0")), CONF_HORIZONTAL_FLIP: config.get(CONF_HORIZONTAL_FLIP),
"vertical_flip": int(config.get("vertical_flip", "0")), CONF_VERTICAL_FLIP: config.get(CONF_VERTICAL_FLIP),
"file_path": config.get("file_path", CONF_FILE_PATH: config.get(CONF_FILE_PATH,
os.path.join(os.path.dirname(__file__), os.path.join(os.path.dirname(__file__),
'image.jpg')) 'image.jpg'))
} }
) )
# check filepath given is writable if not os.access(setup_config[CONF_FILE_PATH], os.W_OK):
if not os.access(setup_config["file_path"], os.W_OK): _LOGGER.error("File path is not writable")
_LOGGER.error("Error: file path is not writable")
return False return False
add_devices([ add_devices([RaspberryCamera(setup_config)])
RaspberryCamera(setup_config)
])
class RaspberryCamera(Camera): class RaspberryCamera(Camera):
@ -53,26 +89,26 @@ class RaspberryCamera(Camera):
"""Initialize Raspberry Pi camera component.""" """Initialize Raspberry Pi camera component."""
super().__init__() super().__init__()
self._name = device_info["name"] self._name = device_info[CONF_NAME]
self._config = device_info self._config = device_info
# kill if there's raspistill instance # Kill if there's raspistill instance
subprocess.Popen(['killall', 'raspistill'], subprocess.Popen(['killall', 'raspistill'],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
cmd_args = [ cmd_args = [
'raspistill', '--nopreview', '-o', str(device_info["file_path"]), 'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH],
'-t', '0', '-w', str(device_info["image_width"]), '-t', '0', '-w', str(device_info[CONF_IMAGE_WIDTH]),
'-h', str(device_info["image_height"]), '-h', str(device_info[CONF_IMAGE_HEIGHT]),
'-tl', str(device_info["timelapse"]), '-tl', str(device_info[CONF_TIMELAPSE]),
'-q', str(device_info["image_quality"]), '-q', str(device_info[CONF_IMAGE_QUALITY]),
'-rot', str(device_info["image_rotation"]) '-rot', str(device_info[CONF_IMAGE_ROTATION])
] ]
if device_info["horizontal_flip"]: if device_info[CONF_HORIZONTAL_FLIP]:
cmd_args.append("-hf") cmd_args.append("-hf")
if device_info["vertical_flip"]: if device_info[CONF_VERTICAL_FLIP]:
cmd_args.append("-vf") cmd_args.append("-vf")
subprocess.Popen(cmd_args, subprocess.Popen(cmd_args,
@ -81,7 +117,7 @@ class RaspberryCamera(Camera):
def camera_image(self): def camera_image(self):
"""Return raspstill image response.""" """Return raspstill image response."""
with open(self._config["file_path"], 'rb') as file: with open(self._config[CONF_FILE_PATH], 'rb') as file:
return file.read() return file.read()
@property @property

View File

@ -0,0 +1,535 @@
"""
Provides functionality to interact with climate devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/climate/
"""
import logging
import os
from numbers import Number
import voluptuous as vol
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.config import load_yaml_config_file
import homeassistant.util as util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELSIUS)
DOMAIN = "climate"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCAN_INTERVAL = 60
SERVICE_SET_AWAY_MODE = "set_away_mode"
SERVICE_SET_AUX_HEAT = "set_aux_heat"
SERVICE_SET_TEMPERATURE = "set_temperature"
SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_OPERATION_MODE = "set_operation_mode"
SERVICE_SET_SWING_MODE = "set_swing_mode"
SERVICE_SET_HUMIDITY = "set_humidity"
STATE_HEAT = "heat"
STATE_COOL = "cool"
STATE_IDLE = "idle"
STATE_AUTO = "auto"
STATE_DRY = "dry"
STATE_FAN_ONLY = "fan_only"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_MAX_TEMP = "max_temp"
ATTR_MIN_TEMP = "min_temp"
ATTR_AWAY_MODE = "away_mode"
ATTR_AUX_HEAT = "aux_heat"
ATTR_FAN_MODE = "fan_mode"
ATTR_FAN_LIST = "fan_list"
ATTR_CURRENT_HUMIDITY = "current_humidity"
ATTR_HUMIDITY = "humidity"
ATTR_MAX_HUMIDITY = "max_humidity"
ATTR_MIN_HUMIDITY = "min_humidity"
ATTR_OPERATION_MODE = "operation_mode"
ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_LIST = "swing_list"
_LOGGER = logging.getLogger(__name__)
SET_AWAY_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AWAY_MODE): cv.boolean,
})
SET_AUX_HEAT_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AUX_HEAT): cv.boolean,
})
SET_TEMPERATURE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_TEMPERATURE): vol.Coerce(float),
})
SET_FAN_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN_MODE): cv.string,
})
SET_OPERATION_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_OPERATION_MODE): cv.string,
})
SET_HUMIDITY_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_HUMIDITY): vol.Coerce(float),
})
SET_SWING_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_SWING_MODE): cv.string,
})
def set_away_mode(hass, away_mode, entity_id=None):
"""Turn all or specified climate devices away mode on."""
data = {
ATTR_AWAY_MODE: away_mode
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
def set_aux_heat(hass, aux_heat, entity_id=None):
"""Turn all or specified climate devices auxillary heater on."""
data = {
ATTR_AUX_HEAT: aux_heat
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
def set_temperature(hass, temperature, entity_id=None):
"""Set new target temperature."""
data = {ATTR_TEMPERATURE: temperature}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data)
def set_humidity(hass, humidity, entity_id=None):
"""Set new target humidity."""
data = {ATTR_HUMIDITY: humidity}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data)
def set_fan_mode(hass, fan, entity_id=None):
"""Set all or specified climate devices fan mode on."""
data = {ATTR_FAN_MODE: fan}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
def set_operation_mode(hass, operation_mode, entity_id=None):
"""Set new target operation mode."""
data = {ATTR_OPERATION_MODE: operation_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)
def set_swing_mode(hass, swing_mode, entity_id=None):
"""Set new target swing mode."""
data = {ATTR_SWING_MODE: swing_mode}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
# pylint: disable=too-many-branches
def setup(hass, config):
"""Setup climate devices."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
def away_mode_set_service(service):
"""Set away mode on target climate devices."""
target_climate = component.extract_from_service(service)
away_mode = service.data.get(ATTR_AWAY_MODE)
if away_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE)
return
for climate in target_climate:
if away_mode:
climate.turn_away_mode_on()
else:
climate.turn_away_mode_off()
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service,
descriptions.get(SERVICE_SET_AWAY_MODE),
schema=SET_AWAY_MODE_SCHEMA)
def aux_heat_set_service(service):
"""Set auxillary heater on target climate devices."""
target_climate = component.extract_from_service(service)
aux_heat = service.data.get(ATTR_AUX_HEAT)
if aux_heat is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT)
return
for climate in target_climate:
if aux_heat:
climate.turn_aux_heat_on()
else:
climate.turn_aux_heat_off()
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service,
descriptions.get(SERVICE_SET_AUX_HEAT),
schema=SET_AUX_HEAT_SCHEMA)
def temperature_set_service(service):
"""Set temperature on the target climate devices."""
target_climate = component.extract_from_service(service)
temperature = util.convert(
service.data.get(ATTR_TEMPERATURE), float)
if temperature is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE)
return
for climate in target_climate:
climate.set_temperature(convert_temperature(
temperature, hass.config.units.temperature_unit,
climate.unit_of_measurement))
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service,
descriptions.get(SERVICE_SET_TEMPERATURE),
schema=SET_TEMPERATURE_SCHEMA)
def humidity_set_service(service):
"""Set humidity on the target climate devices."""
target_climate = component.extract_from_service(service)
humidity = service.data.get(ATTR_HUMIDITY)
if humidity is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_HUMIDITY, ATTR_HUMIDITY)
return
for climate in target_climate:
climate.set_humidity(humidity)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service,
descriptions.get(SERVICE_SET_HUMIDITY),
schema=SET_HUMIDITY_SCHEMA)
def fan_mode_set_service(service):
"""Set fan mode on target climate devices."""
target_climate = component.extract_from_service(service)
fan = service.data.get(ATTR_FAN_MODE)
if fan is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_FAN_MODE, ATTR_FAN_MODE)
return
for climate in target_climate:
climate.set_fan_mode(fan)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service,
descriptions.get(SERVICE_SET_FAN_MODE),
schema=SET_FAN_MODE_SCHEMA)
def operation_set_service(service):
"""Set operating mode on the target climate devices."""
target_climate = component.extract_from_service(service)
operation_mode = service.data.get(ATTR_OPERATION_MODE)
if operation_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE)
return
for climate in target_climate:
climate.set_operation_mode(operation_mode)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service,
descriptions.get(SERVICE_SET_OPERATION_MODE),
schema=SET_OPERATION_MODE_SCHEMA)
def swing_set_service(service):
"""Set swing mode on the target climate devices."""
target_climate = component.extract_from_service(service)
swing_mode = service.data.get(ATTR_SWING_MODE)
if swing_mode is None:
_LOGGER.error(
"Received call to %s without attribute %s",
SERVICE_SET_SWING_MODE, ATTR_SWING_MODE)
return
for climate in target_climate:
climate.set_swing_mode(swing_mode)
if climate.should_poll:
climate.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_SWING_MODE, swing_set_service,
descriptions.get(SERVICE_SET_SWING_MODE),
schema=SET_SWING_MODE_SCHEMA)
return True
class ClimateDevice(Entity):
"""Representation of a climate device."""
# pylint: disable=too-many-public-methods,no-self-use
@property
def state(self):
"""Return the current state."""
return self.current_operation or STATE_UNKNOWN
@property
def state_attributes(self):
"""Return the optional state attributes."""
data = {
ATTR_CURRENT_TEMPERATURE:
self._convert_for_display(self.current_temperature),
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp),
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
ATTR_TEMPERATURE:
self._convert_for_display(self.target_temperature),
}
humidity = self.target_humidity
if humidity is not None:
data[ATTR_HUMIDITY] = humidity
data[ATTR_CURRENT_HUMIDITY] = self.current_humidity
data[ATTR_MIN_HUMIDITY] = self.min_humidity
data[ATTR_MAX_HUMIDITY] = self.max_humidity
fan_mode = self.current_fan_mode
if fan_mode is not None:
data[ATTR_FAN_MODE] = fan_mode
data[ATTR_FAN_LIST] = self.fan_list
operation_mode = self.current_operation
if operation_mode is not None:
data[ATTR_OPERATION_MODE] = operation_mode
data[ATTR_OPERATION_LIST] = self.operation_list
swing_mode = self.current_swing_mode
if swing_mode is not None:
data[ATTR_SWING_MODE] = swing_mode
data[ATTR_SWING_LIST] = self.swing_list
is_away = self.is_away_mode_on
if is_away is not None:
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
is_aux_heat = self.is_aux_heat_on
if is_aux_heat is not None:
data[ATTR_AUX_HEAT] = STATE_ON if is_aux_heat else STATE_OFF
return data
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
raise NotImplementedError
@property
def current_humidity(self):
"""Return the current humidity."""
return None
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return None
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return None
@property
def operation_list(self):
"""List of available operation modes."""
return None
@property
def current_temperature(self):
"""Return the current temperature."""
return None
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return None
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return None
@property
def is_aux_heat_on(self):
"""Return true if aux heater."""
return None
@property
def current_fan_mode(self):
"""Return the fan setting."""
return None
@property
def fan_list(self):
"""List of available fan modes."""
return None
@property
def current_swing_mode(self):
"""Return the fan setting."""
return None
@property
def swing_list(self):
"""List of available swing modes."""
return None
def set_temperature(self, temperature):
"""Set new target temperature."""
raise NotImplementedError()
def set_humidity(self, humidity):
"""Set new target humidity."""
raise NotImplementedError()
def set_fan_mode(self, fan):
"""Set new target fan mode."""
raise NotImplementedError()
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
raise NotImplementedError()
def set_swing_mode(self, swing_mode):
"""Set new target swing operation."""
raise NotImplementedError()
def turn_away_mode_on(self):
"""Turn away mode on."""
raise NotImplementedError()
def turn_away_mode_off(self):
"""Turn away mode off."""
raise NotImplementedError()
def turn_aux_heat_on(self):
"""Turn auxillary heater on."""
raise NotImplementedError()
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
raise NotImplementedError()
@property
def min_temp(self):
"""Return the minimum temperature."""
return convert_temperature(7, TEMP_CELSIUS, self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature."""
return convert_temperature(35, TEMP_CELSIUS, self.unit_of_measurement)
@property
def min_humidity(self):
"""Return the minimum humidity."""
return 30
@property
def max_humidity(self):
"""Return the maximum humidity."""
return 99
def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes."""
if temp is None or not isinstance(temp, Number):
return temp
value = convert_temperature(temp, self.unit_of_measurement,
self.hass.config.units.temperature_unit)
if self.hass.config.units.temperature_unit is TEMP_CELSIUS:
decimal_count = 1
else:
# Users of fahrenheit generally expect integer units.
decimal_count = 0
return round(value, decimal_count)

View File

@ -0,0 +1,164 @@
"""
Demo platform that offers a fake climate device.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo climate devices."""
add_devices([
DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low",
None, None, "Auto", "Heat", None),
DemoClimate("Hvac", 21, TEMP_CELSIUS, True, 22, "On High",
67, 54, "Off", "Cool", False),
])
# pylint: disable=too-many-arguments, too-many-public-methods
class DemoClimate(ClimateDevice):
"""Representation of a demo climate device."""
# pylint: disable=too-many-instance-attributes
def __init__(self, name, target_temperature, unit_of_measurement,
away, current_temperature, current_fan_mode,
target_humidity, current_humidity, current_swing_mode,
current_operation, aux):
"""Initialize the climate device."""
self._name = name
self._target_temperature = target_temperature
self._target_humidity = target_humidity
self._unit_of_measurement = unit_of_measurement
self._away = away
self._current_temperature = current_temperature
self._current_humidity = current_humidity
self._current_fan_mode = current_fan_mode
self._current_operation = current_operation
self._aux = aux
self._current_swing_mode = current_swing_mode
self._fan_list = ["On Low", "On High", "Auto Low", "Auto High", "Off"]
self._operation_list = ["Heat", "Cool", "Auto Changeover", "Off"]
self._swing_list = ["Auto", "1", "2", "3", "Off"]
@property
def should_poll(self):
"""Polling not needed for a demo climate device."""
return False
@property
def name(self):
"""Return the name of the climate device."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def current_humidity(self):
"""Return the current humidity."""
return self._current_humidity
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return self._target_humidity
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._current_operation
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self._away
@property
def is_aux_heat_on(self):
"""Return true if away mode is on."""
return self._aux
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self._current_fan_mode
@property
def fan_list(self):
"""List of available fan modes."""
return self._fan_list
def set_temperature(self, temperature):
"""Set new target temperature."""
self._target_temperature = temperature
self.update_ha_state()
def set_humidity(self, humidity):
"""Set new target temperature."""
self._target_humidity = humidity
self.update_ha_state()
def set_swing_mode(self, swing_mode):
"""Set new target temperature."""
self._current_swing_mode = swing_mode
self.update_ha_state()
def set_fan_mode(self, fan):
"""Set new target temperature."""
self._current_fan_mode = fan
self.update_ha_state()
def set_operation_mode(self, operation_mode):
"""Set new target temperature."""
self._current_operation = operation_mode
self.update_ha_state()
@property
def current_swing_mode(self):
"""Return the swing setting."""
return self._current_swing_mode
@property
def swing_list(self):
"""List of available swing modes."""
return self._swing_list
def turn_away_mode_on(self):
"""Turn away mode on."""
self._away = True
self.update_ha_state()
def turn_away_mode_off(self):
"""Turn away mode off."""
self._away = False
self.update_ha_state()
def turn_aux_heat_on(self):
"""Turn away auxillary heater on."""
self._aux = True
self.update_ha_state()
def turn_aux_heat_off(self):
"""Turn auxillary heater off."""
self._aux = False
self.update_ha_state()

View File

@ -0,0 +1,247 @@
"""
Platform for Ecobee Thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.ecobee/
"""
import logging
from os import path
import voluptuous as vol
from homeassistant.components import ecobee
from homeassistant.components.climate import (
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['ecobee']
_LOGGER = logging.getLogger(__name__)
ECOBEE_CONFIG_FILE = 'ecobee.conf'
_CONFIGURING = {}
ATTR_FAN_MIN_ON_TIME = "fan_min_on_time"
SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time"
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Ecobee Thermostat Platform."""
if discovery_info is None:
return
data = ecobee.NETWORK
hold_temp = discovery_info['hold_temp']
_LOGGER.info(
"Loading ecobee thermostat component with hold_temp set to %s",
hold_temp)
devices = [Thermostat(data, index, hold_temp)
for index in range(len(data.ecobee.thermostats))]
add_devices(devices)
def fan_min_on_time_set_service(service):
"""Set the minimum fan on time on the target thermostats."""
entity_id = service.data.get('entity_id')
if entity_id:
target_thermostats = [device for device in devices
if device.entity_id == entity_id]
else:
target_thermostats = devices
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
for thermostat in target_thermostats:
thermostat.set_fan_min_on_time(str(fan_min_on_time))
thermostat.update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service,
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
# pylint: disable=too-many-public-methods, abstract-method
class Thermostat(ClimateDevice):
"""A thermostat class for Ecobee."""
def __init__(self, data, thermostat_index, hold_temp):
"""Initialize the thermostat."""
self.data = data
self.thermostat_index = thermostat_index
self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index)
self._name = self.thermostat['name']
self.hold_temp = hold_temp
def update(self):
"""Get the latest state from the thermostat."""
self.data.update()
self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index)
@property
def name(self):
"""Return the name of the Ecobee Thermostat."""
return self.thermostat['name']
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
"""Return the current temperature."""
return self.thermostat['runtime']['actualTemperature'] / 10
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if (self.operation_mode == 'heat' or
self.operation_mode == 'auxHeatOnly'):
return self.target_temperature_low
elif self.operation_mode == 'cool':
return self.target_temperature_high
else:
return (self.target_temperature_low +
self.target_temperature_high) / 2
@property
def target_temperature_low(self):
"""Return the lower bound temperature we try to reach."""
return int(self.thermostat['runtime']['desiredHeat'] / 10)
@property
def target_temperature_high(self):
"""Return the upper bound temperature we try to reach."""
return int(self.thermostat['runtime']['desiredCool'] / 10)
@property
def current_humidity(self):
"""Return the current humidity."""
return self.thermostat['runtime']['actualHumidity']
@property
def desired_fan_mode(self):
"""Return the desired fan mode of operation."""
return self.thermostat['runtime']['desiredFanMode']
@property
def fan(self):
"""Return the current fan state."""
if 'fan' in self.thermostat['equipmentStatus']:
return STATE_ON
else:
return STATE_OFF
@property
def operation_mode(self):
"""Return current operation ie. heat, cool, idle."""
status = self.thermostat['equipmentStatus']
if status == '':
return STATE_IDLE
elif 'Cool' in status:
return STATE_COOL
elif 'auxHeat' in status:
return STATE_HEAT
elif 'heatPump' in status:
return STATE_HEAT
else:
return status
@property
def mode(self):
"""Return current mode ie. home, away, sleep."""
return self.thermostat['program']['currentClimateRef']
@property
def current_operation(self):
"""Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off."""
return self.thermostat['settings']['hvacMode']
@property
def fan_min_on_time(self):
"""Return current fan minimum on time."""
return self.thermostat['settings']['fanMinOnTime']
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
# Move these to Thermostat Device and make them global
return {
"humidity": self.current_humidity,
"fan": self.fan,
"mode": self.mode,
"operation_mode": self.current_operation,
"fan_min_on_time": self.fan_min_on_time
}
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
mode = self.mode
events = self.thermostat['events']
for event in events:
if event['running']:
mode = event['holdClimateRef']
break
return 'away' in mode
def turn_away_mode_on(self):
"""Turn away on."""
if self.hold_temp:
self.data.ecobee.set_climate_hold(self.thermostat_index,
"away", "indefinite")
else:
self.data.ecobee.set_climate_hold(self.thermostat_index, "away")
def turn_away_mode_off(self):
"""Turn away off."""
self.data.ecobee.resume_program(self.thermostat_index)
def set_temperature(self, temperature):
"""Set new target temperature."""
temperature = int(temperature)
low_temp = temperature - 1
high_temp = temperature + 1
if self.hold_temp:
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
high_temp, "indefinite")
else:
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
high_temp)
def set_operation_mode(self, operation_mode):
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode)
def set_fan_min_on_time(self, fan_min_on_time):
"""Set the minimum fan on time."""
self.data.ecobee.set_fan_min_on_time(self.thermostat_index,
fan_min_on_time)
# Home and Sleep mode aren't used in UI yet:
# def turn_home_mode_on(self):
# """ Turns home mode on. """
# self.data.ecobee.set_climate_hold(self.thermostat_index, "home")
# def turn_home_mode_off(self):
# """ Turns home mode off. """
# self.data.ecobee.resume_program(self.thermostat_index)
# def turn_sleep_mode_on(self):
# """ Turns sleep mode on. """
# self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep")
# def turn_sleep_mode_off(self):
# """ Turns sleep mode off. """
# self.data.ecobee.resume_program(self.thermostat_index)

View File

@ -0,0 +1,90 @@
"""
Support for eq3 Bluetooth Smart thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.eq3btsmart/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS
from homeassistant.util.temperature import convert
REQUIREMENTS = ['bluepy_devices==0.2.0']
CONF_MAC = 'mac'
CONF_DEVICES = 'devices'
CONF_ID = 'id'
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the eq3 BLE thermostats."""
devices = []
for name, device_cfg in config[CONF_DEVICES].items():
mac = device_cfg[CONF_MAC]
devices.append(EQ3BTSmartThermostat(mac, name))
add_devices(devices)
return True
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
class EQ3BTSmartThermostat(ClimateDevice):
"""Representation of a EQ3 Bluetooth Smart thermostat."""
def __init__(self, _mac, _name):
"""Initialize the thermostat."""
from bluepy_devices.devices import eq3btsmart
self._name = _name
self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement that is used."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Can not report temperature, so return target_temperature."""
return self.target_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._thermostat.target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
self._thermostat.target_temperature = temperature
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {"mode": self._thermostat.mode,
"mode_readable": self._thermostat.mode_readable}
@property
def min_temp(self):
"""Return the minimum temperature."""
return convert(self._thermostat.min_temp, TEMP_CELSIUS,
self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature."""
return convert(self._thermostat.max_temp, TEMP_CELSIUS,
self.unit_of_measurement)
def update(self):
"""Update the data from the thermostat."""
self._thermostat.update()

View File

@ -0,0 +1,216 @@
"""
Adds support for generic thermostat units.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.generic_thermostat/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components import switch
from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice)
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF
from homeassistant.helpers import condition
from homeassistant.helpers.event import track_state_change
DEPENDENCIES = ['switch', 'sensor']
TOL_TEMP = 0.3
CONF_NAME = 'name'
DEFAULT_NAME = 'Generic Thermostat'
CONF_HEATER = 'heater'
CONF_SENSOR = 'target_sensor'
CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp'
CONF_TARGET_TEMP = 'target_temp'
CONF_AC_MODE = 'ac_mode'
CONF_MIN_DUR = 'min_cycle_duration'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required("platform"): "generic_thermostat",
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HEATER): cv.entity_id,
vol.Required(CONF_SENSOR): cv.entity_id,
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
vol.Optional(CONF_AC_MODE): vol.Coerce(bool),
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the generic thermostat."""
name = config.get(CONF_NAME)
heater_entity_id = config.get(CONF_HEATER)
sensor_entity_id = config.get(CONF_SENSOR)
min_temp = config.get(CONF_MIN_TEMP)
max_temp = config.get(CONF_MAX_TEMP)
target_temp = config.get(CONF_TARGET_TEMP)
ac_mode = config.get(CONF_AC_MODE)
min_cycle_duration = config.get(CONF_MIN_DUR)
add_devices([GenericThermostat(hass, name, heater_entity_id,
sensor_entity_id, min_temp,
max_temp, target_temp, ac_mode,
min_cycle_duration)])
# pylint: disable=too-many-instance-attributes, abstract-method
class GenericThermostat(ClimateDevice):
"""Representation of a GenericThermostat device."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
"""Initialize the thermostat."""
self.hass = hass
self._name = name
self.heater_entity_id = heater_entity_id
self.ac_mode = ac_mode
self.min_cycle_duration = min_cycle_duration
self._active = False
self._cur_temp = None
self._min_temp = min_temp
self._max_temp = max_temp
self._target_temp = target_temp
self._unit = hass.config.units.temperature_unit
track_state_change(hass, sensor_entity_id, self._sensor_changed)
sensor_state = hass.states.get(sensor_entity_id)
if sensor_state:
self._update_temp(sensor_state)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the thermostat."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit
@property
def current_temperature(self):
"""Return the sensor temperature."""
return self._cur_temp
@property
def operation(self):
"""Return current operation ie. heat, cool, idle."""
if self.ac_mode:
cooling = self._active and self._is_device_active
return STATE_COOL if cooling else STATE_IDLE
else:
heating = self._active and self._is_device_active
return STATE_HEAT if heating else STATE_IDLE
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temp
def set_temperature(self, temperature):
"""Set new target temperature."""
self._target_temp = temperature
self._control_heating()
self.update_ha_state()
@property
def min_temp(self):
"""Return the minimum temperature."""
# pylint: disable=no-member
if self._min_temp:
return self._min_temp
else:
# get default temp from super class
return ClimateDevice.min_temp.fget(self)
@property
def max_temp(self):
"""Return the maximum temperature."""
# pylint: disable=no-member
if self._min_temp:
return self._max_temp
else:
# Get default temp from super class
return ClimateDevice.max_temp.fget(self)
def _sensor_changed(self, entity_id, old_state, new_state):
"""Called when temperature changes."""
if new_state is None:
return
self._update_temp(new_state)
self._control_heating()
self.update_ha_state()
def _update_temp(self, state):
"""Update thermostat with latest state from sensor."""
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
try:
self._cur_temp = self.hass.config.units.temperature(
float(state.state), unit)
except ValueError as ex:
_LOGGER.error('Unable to update from sensor: %s', ex)
def _control_heating(self):
"""Check if we need to turn heating on or off."""
if not self._active and None not in (self._cur_temp,
self._target_temp):
self._active = True
_LOGGER.info('Obtained current and target temperature. '
'Generic thermostat active.')
if not self._active:
return
if self.min_cycle_duration:
if self._is_device_active:
current_state = STATE_ON
else:
current_state = STATE_OFF
long_enough = condition.state(self.hass, self.heater_entity_id,
current_state,
self.min_cycle_duration)
if not long_enough:
return
if self.ac_mode:
too_hot = self._cur_temp - self._target_temp > TOL_TEMP
is_cooling = self._is_device_active
if too_hot and not is_cooling:
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id)
elif not too_hot and is_cooling:
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id)
else:
too_cold = self._target_temp - self._cur_temp > TOL_TEMP
is_heating = self._is_device_active
if too_cold and not is_heating:
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
switch.turn_on(self.hass, self.heater_entity_id)
elif not too_cold and is_heating:
_LOGGER.info('Turning off heater %s', self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id)
@property
def _is_device_active(self):
"""If the toggleable device is currently active."""
return switch.is_on(self.hass, self.heater_entity_id)

View File

@ -0,0 +1,114 @@
"""
Support for the PRT Heatmiser themostats using the V3 protocol.
See https://github.com/andylockran/heatmiserV3 for more info on the
heatmiserV3 module dependency.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.heatmiser/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS
CONF_IPADDRESS = 'ipaddress'
CONF_PORT = 'port'
CONF_TSTATS = 'tstats'
REQUIREMENTS = ["heatmiserV3==0.9.1"]
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the heatmiser thermostat."""
from heatmiserV3 import heatmiser, connection
ipaddress = str(config[CONF_IPADDRESS])
port = str(config[CONF_PORT])
if ipaddress is None or port is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_IPADDRESS, CONF_PORT)
return False
serport = connection.connection(ipaddress, port)
serport.open()
tstats = []
if CONF_TSTATS in config:
tstats = config[CONF_TSTATS]
if tstats is None:
_LOGGER.error("No thermostats configured.")
return False
for tstat in tstats:
add_devices([
HeatmiserV3Thermostat(
heatmiser,
tstat.get("id"),
tstat.get("name"),
serport)
])
return
class HeatmiserV3Thermostat(ClimateDevice):
"""Representation of a HeatmiserV3 thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, heatmiser, device, name, serport):
"""Initialize the thermostat."""
self.heatmiser = heatmiser
self.device = device
self.serport = serport
self._current_temperature = None
self._name = name
self._id = device
self.dcb = None
self.update()
self._target_temperature = int(self.dcb.get("roomset"))
@property
def name(self):
"""Return the name of the thermostat, if any."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement which this thermostat uses."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
if self.dcb is not None:
low = self.dcb.get("floortemplow ")
high = self.dcb.get("floortemphigh")
temp = (high*256 + low)/10.0
self._current_temperature = temp
else:
self._current_temperature = None
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
temperature = int(temperature)
self.heatmiser.hmSendAddress(
self._id,
18,
temperature,
1,
self.serport)
self._target_temperature = int(temperature)
def update(self):
"""Get the latest data."""
self.dcb = self.heatmiser.hmReadAddress(self._id, 'prt', self.serport)

View File

@ -0,0 +1,143 @@
"""
Support for Homematic thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homematic/
"""
import logging
import homeassistant.components.homematic as homematic
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
from homeassistant.util.temperature import convert
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN
DEPENDENCIES = ['homematic']
STATE_MANUAL = "manual"
STATE_BOOST = "boost"
HM_STATE_MAP = {
"AUTO_MODE": STATE_AUTO,
"MANU_MODE": STATE_MANUAL,
"BOOST_MODE": STATE_BOOST,
}
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the Homematic thermostat platform."""
if discovery_info is None:
return
return homematic.setup_hmdevice_discovery_helper(HMThermostat,
discovery_info,
add_callback_devices)
# pylint: disable=abstract-method
class HMThermostat(homematic.HMDevice, ClimateDevice):
"""Representation of a Homematic thermostat."""
@property
def unit_of_measurement(self):
"""Return the unit of measurement that is used."""
return TEMP_CELSIUS
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
if not self.available:
return None
# read state and search
for mode, state in HM_STATE_MAP.items():
code = getattr(self._hmdevice, mode, 0)
if self._data.get('CONTROL_MODE') == code:
return state
@property
def operation_list(self):
"""List of available operation modes."""
if not self.available:
return None
op_list = []
# generate list
for mode in self._hmdevice.ACTIONNODE:
if mode in HM_STATE_MAP:
op_list.append(HM_STATE_MAP.get(mode))
return op_list
@property
def current_humidity(self):
"""Return the current humidity."""
if not self.available:
return None
return self._data.get('ACTUAL_HUMIDITY', None)
@property
def current_temperature(self):
"""Return the current temperature."""
if not self.available:
return None
return self._data.get('ACTUAL_TEMPERATURE', None)
@property
def target_temperature(self):
"""Return the target temperature."""
if not self.available:
return None
return self._data.get('SET_TEMPERATURE', None)
def set_temperature(self, temperature):
"""Set new target temperature."""
if not self.available:
return None
self._hmdevice.set_temperature(temperature)
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
for mode, state in HM_STATE_MAP.items():
if state == operation_mode:
code = getattr(self._hmdevice, mode, 0)
self._hmdevice.STATE = code
@property
def min_temp(self):
"""Return the minimum temperature - 4.5 means off."""
return convert(4.5, TEMP_CELSIUS, self.unit_of_measurement)
@property
def max_temp(self):
"""Return the maximum temperature - 30.5 means on."""
return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement)
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
# 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 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})
# support humidity
if 'ACTUAL_HUMIDITY' in self._hmdevice.SENSORNODE:
self._data.update({'ACTUAL_HUMIDITY': STATE_UNKNOWN})

View File

@ -0,0 +1,266 @@
"""
Support for Honeywell Round Connected and Honeywell Evohome thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.honeywell/
"""
import logging
import socket
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['evohomeclient==0.2.5',
'somecomfort==0.2.1']
_LOGGER = logging.getLogger(__name__)
CONF_AWAY_TEMP = "away_temperature"
DEFAULT_AWAY_TEMP = 16
def _setup_round(username, password, config, add_devices):
"""Setup rounding function."""
from evohomeclient import EvohomeClient
try:
away_temp = float(config.get(CONF_AWAY_TEMP, DEFAULT_AWAY_TEMP))
except ValueError:
_LOGGER.error("value entered for item %s should convert to a number",
CONF_AWAY_TEMP)
return False
evo_api = EvohomeClient(username, password)
try:
zones = evo_api.temperatures(force_refresh=True)
for i, zone in enumerate(zones):
add_devices([RoundThermostat(evo_api,
zone['id'],
i == 0,
away_temp)])
except socket.error:
_LOGGER.error(
"Connection error logging into the honeywell evohome web service"
)
return False
return True
# config will be used later
def _setup_us(username, password, config, add_devices):
"""Setup user."""
import somecomfort
try:
client = somecomfort.SomeComfort(username, password)
except somecomfort.AuthError:
_LOGGER.error('Failed to login to honeywell account %s', username)
return False
except somecomfort.SomeComfortError as ex:
_LOGGER.error('Failed to initialize honeywell client: %s', str(ex))
return False
dev_id = config.get('thermostat')
loc_id = config.get('location')
add_devices([HoneywellUSThermostat(client, device)
for location in client.locations_by_id.values()
for device in location.devices_by_id.values()
if ((not loc_id or location.locationid == loc_id) and
(not dev_id or device.deviceid == dev_id))])
return True
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the honeywel thermostat."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
region = config.get('region', 'eu').lower()
if username is None or password is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_USERNAME, CONF_PASSWORD)
return False
if region not in ('us', 'eu'):
_LOGGER.error('Region `%s` is invalid (use either us or eu)', region)
return False
if region == 'us':
return _setup_us(username, password, config, add_devices)
else:
return _setup_round(username, password, config, add_devices)
class RoundThermostat(ClimateDevice):
"""Representation of a Honeywell Round Connected thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, device, zone_id, master, away_temp):
"""Initialize the thermostat."""
self.device = device
self._current_temperature = None
self._target_temperature = None
self._name = "round connected"
self._id = zone_id
self._master = master
self._is_dhw = False
self._away_temp = away_temp
self._away = False
self.update()
@property
def name(self):
"""Return the name of the honeywell, if any."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self._is_dhw:
return None
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
self.device.set_temperature(self._name, temperature)
@property
def current_operation(self: ClimateDevice) -> str:
"""Get the current operation of the system."""
return getattr(self.device, 'system_mode', None)
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
"""Set the HVAC mode for the thermostat."""
if hasattr(self.device, 'system_mode'):
self.device.system_mode = operation_mode
def turn_away_mode_on(self):
"""Turn away on.
Evohome does have a proprietary away mode, but it doesn't really work
the way it should. For example: If you set a temperature manually
it doesn't get overwritten when away mode is switched on.
"""
self._away = True
self.device.set_temperature(self._name, self._away_temp)
def turn_away_mode_off(self):
"""Turn away off."""
self._away = False
self.device.cancel_temp_override(self._name)
def update(self):
"""Get the latest date."""
try:
# Only refresh if this is the "master" device,
# others will pick up the cache
for val in self.device.temperatures(force_refresh=self._master):
if val['id'] == self._id:
data = val
except StopIteration:
_LOGGER.error("Did not receive any temperature data from the "
"evohomeclient API.")
return
self._current_temperature = data['temp']
self._target_temperature = data['setpoint']
if data['thermostat'] == "DOMESTIC_HOT_WATER":
self._name = "Hot Water"
self._is_dhw = True
else:
self._name = data['name']
self._is_dhw = False
# pylint: disable=abstract-method
class HoneywellUSThermostat(ClimateDevice):
"""Representation of a Honeywell US Thermostat."""
def __init__(self, client, device):
"""Initialize the thermostat."""
self._client = client
self._device = device
@property
def is_fan_on(self):
"""Return true if fan is on."""
return self._device.fan_running
@property
def name(self):
"""Return the name of the honeywell, if any."""
return self._device.name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return (TEMP_CELSIUS if self._device.temperature_unit == 'C'
else TEMP_FAHRENHEIT)
@property
def current_temperature(self):
"""Return the current temperature."""
self._device.refresh()
return self._device.current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self._device.system_mode == 'cool':
return self._device.setpoint_cool
else:
return self._device.setpoint_heat
@property
def current_operation(self: ClimateDevice) -> str:
"""Return current operation ie. heat, cool, idle."""
return getattr(self._device, 'system_mode', None)
def set_temperature(self, temperature):
"""Set target temperature."""
import somecomfort
try:
if self._device.system_mode == 'cool':
self._device.setpoint_cool = temperature
else:
self._device.setpoint_heat = temperature
except somecomfort.SomeComfortError:
_LOGGER.error('Temperature %.1f out of range', temperature)
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {'fan': (self.is_fan_on and 'running' or 'idle'),
'fanmode': self._device.fan_mode,
'system_mode': self._device.system_mode}
def turn_away_mode_on(self):
"""Turn away on."""
pass
def turn_away_mode_off(self):
"""Turn away off."""
pass
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
"""Set the system mode (Cool, Heat, etc)."""
if hasattr(self._device, 'system_mode'):
self._device.system_mode = operation_mode

View File

@ -0,0 +1,83 @@
"""
Support for KNX thermostats.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/knx/
"""
import logging
from homeassistant.components.climate import ClimateDevice
from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.knx import (
KNXConfig, KNXMultiAddressDevice)
DEPENDENCIES = ["knx"]
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create and add an entity based on the configuration."""
add_entities([
KNXThermostat(hass, KNXConfig(config))
])
class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
"""Representation of a KNX thermostat.
A KNX thermostat will has the following parameters:
- temperature (current temperature)
- setpoint (target temperature in HASS terms)
- operation mode selection (comfort/night/frost protection)
This version supports only polling. Messages from the KNX bus do not
automatically update the state of the thermostat (to be implemented
in future releases)
"""
def __init__(self, hass, config):
"""Initialize the thermostat based on the given configuration."""
KNXMultiAddressDevice.__init__(self, hass, config,
["temperature", "setpoint"],
["mode"])
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
self._away = False # not yet supported
self._is_fan_on = False # not yet supported
@property
def should_poll(self):
"""Polling is needed for the KNX thermostat."""
return True
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value("temperature"))
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
from knxip.conversion import knx2_to_float
return knx2_to_float(self.value("setpoint"))
def set_temperature(self, temperature):
"""Set new target temperature."""
from knxip.conversion import float_to_knx2
self.set_value("setpoint", float_to_knx2(temperature))
_LOGGER.debug("Set target temperature to %s", temperature)
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
raise NotImplementedError()

View File

@ -0,0 +1,189 @@
"""
Support for Nest thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.nest/
"""
import voluptuous as vol
import homeassistant.components.nest as nest
from homeassistant.components.climate import (
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
from homeassistant.const import TEMP_CELSIUS, CONF_PLATFORM, CONF_SCAN_INTERVAL
DEPENDENCIES = ['nest']
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): nest.DOMAIN,
vol.Optional(CONF_SCAN_INTERVAL):
vol.All(vol.Coerce(int), vol.Range(min=1)),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Nest thermostat."""
add_devices([NestThermostat(structure, device)
for structure, device in nest.devices()])
# pylint: disable=abstract-method
class NestThermostat(ClimateDevice):
"""Representation of a Nest thermostat."""
def __init__(self, structure, device):
"""Initialize the thermostat."""
self.structure = structure
self.device = device
@property
def name(self):
"""Return the name of the nest, if any."""
location = self.device.where
name = self.device.name
if location is None:
return name
else:
if name == '':
return location.capitalize()
else:
return location.capitalize() + '(' + name + ')'
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
# Move these to Thermostat Device and make them global
return {
"humidity": self.device.humidity,
"target_humidity": self.device.target_humidity,
"mode": self.device.mode
}
@property
def current_temperature(self):
"""Return the current temperature."""
return self.device.temperature
@property
def operation(self):
"""Return current operation ie. heat, cool, idle."""
if self.device.hvac_ac_state is True:
return STATE_COOL
elif self.device.hvac_heater_state is True:
return STATE_HEAT
else:
return STATE_IDLE
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self.device.mode == 'range':
low, high = self.target_temperature_low, \
self.target_temperature_high
if self.operation == STATE_COOL:
temp = high
elif self.operation == STATE_HEAT:
temp = low
else:
# If the outside temp is lower than the current temp, consider
# the 'low' temp to the target, otherwise use the high temp
if (self.device.structure.weather.current.temperature <
self.current_temperature):
temp = low
else:
temp = high
else:
if self.is_away_mode_on:
# away_temperature is a low, high tuple. Only one should be set
# if not in range mode, the other will be None
temp = self.device.away_temperature[0] or \
self.device.away_temperature[1]
else:
temp = self.device.target
return temp
@property
def target_temperature_low(self):
"""Return the lower bound temperature we try to reach."""
if self.is_away_mode_on and self.device.away_temperature[0]:
# away_temperature is always a low, high tuple
return self.device.away_temperature[0]
if self.device.mode == 'range':
return self.device.target[0]
return self.target_temperature
@property
def target_temperature_high(self):
"""Return the upper bound temperature we try to reach."""
if self.is_away_mode_on and self.device.away_temperature[1]:
# away_temperature is always a low, high tuple
return self.device.away_temperature[1]
if self.device.mode == 'range':
return self.device.target[1]
return self.target_temperature
@property
def is_away_mode_on(self):
"""Return if away mode is on."""
return self.structure.away
def set_temperature(self, temperature):
"""Set new target temperature."""
if self.device.mode == 'range':
if self.target_temperature == self.target_temperature_low:
temperature = (temperature, self.target_temperature_high)
elif self.target_temperature == self.target_temperature_high:
temperature = (self.target_temperature_low, temperature)
self.device.target = temperature
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
self.device.mode = operation_mode
def turn_away_mode_on(self):
"""Turn away on."""
self.structure.away = True
def turn_away_mode_off(self):
"""Turn away off."""
self.structure.away = False
@property
def is_fan_on(self):
"""Return whether the fan is on."""
return self.device.fan
def turn_fan_on(self):
"""Turn fan on."""
self.device.fan = True
def turn_fan_off(self):
"""Turn fan off."""
self.device.fan = False
@property
def min_temp(self):
"""Identify min_temp in Nest API or defaults if not available."""
temp = self.device.away_temperature.low
if temp is None:
return super().min_temp
else:
return temp
@property
def max_temp(self):
"""Identify max_temp in Nest API or defaults if not available."""
temp = self.device.away_temperature.high
if temp is None:
return super().max_temp
else:
return temp
def update(self):
"""Python-nest has its own mechanism for staying up to date."""
pass

View File

@ -0,0 +1,90 @@
"""
Support for Proliphix NT10e Thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.proliphix/
"""
from homeassistant.components.climate import (
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT)
REQUIREMENTS = ['proliphix==0.3.1']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Proliphix thermostats."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
import proliphix
pdp = proliphix.PDP(host, username, password)
add_devices([
ProliphixThermostat(pdp)
])
# pylint: disable=abstract-method
class ProliphixThermostat(ClimateDevice):
"""Representation a Proliphix thermostat."""
def __init__(self, pdp):
"""Initialize the thermostat."""
self._pdp = pdp
# initial data
self._pdp.update()
self._name = self._pdp.name
@property
def should_poll(self):
"""Polling needed for thermostat."""
return True
def update(self):
"""Update the data from the thermostat."""
self._pdp.update()
@property
def name(self):
"""Return the name of the thermostat."""
return self._name
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
"fan": self._pdp.fan_state
}
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def current_temperature(self):
"""Return the current temperature."""
return self._pdp.cur_temp
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._pdp.setback
@property
def current_operation(self):
"""Return the current state of the thermostat."""
state = self._pdp.hvac_state
if state in (1, 2):
return STATE_IDLE
elif state == 3:
return STATE_HEAT
elif state == 6:
return STATE_COOL
def set_temperature(self, temperature):
"""Set new target temperature."""
self._pdp.setback = temperature

View File

@ -0,0 +1,136 @@
"""
Support for Radio Thermostat wifi-enabled home thermostats.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.radiotherm/
"""
import datetime
import logging
from urllib.error import URLError
from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF,
ClimateDevice)
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT
REQUIREMENTS = ['radiotherm==1.2']
HOLD_TEMP = 'hold_temp'
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Radio Thermostat."""
import radiotherm
hosts = []
if CONF_HOST in config:
hosts = config[CONF_HOST]
else:
hosts.append(radiotherm.discover.discover_address())
if hosts is None:
_LOGGER.error("No radiotherm thermostats detected.")
return False
hold_temp = config.get(HOLD_TEMP, False)
tstats = []
for host in hosts:
try:
tstat = radiotherm.get_thermostat(host)
tstats.append(RadioThermostat(tstat, hold_temp))
except (URLError, OSError):
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
host)
add_devices(tstats)
# pylint: disable=abstract-method
class RadioThermostat(ClimateDevice):
"""Representation of a Radio Thermostat."""
def __init__(self, device, hold_temp):
"""Initialize the thermostat."""
self.device = device
self.set_time()
self._target_temperature = None
self._current_temperature = None
self._current_operation = STATE_IDLE
self._name = None
self.hold_temp = hold_temp
self.update()
@property
def name(self):
"""Return the name of the Radio Thermostat."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
"fan": self.device.fmode['human'],
"mode": self.device.tmode['human']
}
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def current_operation(self):
"""Return the current operation. head, cool idle."""
return self._current_operation
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def update(self):
"""Update the data from the thermostat."""
self._current_temperature = self.device.temp['raw']
self._name = self.device.name['raw']
if self.device.tmode['human'] == 'Cool':
self._target_temperature = self.device.t_cool['raw']
self._current_operation = STATE_COOL
elif self.device.tmode['human'] == 'Heat':
self._target_temperature = self.device.t_heat['raw']
self._current_operation = STATE_HEAT
else:
self._current_operation = STATE_IDLE
def set_temperature(self, temperature):
"""Set new target temperature."""
if self._current_operation == STATE_COOL:
self.device.t_cool = temperature
elif self._current_operation == STATE_HEAT:
self.device.t_heat = temperature
if self.hold_temp:
self.device.hold = 1
else:
self.device.hold = 0
def set_time(self):
"""Set device time."""
now = datetime.datetime.now()
self.device.time = {'day': now.weekday(),
'hour': now.hour, 'minute': now.minute}
def set_operation_mode(self, operation_mode):
"""Set operation mode (auto, cool, heat, off)."""
if operation_mode == STATE_OFF:
self.device.tmode = 0
elif operation_mode == STATE_AUTO:
self.device.tmode = 3
elif operation_mode == STATE_COOL:
self.device.t_cool = self._target_temperature
elif operation_mode == STATE_HEAT:
self.device.t_heat = self._target_temperature

View File

@ -0,0 +1,84 @@
set_aux_heat:
description: Turn auxillary heater on/off for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
aux_heat:
description: New value of axillary heater
example: true
set_away_mode:
description: Turn away mode on/off for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
away_mode:
description: New value of away mode
example: true
set_temperature:
description: Set target temperature of climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
temperature:
description: New target temperature for hvac
example: 25
set_humidity:
description: Set target humidity of climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.kitchen'
humidity:
description: New target humidity for climate device
example: 60
set_fan_mode:
description: Set fan operation for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climate.nest'
fan:
description: New value of fan mode
example: On Low
set_operation_mode:
description: Set operation mode for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: 'climet.nest'
operation_mode:
description: New value of operation mode
example: Heat
set_swing_mode:
description: Set swing operation for climate device
fields:
entity_id:
description: Name(s) of entities to change
example: '.nest'
swing_mode:
description: New value of swing mode
example: 1

View File

@ -0,0 +1,253 @@
"""
Support for ZWave climate devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
from homeassistant.components.climate import DOMAIN
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.zwave import (
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
from homeassistant.components import zwave
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
_LOGGER = logging.getLogger(__name__)
CONF_NAME = 'name'
DEFAULT_NAME = 'ZWave Climate'
REMOTEC = 0x5254
REMOTEC_ZXT_120 = 0x8377
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
COMMAND_CLASS_CONFIGURATION = 0x70
WORKAROUND_ZXT_120 = 'zxt_120'
DEVICE_MAPPINGS = {
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
}
ZXT_120_SET_TEMP = {
'Heat': 1,
'Cool': 2,
'Dry Air': 8,
'Auto Changeover': 10
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ZWave Climate devices."""
if discovery_info is None or zwave.NETWORK is None:
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
discovery_info, zwave.NETWORK)
return
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
value = node.values[discovery_info[ATTR_VALUE_ID]]
value.set_change_verified(False)
add_devices([ZWaveClimate(value)])
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
discovery_info, zwave.NETWORK)
# pylint: disable=too-many-arguments, abstract-method
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Represents a ZWave Climate device."""
# pylint: disable=too-many-public-methods, too-many-instance-attributes
def __init__(self, value):
"""Initialize the zwave climate device."""
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
self._target_temperature = None
self._current_temperature = None
self._current_operation = None
self._operation_list = None
self._current_fan_mode = None
self._fan_list = None
self._current_swing_mode = None
self._swing_list = None
self._unit = None
self._index = None
self._zxt_120 = None
self.update_properties()
# register listener
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
" workaround")
self._zxt_120 = 1
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties()
self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Set point
temps = []
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
self._unit = value.units
temps.append(int(value.data))
if value.index == self._index:
self._target_temperature = int(value.data)
self._target_temperature_high = max(temps)
self._target_temperature_low = min(temps)
# Operation Mode
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
self._current_operation = value.data
self._operation_list = list(value.data_items)
_LOGGER.debug("self._operation_list=%s", self._operation_list)
_LOGGER.debug("self._current_operation=%s",
self._current_operation)
# Current Temp
for value in self._node.get_values(
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
if value.label == 'Temperature':
self._current_temperature = int(value.data)
# Fan Mode
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
self._current_fan_mode = value.data
self._fan_list = list(value.data_items)
_LOGGER.debug("self._fan_list=%s", self._fan_list)
_LOGGER.debug("self._current_fan_mode=%s",
self._current_fan_mode)
# Swing mode
if self._zxt_120 == 1:
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data
self._swing_list = list(value.data_items)
_LOGGER.debug("self._swing_list=%s", self._swing_list)
_LOGGER.debug("self._current_swing_mode=%s",
self._current_swing_mode)
@property
def should_poll(self):
"""No polling on ZWave."""
return False
@property
def current_fan_mode(self):
"""Return the fan speed set."""
return self._current_fan_mode
@property
def fan_list(self):
"""List of available fan modes."""
return self._fan_list
@property
def current_swing_mode(self):
"""Return the swing mode set."""
return self._current_swing_mode
@property
def swing_list(self):
"""List of available swing modes."""
return self._swing_list
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
unit = self._unit
if unit == 'C':
return TEMP_CELSIUS
elif unit == 'F':
return TEMP_FAHRENHEIT
else:
_LOGGER.exception("unit_of_measurement=%s is not valid",
unit)
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def current_operation(self):
"""Return the current operation mode."""
return self._current_operation
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
def set_temperature(self, temperature):
"""Set new target temperature."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
if value.command_class != 67 and value.index != self._index:
continue
if self._zxt_120:
# ZXT-120 does not support get setpoint
self._target_temperature = temperature
if ZXT_120_SET_TEMP.get(self._current_operation) \
!= value.index:
continue
_LOGGER.debug("ZXT_120_SET_TEMP=%s and"
" self._current_operation=%s",
ZXT_120_SET_TEMP.get(self._current_operation),
self._current_operation)
# ZXT-120 responds only to whole int
value.data = int(round(temperature, 0))
else:
value.data = int(temperature)
break
def set_fan_mode(self, fan):
"""Set new target fan mode."""
for value in self._node.get_values(
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."""
for value in self._node.get_values(
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."""
if self._zxt_120 == 1:
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
value.data = bytes(swing_mode, 'utf-8')
break

View File

@ -11,26 +11,26 @@ import logging
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity import generate_entity_id
DOMAIN = "configurator"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SERVICE_CONFIGURE = "configure"
STATE_CONFIGURE = "configure"
STATE_CONFIGURED = "configured"
ATTR_LINK_NAME = "link_name"
ATTR_LINK_URL = "link_url"
ATTR_CONFIGURE_ID = "configure_id"
ATTR_DESCRIPTION = "description"
ATTR_DESCRIPTION_IMAGE = "description_image"
ATTR_SUBMIT_CAPTION = "submit_caption"
ATTR_FIELDS = "fields"
ATTR_ERRORS = "errors"
_REQUESTS = {}
_INSTANCES = {} _INSTANCES = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_REQUESTS = {}
ATTR_CONFIGURE_ID = 'configure_id'
ATTR_DESCRIPTION = 'description'
ATTR_DESCRIPTION_IMAGE = 'description_image'
ATTR_ERRORS = 'errors'
ATTR_FIELDS = 'fields'
ATTR_LINK_NAME = 'link_name'
ATTR_LINK_URL = 'link_url'
ATTR_SUBMIT_CAPTION = 'submit_caption'
DOMAIN = 'configurator'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_CONFIGURE = 'configure'
STATE_CONFIGURE = 'configure'
STATE_CONFIGURED = 'configured'
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments

View File

@ -15,20 +15,20 @@ from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = "conversation" REQUIREMENTS = ['fuzzywuzzy==0.11.1']
SERVICE_PROCESS = "process" ATTR_TEXT = 'text'
ATTR_TEXT = "text" DOMAIN = 'conversation'
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
SERVICE_PROCESS = 'process'
SERVICE_PROCESS_SCHEMA = vol.Schema({ SERVICE_PROCESS_SCHEMA = vol.Schema({
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower), vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
}) })
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REQUIREMENTS = ['fuzzywuzzy==0.11.1']
def setup(hass, config): def setup(hass, config):
"""Register the process service.""" """Register the process service."""

View File

@ -0,0 +1,236 @@
"""
Support for Cover devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover/
"""
import os
import logging
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.components import group
from homeassistant.const import (
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION, STATE_OPEN,
STATE_CLOSED, STATE_UNKNOWN, ATTR_ENTITY_ID)
DOMAIN = 'cover'
SCAN_INTERVAL = 15
GROUP_NAME_ALL_COVERS = 'all_covers'
ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format(
GROUP_NAME_ALL_COVERS)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
_LOGGER = logging.getLogger(__name__)
ATTR_CURRENT_POSITION = 'current_position'
ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
ATTR_POSITION = 'position'
ATTR_TILT_POSITION = 'tilt_position'
COVER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
COVER_SET_COVER_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_POSITION):
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
})
COVER_SET_COVER_TILT_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_TILT_POSITION):
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
})
SERVICE_TO_METHOD = {
SERVICE_OPEN_COVER: {'method': 'open_cover'},
SERVICE_CLOSE_COVER: {'method': 'close_cover'},
SERVICE_SET_COVER_POSITION: {
'method': 'set_cover_position',
'schema': COVER_SET_COVER_POSITION_SCHEMA},
SERVICE_STOP_COVER: {'method': 'stop_cover'},
SERVICE_OPEN_COVER_TILT: {'method': 'open_cover_tilt'},
SERVICE_CLOSE_COVER_TILT: {'method': 'close_cover_tilt'},
SERVICE_SET_COVER_TILT_POSITION: {
'method': 'set_cover_tilt_position',
'schema': COVER_SET_COVER_TILT_POSITION_SCHEMA},
}
def is_closed(hass, entity_id=None):
"""Return if the cover is closed based on the statemachine."""
entity_id = entity_id or ENTITY_ID_ALL_COVERS
return hass.states.is_state(entity_id, STATE_CLOSED)
def open_cover(hass, entity_id=None):
"""Open all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_OPEN_COVER, data)
def close_cover(hass, entity_id=None):
"""Close all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, data)
def set_cover_position(hass, position, entity_id=None):
"""Move to specific position all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
data[ATTR_POSITION] = position
hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, data)
def stop_cover(hass, entity_id=None):
"""Stop all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_STOP_COVER, data)
def open_cover_tilt(hass, entity_id=None):
"""Open all or specified cover tilt."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_OPEN_COVER_TILT, data)
def close_cover_tilt(hass, entity_id=None):
"""Close all or specified cover tilt."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_CLOSE_COVER_TILT, data)
def set_cover_tilt_position(hass, tilt_position, entity_id=None):
"""Move to specific tilt position all or specified cover."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
data[ATTR_TILT_POSITION] = tilt_position
hass.services.call(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data)
def stop_cover_tilt(hass, entity_id=None):
"""Stop all or specified cover tilt."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data)
def setup(hass, config):
"""Track states and offer events for covers."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
component.setup(config)
def handle_cover_service(service):
"""Handle calls to the cover services."""
method = SERVICE_TO_METHOD.get(service.service)
params = service.data.copy()
params.pop(ATTR_ENTITY_ID, None)
if method:
for cover in component.extract_from_service(service):
getattr(cover, method['method'])(**params)
if cover.should_poll:
cover.update_ha_state(True)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
for service_name in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service_name].get(
'schema', COVER_SERVICE_SCHEMA)
hass.services.register(DOMAIN, service_name, handle_cover_service,
descriptions.get(service_name), schema=schema)
return True
class CoverDevice(Entity):
"""Representation a cover."""
# pylint: disable=no-self-use
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
pass
@property
def current_cover_tilt_position(self):
"""Return current position of cover tilt.
None is unknown, 0 is closed, 100 is fully open.
"""
pass
@property
def state(self):
"""Return the state of the cover."""
closed = self.is_closed
if closed is None:
return STATE_UNKNOWN
return STATE_CLOSED if closed else STATE_OPEN
@property
def state_attributes(self):
"""Return the state attributes."""
data = {}
current = self.current_cover_position
if current is not None:
data[ATTR_CURRENT_POSITION] = self.current_cover_position
current_tilt = self.current_cover_tilt_position
if current_tilt is not None:
data[ATTR_CURRENT_TILT_POSITION] = self.current_cover_tilt_position
return data
@property
def is_closed(self):
"""Return if the cover is closed or not."""
raise NotImplementedError()
def open_cover(self, **kwargs):
"""Open the cover."""
raise NotImplementedError()
def close_cover(self, **kwargs):
"""Close cover."""
raise NotImplementedError()
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
pass
def stop_cover(self, **kwargs):
"""Stop the cover."""
pass
def open_cover_tilt(self, **kwargs):
"""Open the cover tilt."""
pass
def close_cover_tilt(self, **kwargs):
"""Close the cover tilt."""
pass
def set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
pass
def stop_cover_tilt(self, **kwargs):
"""Stop the cover."""
pass

View File

@ -0,0 +1,128 @@
"""
Support for command line covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.command_line/
"""
import logging
import subprocess
from homeassistant.components.cover import CoverDevice
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.helpers import template
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup cover controlled by shell commands."""
covers = config.get('covers', {})
devices = []
for dev_name, properties in covers.items():
devices.append(
CommandCover(
hass,
properties.get('name', dev_name),
properties.get('opencmd', 'true'),
properties.get('closecmd', 'true'),
properties.get('stopcmd', 'true'),
properties.get('statecmd', False),
properties.get(CONF_VALUE_TEMPLATE, '{{ value }}')))
add_devices_callback(devices)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class CommandCover(CoverDevice):
"""Representation a command line cover."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, command_open, command_close, command_stop,
command_state, value_template):
"""Initialize the cover."""
self._hass = hass
self._name = name
self._state = None
self._command_open = command_open
self._command_close = command_close
self._command_stop = command_stop
self._command_state = command_state
self._value_template = value_template
@staticmethod
def _move_cover(command):
"""Execute the actual commands."""
_LOGGER.info('Running command: %s', command)
success = (subprocess.call(command, shell=True) == 0)
if not success:
_LOGGER.error('Command failed: %s', command)
return success
@staticmethod
def _query_state_value(command):
"""Execute state command for return value."""
_LOGGER.info('Running state command: %s', command)
try:
return_value = subprocess.check_output(command, shell=True)
return return_value.strip().decode('utf-8')
except subprocess.CalledProcessError:
_LOGGER.error('Command failed: %s', command)
@property
def should_poll(self):
"""Only poll if we have state command."""
return self._command_state is not None
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
if self.current_cover_position > 0:
return False
else:
return True
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._state
def _query_state(self):
"""Query for the state."""
if not self._command_state:
_LOGGER.error('No state command specified')
return
return self._query_state_value(self._command_state)
def update(self):
"""Update device state."""
if self._command_state:
payload = str(self._query_state())
if self._value_template:
payload = template.render_with_possible_json_value(
self._hass, self._value_template, payload)
self._state = int(payload)
def open_cover(self, **kwargs):
"""Open the cover."""
self._move_cover(self._command_open)
def close_cover(self, **kwargs):
"""Close the cover."""
self._move_cover(self._command_close)
def stop_cover(self, **kwargs):
"""Stop the cover."""
self._move_cover(self._command_stop)

View File

@ -0,0 +1,173 @@
"""
Demo platform for the cover component.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.cover import CoverDevice
from homeassistant.const import EVENT_TIME_CHANGED
from homeassistant.helpers.event import track_utc_time_change
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo covers."""
add_devices([
DemoCover(hass, 'Kitchen Window'),
DemoCover(hass, 'Hall Window', 10),
DemoCover(hass, 'Living Room Window', 70, 50),
])
class DemoCover(CoverDevice):
"""Representation of a demo cover."""
# pylint: disable=no-self-use, too-many-instance-attributes
def __init__(self, hass, name, position=None, tilt_position=None):
"""Initialize the cover."""
self.hass = hass
self._name = name
self._position = position
self._set_position = None
self._set_tilt_position = None
self._tilt_position = tilt_position
self._closing = True
self._closing_tilt = True
self._listener_cover = None
self._listener_cover_tilt = None
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def should_poll(self):
"""No polling needed for a demo cover."""
return False
@property
def current_cover_position(self):
"""Return the current position of the cover."""
return self._position
@property
def current_cover_tilt_position(self):
"""Return the current tilt position of the cover."""
return self._tilt_position
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._position is not None:
if self.current_cover_position > 0:
return False
else:
return True
else:
return None
def close_cover(self, **kwargs):
"""Close the cover."""
if self._position in (0, None):
return
self._listen_cover()
self._closing = True
def close_cover_tilt(self, **kwargs):
"""Close the cover tilt."""
if self._tilt_position in (0, None):
return
self._listen_cover_tilt()
self._closing_tilt = True
def open_cover(self, **kwargs):
"""Open the cover."""
if self._position in (100, None):
return
self._listen_cover()
self._closing = False
def open_cover_tilt(self, **kwargs):
"""Open the cover tilt."""
if self._tilt_position in (100, None):
return
self._listen_cover_tilt()
self._closing_tilt = False
def set_cover_position(self, position, **kwargs):
"""Move the cover to a specific position."""
self._set_position = round(position, -1)
if self._position == position:
return
self._listen_cover()
self._closing = position < self._position
def set_cover_tilt_position(self, tilt_position, **kwargs):
"""Move the cover til to a specific position."""
self._set_tilt_position = round(tilt_position, -1)
if self._tilt_position == tilt_position:
return
self._listen_cover_tilt()
self._closing_tilt = tilt_position < self._tilt_position
def stop_cover(self, **kwargs):
"""Stop the cover."""
if self._position is None:
return
if self._listener_cover is not None:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED,
self._listener_cover)
self._listener_cover = None
self._set_position = None
def stop_cover_tilt(self, **kwargs):
"""Stop the cover tilt."""
if self._tilt_position is None:
return
if self._listener_cover_tilt is not None:
self.hass.bus.remove_listener(EVENT_TIME_CHANGED,
self._listener_cover_tilt)
self._listener_cover_tilt = None
self._set_tilt_position = None
def _listen_cover(self):
"""Listen for changes in cover."""
if self._listener_cover is None:
self._listener_cover = track_utc_time_change(
self.hass, self._time_changed_cover)
def _time_changed_cover(self, now):
"""Track time changes."""
if self._closing:
self._position -= 10
else:
self._position += 10
if self._position in (100, 0, self._set_position):
self.stop_cover()
self.update_ha_state()
def _listen_cover_tilt(self):
"""Listen for changes in cover tilt."""
if self._listener_cover_tilt is None:
self._listener_cover_tilt = track_utc_time_change(
self.hass, self._time_changed_cover_tilt)
def _time_changed_cover_tilt(self, now):
"""Track time changes."""
if self._closing_tilt:
self._tilt_position -= 10
else:
self._tilt_position += 10
if self._tilt_position in (100, 0, self._set_tilt_position):
self.stop_cover_tilt()
self.update_ha_state()

View File

@ -0,0 +1,101 @@
"""
The homematic cover platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.homematic/
Important: For this platform to work the homematic component has to be
properly configured.
"""
import logging
from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.cover import CoverDevice,\
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(HMCover,
discovery_info,
add_callback_devices)
# pylint: disable=abstract-method
class HMCover(homematic.HMDevice, CoverDevice):
"""Represents a Homematic Cover in Home Assistant."""
@property
def current_cover_position(self):
"""
Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
if self.available:
return int((1 - self._hm_get_state()) * 100)
return None
def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
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 is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
if self.current_cover_position > 0:
return False
else:
return True
def open_cover(self, **kwargs):
"""Open the cover."""
if self.available:
self._hmdevice.move_up(self._channel)
def close_cover(self, **kwargs):
"""Close the cover."""
if self.available:
self._hmdevice.move_down(self._channel)
def stop_cover(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 cover!", 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})

View File

@ -0,0 +1,167 @@
"""
Support for MQTT cover devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.mqtt/
"""
import logging
import voluptuous as vol
import homeassistant.components.mqtt as mqtt
from homeassistant.components.cover import CoverDevice
from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN,
STATE_CLOSED)
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mqtt']
CONF_PAYLOAD_OPEN = 'payload_open'
CONF_PAYLOAD_CLOSE = 'payload_close'
CONF_PAYLOAD_STOP = 'payload_stop'
CONF_STATE_OPEN = 'state_open'
CONF_STATE_CLOSED = 'state_closed'
DEFAULT_NAME = "MQTT Cover"
DEFAULT_PAYLOAD_OPEN = "OPEN"
DEFAULT_PAYLOAD_CLOSE = "CLOSE"
DEFAULT_PAYLOAD_STOP = "STOP"
DEFAULT_OPTIMISTIC = False
DEFAULT_RETAIN = False
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string,
vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string,
vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
})
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Add MQTT Cover."""
add_devices_callback([MqttCover(
hass,
config[CONF_NAME],
config.get(CONF_STATE_TOPIC),
config[CONF_COMMAND_TOPIC],
config[CONF_QOS],
config[CONF_RETAIN],
config[CONF_STATE_OPEN],
config[CONF_STATE_CLOSED],
config[CONF_PAYLOAD_OPEN],
config[CONF_PAYLOAD_CLOSE],
config[CONF_PAYLOAD_STOP],
config[CONF_OPTIMISTIC],
config.get(CONF_VALUE_TEMPLATE)
)])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttCover(CoverDevice):
"""Representation of a cover that can be controlled using MQTT."""
def __init__(self, hass, name, state_topic, command_topic, qos,
retain, state_open, state_closed, payload_open, payload_close,
payload_stop, optimistic, value_template):
"""Initialize the cover."""
self._position = None
self._state = None
self._hass = hass
self._name = name
self._state_topic = state_topic
self._command_topic = command_topic
self._qos = qos
self._payload_open = payload_open
self._payload_close = payload_close
self._payload_stop = payload_stop
self._state_open = state_open
self._state_closed = state_closed
self._retain = retain
self._optimistic = optimistic or state_topic is None
def message_received(topic, payload, qos):
"""A new MQTT message has been received."""
if value_template is not None:
payload = template.render_with_possible_json_value(
hass, value_template, payload)
if payload == self._state_open:
self._state = False
self.update_ha_state()
elif payload == self._state_closed:
self._state = True
self.update_ha_state()
elif payload.isnumeric() and 0 <= int(payload) <= 100:
self._state = int(payload)
self._position = int(payload)
self.update_ha_state()
else:
_LOGGER.warning(
"Payload is not True or False or"
" integer(0-100) %s", payload)
if self._state_topic is None:
# Force into optimistic mode.
self._optimistic = True
else:
mqtt.subscribe(hass, self._state_topic, message_received,
self._qos)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
if self.current_cover_position > 0:
return False
else:
return True
@property
def current_cover_position(self):
"""Return current position of cover.
None is unknown, 0 is closed, 100 is fully open.
"""
return self._position
def open_cover(self, **kwargs):
"""Move the cover up."""
mqtt.publish(self.hass, self._command_topic, self._payload_open,
self._qos, self._retain)
if self._optimistic:
# Optimistically assume that cover has changed state.
self._state = 100
self.update_ha_state()
def close_cover(self, **kwargs):
"""Move the cover down."""
mqtt.publish(self.hass, self._command_topic, self._payload_close,
self._qos, self._retain)
if self._optimistic:
# Optimistically assume that cover has changed state.
self._state = 0
self.update_ha_state()
def stop_cover(self, **kwargs):
"""Stop the device."""
mqtt.publish(self.hass, self._command_topic, self._payload_stop,
self._qos, self._retain)

View File

@ -0,0 +1,67 @@
"""
Support for RFXtrx cover components.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.rfxtrx/
"""
import homeassistant.components.rfxtrx as rfxtrx
from homeassistant.components.cover import CoverDevice
DEPENDENCIES = ['rfxtrx']
PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the RFXtrx cover."""
import RFXtrx as rfxtrxmod
# Add cover from config file
covers = rfxtrx.get_devices_from_config(config,
RfxtrxCover)
add_devices_callback(covers)
def cover_update(event):
"""Callback for cover updates from the RFXtrx gateway."""
if not isinstance(event.device, rfxtrxmod.LightingDevice) or \
event.device.known_to_be_dimmable or \
not event.device.known_to_be_rollershutter:
return
new_device = rfxtrx.get_new_device(event, config, RfxtrxCover)
if new_device:
add_devices_callback([new_device])
rfxtrx.apply_received_command(event)
# Subscribe to main rfxtrx events
if cover_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update)
# pylint: disable=abstract-method
class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice):
"""Representation of an rfxtrx cover."""
@property
def should_poll(self):
"""No polling available in rfxtrx cover."""
return False
@property
def is_closed(self):
"""Return if the cover is closed."""
return None
def open_cover(self, **kwargs):
"""Move the cover up."""
self._send_command("roll_up")
def close_cover(self, **kwargs):
"""Move the cover down."""
self._send_command("roll_down")
def stop_cover(self, **kwargs):
"""Stop the cover."""
self._send_command("stop_roll")

View File

@ -0,0 +1,112 @@
"""
Support for building a Raspberry Pi cover 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/cover.rpi_gpio/
"""
import logging
from time import sleep
import voluptuous as vol
from homeassistant.components.cover import CoverDevice
import homeassistant.components.rpi_gpio as rpi_gpio
import homeassistant.helpers.config_validation as cv
RELAY_TIME = 'relay_time'
STATE_PULL_MODE = 'state_pull_mode'
DEFAULT_PULL_MODE = 'UP'
DEFAULT_RELAY_TIME = .2
DEPENDENCIES = ['rpi_gpio']
_LOGGER = logging.getLogger(__name__)
_COVERS_SCHEMA = vol.All(
cv.ensure_list,
[
vol.Schema({
'name': str,
'relay_pin': int,
'state_pin': int,
})
]
)
PLATFORM_SCHEMA = vol.Schema({
'platform': str,
vol.Required('covers'): _COVERS_SCHEMA,
vol.Optional(STATE_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string,
vol.Optional(RELAY_TIME, default=DEFAULT_RELAY_TIME): vol.Coerce(int),
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the cover platform."""
relay_time = config.get(RELAY_TIME)
state_pull_mode = config.get(STATE_PULL_MODE)
covers = []
covers_conf = config.get('covers')
for cover in covers_conf:
covers.append(RPiGPIOCover(cover['name'], cover['relay_pin'],
cover['state_pin'],
state_pull_mode,
relay_time))
add_devices(covers)
# pylint: disable=abstract-method
class RPiGPIOCover(CoverDevice):
"""Representation of a Raspberry cover."""
# pylint: disable=too-many-arguments
def __init__(self, name, relay_pin, state_pin,
state_pull_mode, relay_time):
"""Initialize the cover."""
self._name = name
self._state = False
self._relay_pin = relay_pin
self._state_pin = state_pin
self._state_pull_mode = state_pull_mode
self._relay_time = relay_time
rpi_gpio.setup_output(self._relay_pin)
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
rpi_gpio.write_output(self._relay_pin, True)
@property
def unique_id(self):
"""Return the ID of this cover."""
return "{}.{}".format(self.__class__, self._name)
@property
def name(self):
"""Return the name of the cover if any."""
return self._name
def update(self):
"""Update the state of the cover."""
self._state = rpi_gpio.read_input(self._state_pin)
@property
def is_closed(self):
"""Return true if cover is closed."""
return self._state
def _trigger(self):
"""Trigger the cover."""
rpi_gpio.write_output(self._relay_pin, False)
sleep(self._relay_time)
rpi_gpio.write_output(self._relay_pin, True)
def close_cover(self):
"""Close the cover."""
if not self.is_closed:
self._trigger()
def open_cover(self):
"""Open the cover."""
if self.is_closed:
self._trigger()

View File

@ -0,0 +1,96 @@
"""
Allow to configure a SCSGate cover.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.scsgate/
"""
import logging
import homeassistant.components.scsgate as scsgate
from homeassistant.components.cover import CoverDevice
from homeassistant.const import CONF_NAME
DEPENDENCIES = ['scsgate']
SCS_ID = 'scs_id'
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the SCSGate cover."""
devices = config.get('devices')
covers = []
logger = logging.getLogger(__name__)
if devices:
for _, entity_info in devices.items():
if entity_info[SCS_ID] in scsgate.SCSGATE.devices:
continue
logger.info("Adding %s scsgate.cover", entity_info[CONF_NAME])
name = entity_info[CONF_NAME]
scs_id = entity_info[SCS_ID]
cover = SCSGateCover(
name=name,
scs_id=scs_id,
logger=logger)
scsgate.SCSGATE.add_device(cover)
covers.append(cover)
add_devices_callback(covers)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class SCSGateCover(CoverDevice):
"""Representation of SCSGate cover."""
def __init__(self, scs_id, name, logger):
"""Initialize the cover."""
self._scs_id = scs_id
self._name = name
self._logger = logger
@property
def scs_id(self):
"""Return the SCSGate ID."""
return self._scs_id
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
return None
def open_cover(self, **kwargs):
"""Move the cover."""
from scsgate.tasks import RaiseRollerShutterTask
scsgate.SCSGATE.append_task(
RaiseRollerShutterTask(target=self._scs_id))
def close_cover(self, **kwargs):
"""Move the cover down."""
from scsgate.tasks import LowerRollerShutterTask
scsgate.SCSGATE.append_task(
LowerRollerShutterTask(target=self._scs_id))
def stop_cover(self, **kwargs):
"""Stop the cover."""
from scsgate.tasks import HaltRollerShutterTask
scsgate.SCSGATE.append_task(HaltRollerShutterTask(target=self._scs_id))
def process_event(self, message):
"""Handle a SCSGate message related with this cover."""
self._logger.debug(
"Rollershutter %s, got message %s",
self._scs_id, message.toggled)

View File

@ -0,0 +1,71 @@
open_cover:
description: Open all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to open
example: 'cover.living_room'
close_cover:
description: Close all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to close
example: 'cover.living_room'
set_cover_position:
description: Move to specific position all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to set cover position
example: 'cover.living_room'
position:
description: Position of the cover (0 to 100)
example: 30
stop_cover:
description: Stop all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to stop
example: 'cover.living_room'
open_cover_tilt:
description: Open all or specified cover tilt
fields:
entity_id:
description: Name(s) of cover(s) tilt to open
example: 'cover.living_room'
close_cover_tilt:
description: Close all or specified cover tilt
fields:
entity_id:
description: Name(s) of cover(s) to close tilt
example: 'cover.living_room'
set_cover_tilt_position:
description: Move to specific position all or specified cover tilt
fields:
entity_id:
description: Name(s) of cover(s) to set cover tilt position
example: 'cover.living_room'
position:
description: Position of the cover (0 to 100)
example: 30
stop_cover_tilt:
description: Stop all or specified cover
fields:
entity_id:
description: Name(s) of cover(s) to stop
example: 'cover.living_room'

View File

@ -0,0 +1,64 @@
"""
Support for Wink Covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.wink/
"""
import logging
from homeassistant.components.cover import CoverDevice
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.13', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink cover platform."""
import pywink
if discovery_info is None:
token = config.get(CONF_ACCESS_TOKEN)
if token is None:
logging.getLogger(__name__).error(
"Missing wink access_token. "
"Get one at https://winkbearertoken.appspot.com/")
return
pywink.set_bearer_token(token)
add_devices(WinkCoverDevice(shade) for shade, door in
pywink.get_shades())
class WinkCoverDevice(WinkDevice, CoverDevice):
"""Representation of a Wink covers."""
def __init__(self, wink):
"""Initialize the cover."""
WinkDevice.__init__(self, wink)
@property
def should_poll(self):
"""Wink Shades don't track their position."""
return False
def close_cover(self):
"""Close the shade."""
self.wink.set_state(0)
def open_cover(self):
"""Open the shade."""
self.wink.set_state(1)
@property
def is_closed(self):
"""Return if the cover is closed."""
state = self.wink.state()
if state == 0:
return True
elif state == 1:
return False
else:
return None

View File

@ -0,0 +1,184 @@
"""
Support for Zwave cover components.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.zwave/
"""
# Because we do not compile openzwave on CI
# pylint: disable=import-error
import logging
from homeassistant.components.cover import DOMAIN
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.components.cover import CoverDevice
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
SOMFY = 0x47
SOMFY_ZRTSI = 0x5a52
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
WORKAROUND = 'workaround'
DEVICE_MAPPINGS = {
SOMFY_ZRTSI_CONTROLLER: WORKAROUND
}
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Z-Wave covers."""
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 and
value.index == 0):
value.set_change_verified(False)
add_devices([ZwaveRollershutter(value)])
elif (value.command_class == zwave.COMMAND_CLASS_SWITCH_BINARY or
value.command_class == zwave.COMMAND_CLASS_BARRIER_OPERATOR):
if value.type != zwave.TYPE_BOOL and \
value.genre != zwave.GENRE_USER:
return
value.set_change_verified(False)
add_devices([ZwaveGarageDoor(value)])
else:
return
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Representation of an Zwave roller shutter."""
def __init__(self, value):
"""Initialize the zwave rollershutter."""
import libopenzwave
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._lozwmgr = libopenzwave.PyManager()
self._lozwmgr.create()
self._node = value.node
self._current_position = None
self._workaround = None
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_type, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND:
_LOGGER.debug("Controller without positioning feedback")
self._workaround = 1
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties()
self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Position value
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Level':
self._current_position = value.data
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position > 0:
return False
else:
return True
@property
def current_cover_position(self):
"""Return the current position of Zwave roller shutter."""
if not self._workaround:
if self._current_position is not None:
if self._current_position <= 5:
return 0
elif self._current_position >= 95:
return 100
else:
return self._current_position
def open_cover(self, **kwargs):
"""Move the roller shutter up."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Open' or \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Down':
self._lozwmgr.pressButton(value.value_id)
break
def close_cover(self, **kwargs):
"""Move the roller shutter down."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Up' or \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Close':
self._lozwmgr.pressButton(value.value_id)
break
def set_cover_position(self, position, **kwargs):
"""Move the roller shutter to a specific position."""
self._node.set_dimmer(self._value.value_id, 100 - position)
def stop_cover(self, **kwargs):
"""Stop the roller shutter."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Open' or \
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Down':
self._lozwmgr.releaseButton(value.value_id)
break
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
"""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._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()
_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_cover(self):
"""Close the garage door."""
self._value.data = False
def open_cover(self):
"""Open the garage door."""
self._value.data = True

View File

@ -11,17 +11,16 @@ import homeassistant.core as ha
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
DOMAIN = "demo"
DEPENDENCIES = ['conversation', 'introduction', 'zone'] DEPENDENCIES = ['conversation', 'introduction', 'zone']
DOMAIN = 'demo'
COMPONENTS_WITH_DEMO_PLATFORM = [ COMPONENTS_WITH_DEMO_PLATFORM = [
'alarm_control_panel', 'alarm_control_panel',
'binary_sensor', 'binary_sensor',
'camera', 'camera',
'climate',
'device_tracker', 'device_tracker',
'garage_door', 'garage_door',
'hvac',
'light', 'light',
'lock', 'lock',
'media_player', 'media_player',
@ -29,7 +28,6 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
'rollershutter', 'rollershutter',
'sensor', 'sensor',
'switch', 'switch',
'thermostat',
] ]

View File

@ -258,7 +258,7 @@ class Device(Entity):
_state = STATE_NOT_HOME _state = STATE_NOT_HOME
def __init__(self, hass, consider_home, home_range, track, dev_id, mac, def __init__(self, hass, consider_home, home_range, track, dev_id, mac,
name=None, picture=None, away_hide=False): name=None, picture=None, gravatar=None, away_hide=False):
"""Initialize a device.""" """Initialize a device."""
self.hass = hass self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id) self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@ -280,7 +280,11 @@ class Device(Entity):
self.config_name = name self.config_name = name
# Configured picture # Configured picture
self.config_picture = picture if gravatar is not None:
self.config_picture = get_gravatar_for_email(gravatar)
else:
self.config_picture = picture
self.away_hide = away_hide self.away_hide = away_hide
@property @property
@ -382,6 +386,7 @@ def load_config(path, hass, consider_home, home_range):
Device(hass, consider_home, home_range, device.get('track', False), Device(hass, consider_home, home_range, device.get('track', False),
str(dev_id).lower(), str(device.get('mac')).upper(), str(dev_id).lower(), str(device.get('mac')).upper(),
device.get('name'), device.get('picture'), device.get('name'), device.get('picture'),
device.get('gravatar'),
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE)) device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
for dev_id, device in load_yaml_config_file(path).items()] for dev_id, device in load_yaml_config_file(path).items()]
except HomeAssistantError: except HomeAssistantError:
@ -425,3 +430,10 @@ def update_config(path, dev_id, device):
(CONF_AWAY_HIDE, (CONF_AWAY_HIDE,
'yes' if device.away_hide else 'no')): 'yes' if device.away_hide else 'no')):
out.write(' {}: {}\n'.format(key, '' if value is None else value)) out.write(' {}: {}\n'.format(key, '' if value is None else value))
def get_gravatar_for_email(email):
"""Return an 80px Gravatar for the given email address."""
import hashlib
url = "https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar"
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())

View File

@ -1,126 +1,129 @@
""" """
Support for Actiontec MI424WR (Verizon FIOS) routers. Support for Actiontec MI424WR (Verizon FIOS) routers.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.actiontec/ https://home-assistant.io/components/device_tracker.actiontec/
""" """
import logging import logging
import re import re
import telnetlib import telnetlib
import threading import threading
from collections import namedtuple from collections import namedtuple
from datetime import timedelta from datetime import timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import DOMAIN import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.util.dt as dt_util
from homeassistant.helpers import validate_config from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA)
from homeassistant.util import Throttle from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) # Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
_LOGGER = logging.getLogger(__name__)
_LEASES_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' + _LEASES_REGEX = re.compile(
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' + r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' + r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' +
r'\ssec') r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' +
r'\ssec')
# pylint: disable=unused-argument PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_scanner(hass, config): vol.Required(CONF_HOST): cv.string,
"""Validate the configuration and return an Actiontec scanner.""" vol.Required(CONF_PASSWORD): cv.string,
if not validate_config(config, vol.Required(CONF_USERNAME): cv.string
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, })
_LOGGER):
return None
scanner = ActiontecDeviceScanner(config[DOMAIN]) # pylint: disable=unused-argument
return scanner if scanner.success_init else None def get_scanner(hass, config):
"""Validate the configuration and return an Actiontec scanner."""
Device = namedtuple("Device", ["mac", "ip", "last_update"]) scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class ActiontecDeviceScanner(object): Device = namedtuple("Device", ["mac", "ip", "last_update"])
"""This class queries a an actiontec router for connected devices."""
def __init__(self, config): class ActiontecDeviceScanner(object):
"""Initialize the scanner.""" """This class queries a an actiontec router for connected devices."""
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME] def __init__(self, config):
self.password = config[CONF_PASSWORD] """Initialize the scanner."""
self.lock = threading.Lock() self.host = config[CONF_HOST]
self.last_results = [] self.username = config[CONF_USERNAME]
data = self.get_actiontec_data() self.password = config[CONF_PASSWORD]
self.success_init = data is not None self.lock = threading.Lock()
_LOGGER.info("actiontec scanner initialized") self.last_results = []
data = self.get_actiontec_data()
def scan_devices(self): self.success_init = data is not None
"""Scan for new devices and return a list with found device IDs.""" _LOGGER.info("actiontec scanner initialized")
self._update_info()
return [client.mac for client in self.last_results] def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
def get_device_name(self, device): self._update_info()
"""Return the name of the given device or None if we don't know.""" return [client.mac for client in self.last_results]
if not self.last_results:
return None def get_device_name(self, device):
for client in self.last_results: """Return the name of the given device or None if we don't know."""
if client.mac == device: if not self.last_results:
return client.ip return None
return None for client in self.last_results:
if client.mac == device:
@Throttle(MIN_TIME_BETWEEN_SCANS) return client.ip
def _update_info(self): return None
"""Ensure the information from the router is up to date.
@Throttle(MIN_TIME_BETWEEN_SCANS)
Return boolean if scanning successful. def _update_info(self):
""" """Ensure the information from the router is up to date.
_LOGGER.info("Scanning")
if not self.success_init: Return boolean if scanning successful.
return False """
_LOGGER.info("Scanning")
with self.lock: if not self.success_init:
now = dt_util.now() return False
actiontec_data = self.get_actiontec_data()
if not actiontec_data: with self.lock:
return False now = dt_util.now()
self.last_results = [Device(data['mac'], name, now) actiontec_data = self.get_actiontec_data()
for name, data in actiontec_data.items() if not actiontec_data:
if data['timevalid'] > -60] return False
_LOGGER.info("actiontec scan successful") self.last_results = [Device(data['mac'], name, now)
return True for name, data in actiontec_data.items()
if data['timevalid'] > -60]
def get_actiontec_data(self): _LOGGER.info("actiontec scan successful")
"""Retrieve data from Actiontec MI424WR and return parsed result.""" return True
try:
telnet = telnetlib.Telnet(self.host) def get_actiontec_data(self):
telnet.read_until(b'Username: ') """Retrieve data from Actiontec MI424WR and return parsed result."""
telnet.write((self.username + '\n').encode('ascii')) try:
telnet.read_until(b'Password: ') telnet = telnetlib.Telnet(self.host)
telnet.write((self.password + '\n').encode('ascii')) telnet.read_until(b'Username: ')
prompt = telnet.read_until( telnet.write((self.username + '\n').encode('ascii'))
b'Wireless Broadband Router> ').split(b'\n')[-1] telnet.read_until(b'Password: ')
telnet.write('firewall mac_cache_dump\n'.encode('ascii')) telnet.write((self.password + '\n').encode('ascii'))
telnet.write('\n'.encode('ascii')) prompt = telnet.read_until(
telnet.read_until(prompt) b'Wireless Broadband Router> ').split(b'\n')[-1]
leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] telnet.write('firewall mac_cache_dump\n'.encode('ascii'))
telnet.write('exit\n'.encode('ascii')) telnet.write('\n'.encode('ascii'))
except EOFError: telnet.read_until(prompt)
_LOGGER.exception("Unexpected response from router") leases_result = telnet.read_until(prompt).split(b'\n')[1:-1]
return telnet.write('exit\n'.encode('ascii'))
except ConnectionRefusedError: except EOFError:
_LOGGER.exception("Connection refused by router," + _LOGGER.exception("Unexpected response from router")
" is telnet enabled?") return
return None except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
devices = {} " is telnet enabled?")
for lease in leases_result: return None
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if match is not None: devices = {}
devices[match.group('ip')] = { for lease in leases_result:
'ip': match.group('ip'), match = _LEASES_REGEX.search(lease.decode('utf-8'))
'mac': match.group('mac').upper(), if match is not None:
'timevalid': int(match.group('timevalid')) devices[match.group('ip')] = {
} 'ip': match.group('ip'),
return devices 'mac': match.group('mac').upper(),
'timevalid': int(match.group('timevalid'))
}
return devices

View File

@ -12,14 +12,36 @@ import threading
from collections import namedtuple from collections import namedtuple
from datetime import timedelta from datetime import timedelta
from homeassistant.components.device_tracker import DOMAIN import voluptuous as vol
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
# Return cached results if last scan was less then this time ago. # Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
CONF_PROTOCOL = 'protocol'
CONF_MODE = 'mode'
CONF_SSH_KEY = 'ssh_key'
CONF_PUB_KEY = 'pub_key'
PLATFORM_SCHEMA = vol.All(
cv.has_at_least_one_key(CONF_PASSWORD, CONF_PUB_KEY, CONF_SSH_KEY),
PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PROTOCOL, default='ssh'):
vol.Schema(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'):
vol.Schema(['router', 'ap']),
vol.Optional(CONF_SSH_KEY): cv.isfile,
vol.Optional(CONF_PUB_KEY): cv.isfile
}))
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pexpect==4.0.1'] REQUIREMENTS = ['pexpect==4.0.1']
@ -57,16 +79,6 @@ _IP_NEIGH_REGEX = re.compile(
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_scanner(hass, config): def get_scanner(hass, config):
"""Validate the configuration and return an ASUS-WRT scanner.""" """Validate the configuration and return an ASUS-WRT scanner."""
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME]},
_LOGGER):
return None
elif CONF_PASSWORD not in config[DOMAIN] and \
'ssh_key' not in config[DOMAIN] and \
'pub_key' not in config[DOMAIN]:
_LOGGER.error('Either a private key or password must be provided')
return None
scanner = AsusWrtDeviceScanner(config[DOMAIN]) scanner = AsusWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
@ -83,11 +95,11 @@ class AsusWrtDeviceScanner(object):
def __init__(self, config): def __init__(self, config):
"""Initialize the scanner.""" """Initialize the scanner."""
self.host = config[CONF_HOST] self.host = config[CONF_HOST]
self.username = str(config[CONF_USERNAME]) self.username = config[CONF_USERNAME]
self.password = str(config.get(CONF_PASSWORD, '')) self.password = config.get(CONF_PASSWORD, '')
self.ssh_key = str(config.get('ssh_key', config.get('pub_key', ''))) self.ssh_key = config.get('ssh_key', config.get('pub_key', ''))
self.protocol = config.get('protocol') self.protocol = config[CONF_PROTOCOL]
self.mode = config.get('mode') self.mode = config[CONF_MODE]
self.lock = threading.Lock() self.lock = threading.Lock()

View File

@ -0,0 +1,108 @@
"""Tracking for bluetooth devices."""
import logging
from datetime import timedelta
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.components.device_tracker import (
YAML_DEVICES,
CONF_TRACK_NEW,
CONF_SCAN_INTERVAL,
DEFAULT_SCAN_INTERVAL,
load_config,
)
import homeassistant.util as util
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['gattlib==0.20150805']
BLE_PREFIX = 'BLE_'
MIN_SEEN_NEW = 5
def setup_scanner(hass, config, see):
"""Setup the Bluetooth LE Scanner."""
# pylint: disable=import-error
from gattlib import DiscoveryService
new_devices = {}
def see_device(address, name, new_device=False):
"""Mark a device as seen."""
if new_device:
if address in new_devices:
_LOGGER.debug("Seen %s %s times", address,
new_devices[address])
new_devices[address] += 1
if new_devices[address] >= MIN_SEEN_NEW:
_LOGGER.debug("Adding %s to tracked devices", address)
devs_to_track.append(address)
else:
return
else:
_LOGGER.debug("Seen %s for the first time", address)
new_devices[address] = 1
return
see(mac=BLE_PREFIX + address, host_name=name.strip("\x00"))
def discover_ble_devices():
"""Discover Bluetooth LE devices."""
_LOGGER.debug("Discovering Bluetooth LE devices")
service = DiscoveryService()
devices = service.discover(10)
_LOGGER.debug("Bluetooth LE devices discovered = %s", devices)
return devices
yaml_path = hass.config.path(YAML_DEVICES)
devs_to_track = []
devs_donot_track = []
# Load all known devices.
# We just need the devices so set consider_home and home range
# to 0
for device in load_config(yaml_path, hass, 0, 0):
# check if device is a valid bluetooth device
if device.mac and device.mac[:3].upper() == BLE_PREFIX:
if device.track:
devs_to_track.append(device.mac[3:])
else:
devs_donot_track.append(device.mac[3:])
# if track new devices is true discover new devices
# on every scan.
track_new = util.convert(config.get(CONF_TRACK_NEW), bool,
len(devs_to_track) == 0)
if not devs_to_track and not track_new:
_LOGGER.warning("No Bluetooth LE devices to track!")
return False
interval = util.convert(config.get(CONF_SCAN_INTERVAL), int,
DEFAULT_SCAN_INTERVAL)
def update_ble(now):
"""Lookup Bluetooth LE devices and update status."""
devs = discover_ble_devices()
for mac in devs_to_track:
_LOGGER.debug("Checking " + mac)
result = mac in devs
if not result:
# Could not lookup device name
continue
see_device(mac, devs[mac])
if track_new:
for address in devs:
if address not in devs_to_track and \
address not in devs_donot_track:
_LOGGER.info("Discovered Bluetooth LE device %s", address)
see_device(address, devs[address], new_device=True)
track_point_in_utc_time(hass, update_ble,
now + timedelta(seconds=interval))
update_ble(dt_util.utcnow())
return True

View File

@ -29,6 +29,9 @@ LOCK = threading.Lock()
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
VALIDATE_LOCATION = 'location'
VALIDATE_TRANSITION = 'transition'
def setup_scanner(hass, config, see): def setup_scanner(hass, config, see):
"""Setup an OwnTracks tracker.""" """Setup an OwnTracks tracker."""
@ -47,16 +50,18 @@ def setup_scanner(hass, config, see):
'because of missing or malformatted data: %s', 'because of missing or malformatted data: %s',
data_type, data) data_type, data)
return None return None
if data_type == VALIDATE_TRANSITION:
return data
if max_gps_accuracy is not None and \ if max_gps_accuracy is not None and \
convert(data.get('acc'), float, 0.0) > max_gps_accuracy: convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
_LOGGER.debug('Skipping %s update because expected GPS ' _LOGGER.warning('Ignoring %s update because expected GPS '
'accuracy %s is not met: %s', 'accuracy %s is not met: %s',
data_type, max_gps_accuracy, data) data_type, max_gps_accuracy, payload)
return None return None
if convert(data.get('acc'), float, 1.0) == 0.0: if convert(data.get('acc'), float, 1.0) == 0.0:
_LOGGER.debug('Skipping %s update because GPS accuracy' _LOGGER.warning('Ignoring %s update because GPS accuracy'
'is zero', 'is zero: %s',
data_type) data_type, payload)
return None return None
return data return data
@ -65,7 +70,7 @@ def setup_scanner(hass, config, see):
"""MQTT message received.""" """MQTT message received."""
# Docs on available data: # Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typelocation # http://owntracks.org/booklet/tech/json/#_typelocation
data = validate_payload(payload, 'location') data = validate_payload(payload, VALIDATE_LOCATION)
if not data: if not data:
return return
@ -86,7 +91,7 @@ def setup_scanner(hass, config, see):
"""MQTT event (geofences) received.""" """MQTT event (geofences) received."""
# Docs on available data: # Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typetransition # http://owntracks.org/booklet/tech/json/#_typetransition
data = validate_payload(payload, 'transition') data = validate_payload(payload, VALIDATE_TRANSITION)
if not data: if not data:
return return
@ -143,14 +148,24 @@ def setup_scanner(hass, config, see):
else: else:
_LOGGER.info("Exit to GPS") _LOGGER.info("Exit to GPS")
# Check for GPS accuracy # Check for GPS accuracy
if not ('acc' in data and valid_gps = True
max_gps_accuracy is not None and if 'acc' in data:
data['acc'] > max_gps_accuracy): if data['acc'] == 0.0:
valid_gps = False
_LOGGER.warning(
'Ignoring GPS in region exit because accuracy'
'is zero: %s',
payload)
if (max_gps_accuracy is not None and
data['acc'] > max_gps_accuracy):
valid_gps = False
_LOGGER.warning(
'Ignoring GPS in region exit because expected '
'GPS accuracy %s is not met: %s',
max_gps_accuracy, payload)
if valid_gps:
see(**kwargs) see(**kwargs)
see_beacons(dev_id, kwargs) see_beacons(dev_id, kwargs)
else:
_LOGGER.info("Inaccurate GPS reported")
beacons = MOBILE_BEACONS_ACTIVE[dev_id] beacons = MOBILE_BEACONS_ACTIVE[dev_id]
if location in beacons: if location in beacons:

View File

@ -313,19 +313,23 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
_LOGGER.info("Loading wireless clients...") _LOGGER.info("Loading wireless clients...")
url = 'http://{}/{}/userRpm/WlanStationRpm.htm' \ mac_results = []
.format(self.host, self.token)
referer = 'http://{}'.format(self.host)
cookie = 'Authorization=Basic {}'.format(self.credentials)
page = requests.get(url, headers={ # Check both the 2.4GHz and 5GHz client list URLs
'cookie': cookie, for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'):
'referer': referer url = 'http://{}/{}/userRpm/{}' \
}) .format(self.host, self.token, clients_url)
result = self.parse_macs.findall(page.text) referer = 'http://{}'.format(self.host)
cookie = 'Authorization=Basic {}'.format(self.credentials)
if not result: page = requests.get(url, headers={
'cookie': cookie,
'referer': referer
})
mac_results.extend(self.parse_macs.findall(page.text))
if not mac_results:
return False return False
self.last_results = [mac.replace("-", ":") for mac in result] self.last_results = [mac.replace("-", ":") for mac in mac_results]
return True return True

View File

@ -12,13 +12,13 @@ import threading
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover from homeassistant.helpers.discovery import load_platform, discover
DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.7.1'] REQUIREMENTS = ['netdisco==0.7.1']
SCAN_INTERVAL = 300 # seconds DOMAIN = 'discovery'
SERVICE_WEMO = 'belkin_wemo' SCAN_INTERVAL = 300 # seconds
SERVICE_NETGEAR = 'netgear_router' SERVICE_NETGEAR = 'netgear_router'
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_NETGEAR: ('device_tracker', None), SERVICE_NETGEAR: ('device_tracker', None),

View File

@ -16,21 +16,20 @@ from homeassistant.helpers import validate_config
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import sanitize_filename from homeassistant.util import sanitize_filename
DOMAIN = "downloader" ATTR_SUBDIR = 'subdir'
ATTR_URL = 'url'
SERVICE_DOWNLOAD_FILE = "download_file"
ATTR_URL = "url"
ATTR_SUBDIR = "subdir"
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
# pylint: disable=no-value-for-parameter
vol.Required(ATTR_URL): vol.Url(),
vol.Optional(ATTR_SUBDIR): cv.string,
})
CONF_DOWNLOAD_DIR = 'download_dir' CONF_DOWNLOAD_DIR = 'download_dir'
DOMAIN = 'downloader'
SERVICE_DOWNLOAD_FILE = 'download_file'
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
vol.Required(ATTR_URL): cv.url,
vol.Optional(ATTR_SUBDIR): cv.string,
})
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def setup(hass, config): def setup(hass, config):

View File

@ -6,30 +6,28 @@ https://home-assistant.io/components/dweet/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNKNOWN from homeassistant.const import (
CONF_NAME, CONF_WHITELIST, EVENT_STATE_CHANGED, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import state as state_helper from homeassistant.helpers import state as state_helper
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['dweepy==0.2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "dweet" DOMAIN = 'dweet'
DEPENDENCIES = []
REQUIREMENTS = ['dweepy==0.2.0']
CONF_NAME = 'name'
CONF_WHITELIST = 'whitelist'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_WHITELIST): cv.string, vol.Required(CONF_WHITELIST, default=[]):
vol.All(cv.ensure_list, [cv.entity_id]),
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -38,8 +36,8 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Setup the Dweet.io component.""" """Setup the Dweet.io component."""
conf = config[DOMAIN] conf = config[DOMAIN]
name = conf[CONF_NAME] name = conf.get(CONF_NAME)
whitelist = conf.get(CONF_WHITELIST, []) whitelist = conf.get(CONF_WHITELIST)
json_body = {} json_body = {}
def dweet_event_listener(event): def dweet_event_listener(event):

View File

@ -7,7 +7,9 @@ https://home-assistant.io/components/ecobee/
import logging import logging
import os import os
from datetime import timedelta from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.loader import get_component from homeassistant.loader import get_component
@ -15,12 +17,19 @@ from homeassistant.util import Throttle
DOMAIN = "ecobee" DOMAIN = "ecobee"
NETWORK = None NETWORK = None
HOLD_TEMP = 'hold_temp' CONF_HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [ REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/' 'https://github.com/nkgilley/python-ecobee-api/archive/'
'4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6'] '4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_API_KEY): cv.string,
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean
})
}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ECOBEE_CONFIG_FILE = 'ecobee.conf' ECOBEE_CONFIG_FILE = 'ecobee.conf'
@ -67,11 +76,12 @@ def setup_ecobee(hass, network, config):
configurator = get_component('configurator') configurator = get_component('configurator')
configurator.request_done(_CONFIGURING.pop('ecobee')) configurator.request_done(_CONFIGURING.pop('ecobee'))
hold_temp = config[DOMAIN].get(HOLD_TEMP, False) hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP)
discovery.load_platform(hass, 'thermostat', DOMAIN, discovery.load_platform(hass, 'climate', DOMAIN,
{'hold_temp': hold_temp}, config) {'hold_temp': hold_temp}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods

View File

@ -0,0 +1,542 @@
"""
Support for local control of entities by emulating the Phillips Hue bridge.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_hue/
"""
import threading
import socket
import logging
import json
import os
import select
import voluptuous as vol
from homeassistant import util, core
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
STATE_ON
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
)
from homeassistant.components.http import (
HomeAssistantView, HomeAssistantWSGI
)
import homeassistant.helpers.config_validation as cv
DOMAIN = 'emulated_hue'
_LOGGER = logging.getLogger(__name__)
CONF_HOST_IP = 'host_ip'
CONF_LISTEN_PORT = 'listen_port'
CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
CONF_EXPOSED_DOMAINS = 'exposed_domains'
ATTR_EMULATED_HUE = 'emulated_hue'
ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
DEFAULT_LISTEN_PORT = 8300
DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene']
DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [
'switch', 'light', 'group', 'input_boolean', 'media_player'
]
HUE_API_STATE_ON = 'on'
HUE_API_STATE_BRI = 'bri'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST_IP): cv.string,
vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list,
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, yaml_config):
"""Activate the emulated_hue component."""
config = Config(yaml_config)
server = HomeAssistantWSGI(
hass,
development=False,
server_host=config.host_ip_addr,
server_port=config.listen_port,
api_password=None,
ssl_certificate=None,
ssl_key=None,
cors_origins=[]
)
server.register_view(DescriptionXmlView(hass, config))
server.register_view(HueUsernameView(hass))
server.register_view(HueLightsView(hass, config))
upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port)
def start_emulated_hue_bridge(event):
"""Start the emulated hue bridge."""
server.start()
upnp_listener.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge."""
upnp_listener.stop()
server.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge)
return True
# pylint: disable=too-few-public-methods
class Config(object):
"""Holds configuration variables for the emulated hue bridge."""
def __init__(self, yaml_config):
"""Initialize the instance."""
conf = yaml_config.get(DOMAIN, {})
# Get the IP address that will be passed to the Echo during discovery
self.host_ip_addr = conf.get(CONF_HOST_IP)
if self.host_ip_addr is None:
self.host_ip_addr = util.get_local_ip()
_LOGGER.warning(
"Listen IP address not specified, auto-detected address is %s",
self.host_ip_addr)
# Get the port that the Hue bridge will listen on
self.listen_port = conf.get(CONF_LISTEN_PORT)
if not isinstance(self.listen_port, int):
self.listen_port = DEFAULT_LISTEN_PORT
_LOGGER.warning(
"Listen port not specified, defaulting to %s",
self.listen_port)
# Get domains that cause both "on" and "off" commands to map to "on"
# This is primarily useful for things like scenes or scripts, which
# don't really have a concept of being off
self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS)
if not isinstance(self.off_maps_to_on_domains, list):
self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS
# Get whether or not entities should be exposed by default, or if only
# explicitly marked ones will be exposed
self.expose_by_default = conf.get(
CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT)
# Get domains that are exposed by default when expose_by_default is
# True
self.exposed_domains = conf.get(
CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS)
class DescriptionXmlView(HomeAssistantView):
"""Handles requests for the description.xml file."""
url = '/description.xml'
name = 'description:xml'
requires_auth = False
def __init__(self, hass, config):
"""Initialize the instance of the view."""
super().__init__(hass)
self.config = config
def get(self, request):
"""Handle a GET request."""
xml_template = """<?xml version="1.0" encoding="UTF-8" ?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://{0}:{1}/</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
<friendlyName>HASS Bridge ({0})</friendlyName>
<manufacturer>Royal Philips Electronics</manufacturer>
<manufacturerURL>http://www.philips.com</manufacturerURL>
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
<modelName>Philips hue bridge 2015</modelName>
<modelNumber>BSB002</modelNumber>
<modelURL>http://www.meethue.com</modelURL>
<serialNumber>1234</serialNumber>
<UDN>uuid:2f402f80-da50-11e1-9b23-001788255acc</UDN>
</device>
</root>
"""
resp_text = xml_template.format(
self.config.host_ip_addr, self.config.listen_port)
return self.Response(resp_text, mimetype='text/xml')
class HueUsernameView(HomeAssistantView):
"""Handle requests to create a username for the emulated hue bridge."""
url = '/api'
name = 'hue:api'
requires_auth = False
def __init__(self, hass):
"""Initialize the instance of the view."""
super().__init__(hass)
def post(self, request):
"""Handle a POST request."""
data = request.json
if 'devicetype' not in data:
return self.Response("devicetype not specified", status=400)
json_response = [{'success': {'username': '12345678901234567890'}}]
return self.json(json_response)
class HueLightsView(HomeAssistantView):
"""Handle requests for getting and setting info about entities."""
url = '/api/<username>/lights'
name = 'api:username:lights'
extra_urls = ['/api/<username>/lights/<entity_id>',
'/api/<username>/lights/<entity_id>/state']
requires_auth = False
def __init__(self, hass, config):
"""Initialize the instance of the view."""
super().__init__(hass)
self.config = config
self.cached_states = {}
def get(self, request, username, entity_id=None):
"""Handle a GET request."""
if entity_id is None:
return self.get_lights_list()
if not request.base_url.endswith('state'):
return self.get_light_state(entity_id)
return self.Response("Method not allowed", status=405)
def put(self, request, username, entity_id=None):
"""Handle a PUT request."""
if not request.base_url.endswith('state'):
return self.Response("Method not allowed", status=405)
content_type = request.environ.get('CONTENT_TYPE', '')
if content_type == 'application/x-www-form-urlencoded':
# Alexa sends JSON data with a form data content type, for
# whatever reason, and Werkzeug parses form data automatically,
# so we need to do some gymnastics to get the data we need
json_data = None
for key in request.form:
try:
json_data = json.loads(key)
break
except ValueError:
# Try the next key?
pass
if json_data is None:
return self.Response("Bad request", status=400)
else:
json_data = request.json
return self.put_light_state(json_data, entity_id)
def get_lights_list(self):
"""Process a request to get the list of available lights."""
json_response = {}
for entity in self.hass.states.all():
if self.is_entity_exposed(entity):
json_response[entity.entity_id] = entity_to_json(entity)
return self.json(json_response)
def get_light_state(self, entity_id):
"""Process a request to get the state of an individual light."""
entity = self.hass.states.get(entity_id)
if entity is None or not self.is_entity_exposed(entity):
return self.Response("Entity not found", status=404)
cached_state = self.cached_states.get(entity_id, None)
if cached_state is None:
final_state = entity.state == STATE_ON
final_brightness = entity.attributes.get(
ATTR_BRIGHTNESS, 255 if final_state else 0)
else:
final_state, final_brightness = cached_state
json_response = entity_to_json(entity, final_state, final_brightness)
return self.json(json_response)
def put_light_state(self, request_json, entity_id):
"""Process a request to set the state of an individual light."""
config = self.config
# Retrieve the entity from the state machine
entity = self.hass.states.get(entity_id)
if entity is None:
return self.Response("Entity not found", status=404)
if not self.is_entity_exposed(entity):
return self.Response("Entity not found", status=404)
# Parse the request into requested "on" status and brightness
parsed = parse_hue_api_put_light_body(request_json, entity)
if parsed is None:
return self.Response("Bad request", status=400)
result, brightness = parsed
# Convert the resulting "on" status into the service we need to call
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
# Construct what we need to send to the service
data = {ATTR_ENTITY_ID: entity_id}
if brightness is not None:
data[ATTR_BRIGHTNESS] = brightness
if entity.domain.lower() in config.off_maps_to_on_domains:
# Map the off command to on
service = SERVICE_TURN_ON
# Caching is required because things like scripts and scenes won't
# report as "off" to Alexa if an "off" command is received, because
# they'll map to "on". Thus, instead of reporting its actual
# status, we report what Alexa will want to see, which is the same
# as the actual requested command.
self.cached_states[entity_id] = (result, brightness)
# Perform the requested action
self.hass.services.call(core.DOMAIN, service, data, blocking=True)
json_response = \
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
if brightness is not None:
json_response.append(create_hue_success_response(
entity_id, HUE_API_STATE_BRI, brightness))
return self.json(json_response)
def is_entity_exposed(self, entity):
"""Determine if an entity should be exposed on the emulated bridge."""
config = self.config
if entity.attributes.get('view') is not None:
# Ignore entities that are views
return False
domain = entity.domain.lower()
explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
domain_exposed_by_default = \
config.expose_by_default and domain in config.exposed_domains
# Expose an entity if the entity's domain is exposed by default and
# the configuration doesn't explicitly exclude it from being
# exposed, or if the entity is explicitly exposed
is_default_exposed = \
domain_exposed_by_default and explicit_expose is not False
return is_default_exposed or explicit_expose
def parse_hue_api_put_light_body(request_json, entity):
"""Parse the body of a request to change the state of a light."""
if HUE_API_STATE_ON in request_json:
if not isinstance(request_json[HUE_API_STATE_ON], bool):
return None
if request_json['on']:
# Echo requested device be turned on
brightness = None
report_brightness = False
result = True
else:
# Echo requested device be turned off
brightness = None
report_brightness = False
result = False
if HUE_API_STATE_BRI in request_json:
# Make sure the entity actually supports brightness
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
try:
# Clamp brightness from 0 to 255
brightness = \
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
except ValueError:
return None
report_brightness = True
result = (brightness > 0)
return (result, brightness) if report_brightness else (result, None)
def entity_to_json(entity, is_on=None, brightness=None):
"""Convert an entity to its Hue bridge JSON representation."""
if is_on is None:
is_on = entity.state == STATE_ON
if brightness is None:
brightness = 255 if is_on else 0
name = entity.attributes.get(
ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
return {
'state':
{
HUE_API_STATE_ON: is_on,
HUE_API_STATE_BRI: brightness,
'reachable': True
},
'type': 'Dimmable light',
'name': name,
'modelid': 'HASS123',
'uniqueid': entity.entity_id,
'swversion': '123'
}
def create_hue_success_response(entity_id, attr, value):
"""Create a success response for an attribute set on a light."""
success_key = '/lights/{}/state/{}'.format(entity_id, attr)
return {'success': {success_key: value}}
class UPNPResponderThread(threading.Thread):
"""Handle responding to UPNP/SSDP discovery requests."""
_interrupted = False
def __init__(self, host_ip_addr, listen_port):
"""Initialize the class."""
threading.Thread.__init__(self)
self.host_ip_addr = host_ip_addr
self.listen_port = listen_port
# Note that the double newline at the end of
# this string is required per the SSDP spec
resp_template = """HTTP/1.1 200 OK
CACHE-CONTROL: max-age=60
EXT:
LOCATION: http://{0}:{1}/description.xml
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
ST: urn:schemas-upnp-org:device:basic:1
USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
"""
self.upnp_response = resp_template.format(host_ip_addr, listen_port) \
.replace("\n", "\r\n") \
.encode('utf-8')
# Set up a pipe for signaling to the receiver that it's time to
# shutdown. Essentially, we place the SSDP socket into nonblocking
# mode and use select() to wait for data to arrive on either the SSDP
# socket or the pipe. If data arrives on either one, select() returns
# and tells us which filenos have data ready to read.
#
# When we want to stop the responder, we write data to the pipe, which
# causes the select() to return and indicate that said pipe has data
# ready to be read, which indicates to us that the responder needs to
# be shutdown.
self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe()
def run(self):
"""Run the server."""
# Listen for UDP port 1900 packets sent to SSDP multicast address
ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ssdp_socket.setblocking(False)
# Required for receiving multicast
ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ssdp_socket.setsockopt(
socket.SOL_IP,
socket.IP_MULTICAST_IF,
socket.inet_aton(self.host_ip_addr))
ssdp_socket.setsockopt(
socket.SOL_IP,
socket.IP_ADD_MEMBERSHIP,
socket.inet_aton("239.255.255.250") +
socket.inet_aton(self.host_ip_addr))
ssdp_socket.bind(("239.255.255.250", 1900))
while True:
if self._interrupted:
clean_socket_close(ssdp_socket)
return
try:
read, _, _ = select.select(
[self._interrupted_read_pipe, ssdp_socket], [],
[ssdp_socket])
if self._interrupted_read_pipe in read:
# Implies self._interrupted is True
clean_socket_close(ssdp_socket)
return
elif ssdp_socket in read:
data, addr = ssdp_socket.recvfrom(1024)
else:
continue
except socket.error as ex:
if self._interrupted:
clean_socket_close(ssdp_socket)
return
_LOGGER.error("UPNP Responder socket exception occured: %s",
ex.__str__)
if "M-SEARCH" in data.decode('utf-8'):
# SSDP M-SEARCH method received, respond to it with our info
resp_socket = socket.socket(
socket.AF_INET, socket.SOCK_DGRAM)
resp_socket.sendto(self.upnp_response, addr)
resp_socket.close()
def stop(self):
"""Stop the server."""
# Request for server
self._interrupted = True
os.write(self._interrupted_write_pipe, bytes([0]))
self.join()
def clean_socket_close(sock):
"""Close a socket connection and logs its closure."""
_LOGGER.info("UPNP responder shutting down.")
sock.close()

View File

@ -4,23 +4,36 @@ EnOcean Component.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/EnOcean/ https://home-assistant.io/components/EnOcean/
""" """
import logging
DOMAIN = "enocean" import voluptuous as vol
from homeassistant.const import CONF_DEVICE
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['enocean==0.31'] REQUIREMENTS = ['enocean==0.31']
CONF_DEVICE = "device" _LOGGER = logging.getLogger(__name__)
DOMAIN = 'enocean'
ENOCEAN_DONGLE = None ENOCEAN_DONGLE = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config): def setup(hass, config):
"""Setup the EnOcean component.""" """Setup the EnOcean component."""
global ENOCEAN_DONGLE global ENOCEAN_DONGLE
serial_dev = config[DOMAIN].get(CONF_DEVICE, "/dev/ttyUSB0") serial_dev = config[DOMAIN].get(CONF_DEVICE)
ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev) ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev)
return True return True

View File

@ -62,8 +62,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_CODE): cv.string, vol.Required(CONF_CODE): cv.string,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA}, vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT): vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT): cv.port,
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION): vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION):
vol.All(vol.Coerce(int), vol.Range(min=3, max=4)), vol.All(vol.Coerce(int), vol.Range(min=3, max=4)),
vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE):

View File

@ -0,0 +1,247 @@
"""
Provides functionality to interact with fans.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/fan/
"""
import logging
import os
import voluptuous as vol
from homeassistant.components import group
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE,
SERVICE_TURN_OFF, ATTR_ENTITY_ID,
STATE_UNKNOWN)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
DOMAIN = 'fan'
SCAN_INTERVAL = 30
GROUP_NAME_ALL_FANS = 'all fans'
ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS)
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Bitfield of features supported by the fan entity
ATTR_SUPPORTED_FEATURES = 'supported_features'
SUPPORT_SET_SPEED = 1
SUPPORT_OSCILLATE = 2
SERVICE_SET_SPEED = 'set_speed'
SERVICE_OSCILLATE = 'oscillate'
SPEED_OFF = 'off'
SPEED_LOW = 'low'
SPEED_MED = 'med'
SPEED_HIGH = 'high'
ATTR_SPEED = 'speed'
ATTR_SPEED_LIST = 'speed_list'
ATTR_OSCILLATING = 'oscillating'
PROP_TO_ATTR = {
'speed': ATTR_SPEED,
'speed_list': ATTR_SPEED_LIST,
'oscillating': ATTR_OSCILLATING,
'supported_features': ATTR_SUPPORTED_FEATURES,
} # type: dict
FAN_SET_SPEED_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_SPEED): cv.string
}) # type: dict
FAN_TURN_ON_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_SPEED): cv.string
}) # type: dict
FAN_TURN_OFF_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids
}) # type: dict
FAN_OSCILLATE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_OSCILLATING): cv.boolean
}) # type: dict
FAN_TOGGLE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids
})
_LOGGER = logging.getLogger(__name__)
def is_on(hass, entity_id: str=None) -> bool:
"""Return if the fans are on based on the statemachine."""
entity_id = entity_id or ENTITY_ID_ALL_FANS
state = hass.states.get(entity_id)
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN]
# pylint: disable=too-many-arguments
def turn_on(hass, entity_id: str=None, speed: str=None) -> None:
"""Turn all or specified fan on."""
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_SPEED, speed),
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
def turn_off(hass, entity_id: str=None) -> None:
"""Turn all or specified fan off."""
data = {
ATTR_ENTITY_ID: entity_id,
}
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
def toggle(hass, entity_id: str=None) -> None:
"""Toggle all or specified fans."""
data = {
ATTR_ENTITY_ID: entity_id
}
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
def oscillate(hass, entity_id: str=None, should_oscillate: bool=True) -> None:
"""Set oscillation on all or specified fan."""
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_OSCILLATING, should_oscillate),
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_OSCILLATE, data)
def set_speed(hass, entity_id: str=None, speed: str=None) -> None:
"""Set speed for all or specified fan."""
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_SPEED, speed),
] if value is not None
}
hass.services.call(DOMAIN, SERVICE_SET_SPEED, data)
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
def setup(hass, config: dict) -> None:
"""Expose fan control via statemachine and services."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS)
component.setup(config)
def handle_fan_service(service: str) -> None:
"""Hande service call for fans."""
# Get the validated data
params = service.data.copy()
# Convert the entity ids to valid fan ids
target_fans = component.extract_from_service(service)
params.pop(ATTR_ENTITY_ID, None)
service_fun = None
for service_def in [SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_SET_SPEED, SERVICE_OSCILLATE]:
if service_def == service.service:
service_fun = service_def
break
if service_fun:
for fan in target_fans:
getattr(fan, service_fun)(**params)
for fan in target_fans:
if fan.should_poll:
fan.update_ha_state(True)
return
# Listen for fan service calls.
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_fan_service,
descriptions.get(SERVICE_TURN_ON),
schema=FAN_TURN_ON_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_fan_service,
descriptions.get(SERVICE_TURN_OFF),
schema=FAN_TURN_OFF_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SET_SPEED, handle_fan_service,
descriptions.get(SERVICE_SET_SPEED),
schema=FAN_SET_SPEED_SCHEMA)
hass.services.register(DOMAIN, SERVICE_OSCILLATE, handle_fan_service,
descriptions.get(SERVICE_OSCILLATE),
schema=FAN_OSCILLATE_SCHEMA)
return True
class FanEntity(ToggleEntity):
"""Representation of a fan."""
# pylint: disable=no-self-use, abstract-method
def set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan."""
pass
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
"""Turn on the fan."""
raise NotImplementedError()
def turn_off(self: ToggleEntity, **kwargs) -> None:
"""Turn off the fan."""
raise NotImplementedError()
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
"""Oscillate the fan."""
pass
@property
def is_on(self):
"""Return true if the entity is on."""
return self.state_attributes.get(ATTR_SPEED, STATE_UNKNOWN) \
not in [SPEED_OFF, STATE_UNKNOWN]
@property
def speed_list(self: ToggleEntity) -> list:
"""Get the list of available speeds."""
return []
@property
def state_attributes(self: ToggleEntity) -> dict:
"""Return optional state attributes."""
data = {} # type: dict
for prop, attr in PROP_TO_ATTR.items():
if not hasattr(self, prop):
continue
value = getattr(self, prop)
if value is not None:
data[attr] = value
return data
@property
def supported_features(self: ToggleEntity) -> int:
"""Flag supported features."""
return 0

View File

@ -0,0 +1,75 @@
"""
Demo garage door platform that has a fake fan.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_HIGH,
FanEntity, SUPPORT_SET_SPEED,
SUPPORT_OSCILLATE)
from homeassistant.const import STATE_OFF
FAN_NAME = 'Living Room Fan'
FAN_ENTITY_ID = 'fan.living_room_fan'
DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup demo garage door platform."""
add_devices_callback([
DemoFan(hass, FAN_NAME, STATE_OFF),
])
class DemoFan(FanEntity):
"""A demonstration fan component."""
def __init__(self, hass, name: str, initial_state: str) -> None:
"""Initialize the entity."""
self.hass = hass
self.speed = initial_state
self.oscillating = False
self._name = name
@property
def name(self) -> str:
"""Get entity name."""
return self._name
@property
def should_poll(self):
"""No polling needed for a demo fan."""
return False
@property
def speed_list(self) -> list:
"""Get the list of available speeds."""
return [STATE_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]
def turn_on(self, speed: str=SPEED_MED) -> None:
"""Turn on the entity."""
self.set_speed(speed)
def turn_off(self) -> None:
"""Turn off the entity."""
self.oscillate(False)
self.set_speed(STATE_OFF)
def set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
self.speed = speed
self.update_ha_state()
def oscillate(self, oscillating: bool) -> None:
"""Set oscillation."""
self.oscillating = oscillating
self.update_ha_state()
@property
def supported_features(self) -> int:
"""Flag supported features."""
return DEMO_SUPPORT

View File

@ -0,0 +1,66 @@
"""
Support for Insteon FanLinc.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.insteon/
"""
import logging
from homeassistant.components.fan import (FanEntity, SUPPORT_SET_SPEED,
SPEED_OFF, SPEED_LOW, SPEED_MED,
SPEED_HIGH)
from homeassistant.components.insteon_hub import (InsteonDevice, INSTEON,
filter_devices)
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
DEVICE_CATEGORIES = [
{
'DevCat': 1,
'SubCat': [46]
}
]
DEPENDENCIES = ['insteon_hub']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Insteon Hub fan platform."""
devs = []
for device in filter_devices(INSTEON.devices, DEVICE_CATEGORIES):
devs.append(InsteonFanDevice(device))
add_devices(devs)
class InsteonFanDevice(InsteonDevice, FanEntity):
"""Represet an insteon fan device."""
def __init__(self, node: object) -> None:
"""Initialize the device."""
super(InsteonFanDevice, self).__init__(node)
self.speed = STATE_UNKNOWN # Insteon hub can't get state via REST
def turn_on(self, speed: str=None):
"""Turn the fan on."""
self.set_speed(speed if speed else SPEED_MED)
def turn_off(self):
"""Turn the fan off."""
self.set_speed(SPEED_OFF)
def set_speed(self, speed: str) -> None:
"""Set the fan speed."""
if self._send_command('fan', payload={'speed', speed}):
self.speed = speed
@property
def supported_features(self) -> int:
"""Get the supported features for device."""
return SUPPORT_SET_SPEED
@property
def speed_list(self) -> list:
"""Get the available speeds for the fan."""
return [SPEED_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]

View File

@ -0,0 +1,53 @@
# Describes the format for available fan services
set_speed:
description: Sets fan speed
fields:
entity_id:
description: Name(s) of the entities to set
example: 'fan.living_room'
speed:
description: Speed setting
example: 'low'
turn_on:
description: Turns fan on
fields:
entity_id:
description: Names(s) of the entities to turn on
example: 'fan.living_room'
speed:
description: Speed setting
example: 'high'
turn_off:
description: Turns fan off
fields:
entity_id:
description: Names(s) of the entities to turn off
example: 'fan.living_room'
oscillate:
description: Oscillates the fan
fields:
entity_id:
description: Name(s) of the entities to oscillate
example: 'fan.desk_fan'
oscillating:
description: Flag to turn on/off oscillation
example: True
toggle:
description: Toggle the fan on/off
fields:
entity_id:
description: Name(s) of the entities to toggle
exampl: 'fan.living_room'

View File

@ -14,6 +14,19 @@ URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static') STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
PANELS = {} PANELS = {}
MANIFEST_JSON = {
"background_color": "#FFFFFF",
"description": "Open-source home automation platform running on Python 3.",
"dir": "ltr",
"display": "standalone",
"icons": [],
"lang": "en-US",
"name": "Home Assistant",
"orientation": "any",
"short_name": "Assistant",
"start_url": "/",
"theme_color": "#03A9F4"
}
# To keep track we don't register a component twice (gives a warning) # To keep track we don't register a component twice (gives a warning)
_REGISTERED_COMPONENTS = set() _REGISTERED_COMPONENTS = set()
@ -94,9 +107,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
PANELS[url_path] = data PANELS[url_path] = data
def add_manifest_json_key(key, val):
"""Add a keyval to the manifest.json."""
MANIFEST_JSON[key] = val
def setup(hass, config): def setup(hass, config):
"""Setup serving the frontend.""" """Setup serving the frontend."""
hass.wsgi.register_view(BootstrapView) hass.wsgi.register_view(BootstrapView)
hass.wsgi.register_view(ManifestJSONView)
if hass.wsgi.development: if hass.wsgi.development:
sw_path = "home-assistant-polymer/build/service_worker.js" sw_path = "home-assistant-polymer/build/service_worker.js"
@ -126,6 +145,13 @@ def setup(hass, config):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
for size in (192, 384, 512, 1024):
MANIFEST_JSON['icons'].append({
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
"sizes": "{}x{}".format(size, size),
"type": "image/png"
})
return True return True
@ -199,3 +225,17 @@ class IndexView(HomeAssistantView):
panel_url=panel_url, panels=PANELS) panel_url=panel_url, panels=PANELS)
return self.Response(resp, mimetype='text/html') return self.Response(resp, mimetype='text/html')
class ManifestJSONView(HomeAssistantView):
"""View to return a manifest.json."""
requires_auth = False
url = "/manifest.json"
name = "manifestjson"
def get(self, request):
"""Return the manifest.json."""
import json
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
return self.Response(msg, mimetype="application/manifest+json")

View File

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Home Assistant</title> <title>Home Assistant</title>
<link rel='manifest' href='/static/manifest.json'> <link rel='manifest' href='/manifest.json'>
<link rel='icon' href='/static/icons/favicon.ico'> <link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180' <link rel='apple-touch-icon' sizes='180x180'
href='/static/icons/favicon-apple-180x180.png'> href='/static/icons/favicon-apple-180x180.png'>

View File

@ -1,9 +1,9 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = { FINGERPRINTS = {
"core.js": "457d5acd123e7dc38947c07984b3a5e8", "core.js": "1fd10c1fcdf56a61f60cf861d5a0368c",
"frontend.html": "829ee7cb591b8a63d7f22948a7aeb07a", "frontend.html": "88c97d278de3320278da6c32fe9e7d61",
"mdi.html": "b399b5d3798f5b68b0a4fbaae3432d48", "mdi.html": "710b84acc99b32514f52291aba9cd8e8",
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b", "panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169", "panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
"panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa", "panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 474366c536ec3e471da12d5f15b07b79fe9b07e2 Subproject commit 670ba0292bfca2b65aeca70804c0856b6cabf10e

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,30 +0,0 @@
{
"name": "Home Assistant",
"short_name": "Assistant",
"start_url": "/",
"display": "standalone",
"theme_color": "#03A9F4",
"background_color": "#FFFFFF",
"icons": [
{
"src": "/static/icons/favicon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/icons/favicon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/static/icons/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/static/icons/favicon-1024x1024.png",
"sizes": "1024x1024",
"type": "image/png"
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -54,6 +54,9 @@ def open_door(hass, entity_id=None):
def setup(hass, config): def setup(hass, config):
"""Track states and offer events for garage door.""" """Track states and offer events for garage door."""
_LOGGER.warning('This component has been deprecated in favour of the '
'"cover" component and will be removed in the future.'
' Please upgrade.')
component = EntityComponent( component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_GARAGE_DOORS) _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_GARAGE_DOORS)
component.setup(config) component.setup(config)

View File

@ -15,6 +15,10 @@ from homeassistant.components.garage_door import GarageDoorDevice
import homeassistant.components.rpi_gpio as rpi_gpio import homeassistant.components.rpi_gpio as rpi_gpio
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
RELAY_TIME = 'relay_time'
STATE_PULL_MODE = 'state_pull_mode'
DEFAULT_PULL_MODE = 'UP'
DEFAULT_RELAY_TIME = .2
DEPENDENCIES = ['rpi_gpio'] DEPENDENCIES = ['rpi_gpio']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,32 +36,42 @@ _DOORS_SCHEMA = vol.All(
PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = vol.Schema({
'platform': str, 'platform': str,
vol.Required('doors'): _DOORS_SCHEMA, vol.Required('doors'): _DOORS_SCHEMA,
vol.Optional(STATE_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string,
vol.Optional(RELAY_TIME, default=DEFAULT_RELAY_TIME): vol.Coerce(int),
}) })
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the garage door platform.""" """Setup the garage door platform."""
relay_time = config.get(RELAY_TIME)
state_pull_mode = config.get(STATE_PULL_MODE)
doors = [] doors = []
doors_conf = config.get('doors') doors_conf = config.get('doors')
for door in doors_conf: for door in doors_conf:
doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'], doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'],
door['state_pin'])) door['state_pin'],
state_pull_mode,
relay_time))
add_devices(doors) add_devices(doors)
class RPiGPIOGarageDoor(GarageDoorDevice): class RPiGPIOGarageDoor(GarageDoorDevice):
"""Representation of a Raspberry garage door.""" """Representation of a Raspberry garage door."""
def __init__(self, name, relay_pin, state_pin): # pylint: disable=too-many-arguments
def __init__(self, name, relay_pin, state_pin,
state_pull_mode, relay_time):
"""Initialize the garage door.""" """Initialize the garage door."""
self._name = name self._name = name
self._state = False self._state = False
self._relay_pin = relay_pin self._relay_pin = relay_pin
self._state_pin = state_pin self._state_pin = state_pin
self._state_pull_mode = state_pull_mode
self._relay_time = relay_time
rpi_gpio.setup_output(self._relay_pin) rpi_gpio.setup_output(self._relay_pin)
rpi_gpio.setup_input(self._state_pin, 'UP') rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
rpi_gpio.write_output(self._relay_pin, True) rpi_gpio.write_output(self._relay_pin, True)
@property @property
@ -82,7 +96,7 @@ class RPiGPIOGarageDoor(GarageDoorDevice):
def _trigger(self): def _trigger(self):
"""Trigger the door.""" """Trigger the door."""
rpi_gpio.write_output(self._relay_pin, False) rpi_gpio.write_output(self._relay_pin, False)
sleep(0.2) sleep(self._relay_time)
rpi_gpio.write_output(self._relay_pin, True) rpi_gpio.write_output(self._relay_pin, True)
def close_door(self): def close_door(self):

View File

@ -10,7 +10,7 @@ from homeassistant.components.garage_door import GarageDoorDevice
from homeassistant.components.wink import WinkDevice from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] REQUIREMENTS = ['python-wink==0.7.13', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -1,5 +1,5 @@
""" """
Component that sends data to aGraphite installation. Component that sends data to a Graphite installation.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/graphite/ https://home-assistant.io/components/graphite/
@ -10,26 +10,48 @@ import socket
import threading import threading
import time import time
from homeassistant.const import ( import voluptuous as vol
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED)
from homeassistant.helpers import state from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED)
from homeassistant.helpers import state
import homeassistant.helpers.config_validation as cv
DOMAIN = "graphite"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 2003
DEFAULT_PREFIX = 'ha'
DOMAIN = 'graphite'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config): def setup(hass, config):
"""Setup the Graphite feeder.""" """Setup the Graphite feeder."""
graphite_config = config.get('graphite', {}) conf = config[DOMAIN]
host = graphite_config.get('host', 'localhost') host = conf.get(CONF_HOST)
prefix = graphite_config.get('prefix', 'ha') prefix = conf.get(CONF_PREFIX)
port = conf.get(CONF_PORT)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try: try:
port = int(graphite_config.get('port', 2003)) sock.connect((host, port))
except ValueError: sock.shutdown(2)
_LOGGER.error('Invalid port specified') _LOGGER.debug('Connection to Graphite possible')
except socket.error:
_LOGGER.error('Not able to connect to Graphite')
return False return False
GraphiteFeeder(hass, host, port, prefix) GraphiteFeeder(hass, host, port, prefix)
return True return True

View File

@ -7,38 +7,49 @@ https://home-assistant.io/components/homematic/
import os import os
import time import time
import logging import logging
from datetime import timedelta
from functools import partial from functools import partial
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN,
CONF_USERNAME, CONF_PASSWORD) CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM,
ATTR_ENTITY_ID)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.util import Throttle
DOMAIN = 'homematic' DOMAIN = 'homematic'
REQUIREMENTS = ["pyhomematic==0.1.11"] REQUIREMENTS = ["pyhomematic==0.1.13"]
HOMEMATIC = None HOMEMATIC = None
HOMEMATIC_LINK_DELAY = 0.5 HOMEMATIC_LINK_DELAY = 0.5
MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300)
MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=60)
DISCOVER_SWITCHES = 'homematic.switch' DISCOVER_SWITCHES = 'homematic.switch'
DISCOVER_LIGHTS = 'homematic.light' DISCOVER_LIGHTS = 'homematic.light'
DISCOVER_SENSORS = 'homematic.sensor' DISCOVER_SENSORS = 'homematic.sensor'
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
DISCOVER_ROLLERSHUTTER = 'homematic.rollershutter' DISCOVER_COVER = 'homematic.cover'
DISCOVER_THERMOSTATS = 'homematic.thermostat' DISCOVER_CLIMATE = 'homematic.climate'
ATTR_DISCOVER_DEVICES = 'devices' ATTR_DISCOVER_DEVICES = 'devices'
ATTR_PARAM = 'param' ATTR_PARAM = 'param'
ATTR_CHANNEL = 'channel' ATTR_CHANNEL = 'channel'
ATTR_NAME = 'name' ATTR_NAME = 'name'
ATTR_ADDRESS = 'address' ATTR_ADDRESS = 'address'
ATTR_VALUE = 'value'
EVENT_KEYPRESS = 'homematic.keypress' EVENT_KEYPRESS = 'homematic.keypress'
EVENT_IMPULSE = 'homematic.impulse' EVENT_IMPULSE = 'homematic.impulse'
SERVICE_VIRTUALKEY = 'virtualkey' SERVICE_VIRTUALKEY = 'virtualkey'
SERVICE_SET_VALUE = 'set_value'
HM_DEVICE_TYPES = { HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'], DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'],
@ -46,15 +57,17 @@ HM_DEVICE_TYPES = {
DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2', DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2',
'RemoteMotion', 'ThermostatWall', 'AreaThermostat', 'RemoteMotion', 'ThermostatWall', 'AreaThermostat',
'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas', 'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas',
'LuxSensor'], 'LuxSensor', 'WeatherSensor', 'WeatherStation'],
DISCOVER_THERMOSTATS: ['Thermostat', 'ThermostatWall', 'MAXThermostat'], DISCOVER_CLIMATE: ['Thermostat', 'ThermostatWall', 'MAXThermostat'],
DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion', DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion',
'MotionV2', 'RemoteMotion'], 'MotionV2', 'RemoteMotion', 'WeatherSensor',
DISCOVER_ROLLERSHUTTER: ['Blind'] 'TiltSensor'],
DISCOVER_COVER: ['Blind']
} }
HM_IGNORE_DISCOVERY_NODE = [ HM_IGNORE_DISCOVERY_NODE = [
'ACTUAL_TEMPERATURE' 'ACTUAL_TEMPERATURE',
'ACTUAL_HUMIDITY'
] ]
HM_ATTRIBUTE_SUPPORT = { HM_ATTRIBUTE_SUPPORT = {
@ -66,7 +79,8 @@ HM_ATTRIBUTE_SUPPORT = {
'CONTROL_MODE': ['Mode', {0: 'Auto', 1: 'Manual', 2: 'Away', 3: 'Boost'}], 'CONTROL_MODE': ['Mode', {0: 'Auto', 1: 'Manual', 2: 'Away', 3: 'Boost'}],
'POWER': ['Power', {}], 'POWER': ['Power', {}],
'CURRENT': ['Current', {}], 'CURRENT': ['Current', {}],
'VOLTAGE': ['Voltage', {}] 'VOLTAGE': ['Voltage', {}],
'WORKING': ['Working', {0: 'No', 1: 'Yes'}],
} }
HM_PRESS_EVENTS = [ HM_PRESS_EVENTS = [
@ -96,51 +110,85 @@ CONF_REMOTE_PORT = 'remote_port'
CONF_RESOLVENAMES = 'resolvenames' CONF_RESOLVENAMES = 'resolvenames'
CONF_DELAY = 'delay' CONF_DELAY = 'delay'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_LOCAL_IP): vol.Coerce(str), DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_LOCAL_PORT, default=8943): vol.Required(CONF_PLATFORM): "homematic",
vol.All(vol.Coerce(int), vol.Required(ATTR_NAME): cv.string,
vol.Range(min=1, max=65535)), vol.Required(ATTR_ADDRESS): cv.string,
vol.Required(CONF_REMOTE_IP): vol.Coerce(str), vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
vol.Optional(CONF_REMOTE_PORT, default=2001): vol.Optional(ATTR_PARAM): cv.string,
vol.All(vol.Coerce(int),
vol.Range(min=1, max=65535)),
vol.Optional(CONF_RESOLVENAMES, default=False):
vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_USERNAME, default="Admin"): vol.Coerce(str),
vol.Optional(CONF_PASSWORD, default=""): vol.Coerce(str),
vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float)
}) })
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_LOCAL_IP): cv.string,
vol.Optional(CONF_LOCAL_PORT, default=8943): cv.port,
vol.Required(CONF_REMOTE_IP): cv.string,
vol.Optional(CONF_REMOTE_PORT, default=2001): cv.port,
vol.Optional(CONF_RESOLVENAMES, default=False):
vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_USERNAME, default="Admin"): cv.string,
vol.Optional(CONF_PASSWORD, default=""): cv.string,
vol.Optional(CONF_DELAY, default=0.5): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
vol.Required(ATTR_ADDRESS): vol.Coerce(str), vol.Required(ATTR_ADDRESS): cv.string,
vol.Required(ATTR_CHANNEL): vol.Coerce(int), vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): vol.Coerce(str) vol.Required(ATTR_PARAM): cv.string,
})
SCHEMA_SERVICE_SET_VALUE = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_VALUE): cv.match_all,
}) })
# pylint: disable=unused-argument def virtualkey(hass, address, channel, param):
"""Send virtual keypress to homematic controlller."""
data = {
ATTR_ADDRESS: address,
ATTR_CHANNEL: channel,
ATTR_PARAM: param,
}
hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data)
def set_value(hass, entity_id, value):
"""Change value of homematic system variable."""
data = {
ATTR_ENTITY_ID: entity_id,
ATTR_VALUE: value,
}
hass.services.call(DOMAIN, SERVICE_SET_VALUE, data)
# pylint: disable=unused-argument,too-many-locals
def setup(hass, config): def setup(hass, config):
"""Setup the Homematic component.""" """Setup the Homematic component."""
global HOMEMATIC, HOMEMATIC_LINK_DELAY global HOMEMATIC, HOMEMATIC_LINK_DELAY
from pyhomematic import HMConnection from pyhomematic import HMConnection
local_ip = config[DOMAIN][0].get(CONF_LOCAL_IP) component = EntityComponent(_LOGGER, DOMAIN, hass)
local_port = config[DOMAIN][0].get(CONF_LOCAL_PORT)
remote_ip = config[DOMAIN][0].get(CONF_REMOTE_IP) local_ip = config[DOMAIN].get(CONF_LOCAL_IP)
remote_port = config[DOMAIN][0].get(CONF_REMOTE_PORT) local_port = config[DOMAIN].get(CONF_LOCAL_PORT)
resolvenames = config[DOMAIN][0].get(CONF_RESOLVENAMES) remote_ip = config[DOMAIN].get(CONF_REMOTE_IP)
username = config[DOMAIN][0].get(CONF_USERNAME) remote_port = config[DOMAIN].get(CONF_REMOTE_PORT)
password = config[DOMAIN][0].get(CONF_PASSWORD) resolvenames = config[DOMAIN].get(CONF_RESOLVENAMES)
HOMEMATIC_LINK_DELAY = config[DOMAIN][0].get(CONF_DELAY) username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD)
HOMEMATIC_LINK_DELAY = config[DOMAIN].get(CONF_DELAY)
if remote_ip is None or local_ip is None: if remote_ip is None or local_ip is None:
_LOGGER.error("Missing remote CCU/Homegear or local address") _LOGGER.error("Missing remote CCU/Homegear or local address")
return False return False
# Create server thread # Create server thread
bound_system_callback = partial(system_callback_handler, hass, config) bound_system_callback = partial(_system_callback_handler, hass, config)
HOMEMATIC = HMConnection(local=local_ip, HOMEMATIC = HMConnection(local=local_ip,
localport=local_port, localport=local_port,
remote=remote_ip, remote=remote_ip,
@ -165,13 +213,45 @@ def setup(hass, config):
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY, hass.services.register(DOMAIN, SERVICE_VIRTUALKEY,
_hm_service_virtualkey, _hm_service_virtualkey,
descriptions[DOMAIN][SERVICE_VIRTUALKEY], descriptions[DOMAIN][SERVICE_VIRTUALKEY],
SCHEMA_SERVICE_VIRTUALKEY) schema=SCHEMA_SERVICE_VIRTUALKEY)
entities = []
##
# init HM variable
variables = HOMEMATIC.getAllSystemVariables()
if variables is not None:
for key, value in variables.items():
entities.append(HMVariable(key, value))
# add homematic entites
entities.append(HMHub())
component.add_entities(entities)
##
# register set_value service if exists variables
if not variables:
return True
def _service_handle_value(service):
"""Set value on homematic variable object."""
variable_list = component.extract_from_service(service)
value = service.data[ATTR_VALUE]
for hm_variable in variable_list:
hm_variable.hm_set(value)
hass.services.register(DOMAIN, SERVICE_SET_VALUE,
_service_handle_value,
descriptions[DOMAIN][SERVICE_SET_VALUE],
schema=SCHEMA_SERVICE_SET_VALUE)
return True return True
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def system_callback_handler(hass, config, src, *args): def _system_callback_handler(hass, config, src, *args):
"""Callback handler.""" """Callback handler."""
if src == 'newDevices': if src == 'newDevices':
_LOGGER.debug("newDevices with: %s", str(args)) _LOGGER.debug("newDevices with: %s", str(args))
@ -202,10 +282,10 @@ def system_callback_handler(hass, config, src, *args):
for component_name, discovery_type in ( for component_name, discovery_type in (
('switch', DISCOVER_SWITCHES), ('switch', DISCOVER_SWITCHES),
('light', DISCOVER_LIGHTS), ('light', DISCOVER_LIGHTS),
('rollershutter', DISCOVER_ROLLERSHUTTER), ('rollershutter', DISCOVER_COVER),
('binary_sensor', DISCOVER_BINARY_SENSORS), ('binary_sensor', DISCOVER_BINARY_SENSORS),
('sensor', DISCOVER_SENSORS), ('sensor', DISCOVER_SENSORS),
('thermostat', DISCOVER_THERMOSTATS)): ('climate', DISCOVER_CLIMATE)):
# Get all devices of a specific type # Get all devices of a specific type
found_devices = _get_devices(discovery_type, key_dict) found_devices = _get_devices(discovery_type, key_dict)
@ -220,8 +300,9 @@ def system_callback_handler(hass, config, src, *args):
def _get_devices(device_type, keys): def _get_devices(device_type, keys):
"""Get the Homematic devices.""" """Get the Homematic devices."""
# run
device_arr = [] device_arr = []
# pylint: disable=too-many-nested-blocks
for key in keys: for key in keys:
device = HOMEMATIC.devices[key] device = HOMEMATIC.devices[key]
class_name = device.__class__.__name__ class_name = device.__class__.__name__
@ -247,15 +328,22 @@ def _get_devices(device_type, keys):
name = _create_ha_name(name=device.NAME, name = _create_ha_name(name=device.NAME,
channel=channel, channel=channel,
param=param) param=param)
device_dict = dict(platform="homematic", device_dict = {
address=key, CONF_PLATFORM: "homematic",
name=name, ATTR_ADDRESS: key,
channel=channel) ATTR_NAME: name,
ATTR_CHANNEL: channel
}
if param is not None: if param is not None:
device_dict[ATTR_PARAM] = param device_dict.update({ATTR_PARAM: param})
# Add new device # Add new device
device_arr.append(device_dict) try:
DEVICE_SCHEMA(device_dict)
device_arr.append(device_dict)
except vol.MultipleInvalid as err:
_LOGGER.error("Invalid device config: %s",
str(err))
else: else:
_LOGGER.debug("Channel %i not in params", channel) _LOGGER.debug("Channel %i not in params", channel)
else: else:
@ -405,16 +493,99 @@ def _hm_service_virtualkey(call):
hmdevice.actionNodeData(param, 1, channel) hmdevice.actionNodeData(param, 1, channel)
class HMHub(Entity):
"""The Homematic hub. I.e. CCU2/HomeGear."""
def __init__(self):
"""Initialize Homematic hub."""
self._state = STATE_UNKNOWN
@property
def name(self):
"""Return the name of the device."""
return 'Homematic'
@property
def state(self):
"""Return the state of the entity."""
return self._state
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
return {}
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return "mdi:gradient"
@property
def available(self):
"""Return true if device is available."""
return True if HOMEMATIC is not None else False
@Throttle(MIN_TIME_BETWEEN_UPDATE_HUB)
def update(self):
"""Retrieve latest state."""
if HOMEMATIC is None:
return
state = HOMEMATIC.getServiceMessages()
self._state = STATE_UNKNOWN if state is None else len(state)
class HMVariable(Entity):
"""The Homematic system variable."""
def __init__(self, name, state):
"""Initialize Homematic hub."""
self._state = state
self._name = name
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the entity."""
return self._state
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return "mdi:code-string"
@Throttle(MIN_TIME_BETWEEN_UPDATE_VAR)
def update(self):
"""Retrieve latest state."""
if HOMEMATIC is None:
return
self._state = HOMEMATIC.getSystemVariable(self._name)
def hm_set(self, value):
"""Set variable on homematic controller."""
if HOMEMATIC is not None:
if isinstance(self._state, bool):
value = cv.boolean(value)
else:
value = float(value)
HOMEMATIC.setSystemVariable(self._name, value)
self._state = value
self.update_ha_state()
class HMDevice(Entity): class HMDevice(Entity):
"""The Homematic device base object.""" """The Homematic device base object."""
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__(self, config): def __init__(self, config):
"""Initialize a generic Homematic device.""" """Initialize a generic Homematic device."""
self._name = config.get(ATTR_NAME, None) self._name = config.get(ATTR_NAME)
self._address = config.get(ATTR_ADDRESS, None) self._address = config.get(ATTR_ADDRESS)
self._channel = config.get(ATTR_CHANNEL, 1) self._channel = config.get(ATTR_CHANNEL)
self._state = config.get(ATTR_PARAM, None) self._state = config.get(ATTR_PARAM)
self._data = {} self._data = {}
self._hmdevice = None self._hmdevice = None
self._connected = False self._connected = False

View File

@ -135,6 +135,9 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def setup(hass, config): def setup(hass, config):
"""Setup hvacs.""" """Setup hvacs."""
_LOGGER.warning('This component has been deprecated in favour of'
' the "climate" component and will be removed '
'in the future. Please upgrade.')
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config) component.setup(config)

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