commit
7b2f0e709b
19
.coveragerc
19
.coveragerc
|
@ -101,18 +101,29 @@ omit =
|
|||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/ffmpeg.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/generic.py
|
||||
homeassistant/components/camera/mjpeg.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/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.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/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
|
@ -173,8 +184,10 @@ omit =
|
|||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/group.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
|
@ -202,13 +215,17 @@ omit =
|
|||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/lastfm.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/nzbget.py
|
||||
homeassistant/components/sensor/ohmconnect.py
|
||||
|
|
|
@ -19,6 +19,7 @@ import homeassistant.config as conf_util
|
|||
import homeassistant.core as core
|
||||
import homeassistant.loader as loader
|
||||
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.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
|
@ -103,7 +104,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
|||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
_log_exception(ex, domain, config)
|
||||
log_exception(ex, domain, config)
|
||||
return False
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
|
@ -113,7 +114,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
|||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
_log_exception(ex, domain, p_config)
|
||||
log_exception(ex, domain, p_config)
|
||||
return False
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
|
@ -134,8 +135,8 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
|||
try:
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.MultipleInvalid as ex:
|
||||
_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated)
|
||||
log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated)
|
||||
return False
|
||||
|
||||
platforms.append(p_validated)
|
||||
|
@ -239,7 +240,7 @@ def from_config_dict(config: Dict[str, Any],
|
|||
try:
|
||||
conf_util.process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
_log_exception(ex, 'homeassistant', core_config)
|
||||
log_exception(ex, 'homeassistant', core_config)
|
||||
return None
|
||||
|
||||
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)
|
||||
except HomeAssistantError:
|
||||
return None
|
||||
finally:
|
||||
clear_secret_cache()
|
||||
|
||||
return from_config_dict(config_dict, hass, enable_log=False,
|
||||
skip_pip=skip_pip)
|
||||
|
@ -371,7 +374,7 @@ def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
|||
loader.prepare(hass)
|
||||
|
||||
|
||||
def _log_exception(ex, domain, config):
|
||||
def log_exception(ex, domain, config):
|
||||
"""Generate log exception for config validation."""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
|
|
|
@ -80,7 +80,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
if not self._validate_code(code, 'disarming home'):
|
||||
return
|
||||
from pyalarmdotcom.pyalarmdotcom import Alarmdotcom
|
||||
# Open another session to alarm.com to fire off the command
|
||||
|
|
|
@ -10,5 +10,5 @@ import homeassistant.components.alarm_control_panel.manual as manual
|
|||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Demo alarm control panel platform."""
|
||||
add_devices([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10),
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False),
|
||||
])
|
||||
|
|
|
@ -7,28 +7,46 @@ https://home-assistant.io/components/alarm_control_panel.manual/
|
|||
import datetime
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_PENDING_TIME = 60
|
||||
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):
|
||||
"""Setup the manual alarm platform."""
|
||||
add_devices([ManualAlarm(
|
||||
hass,
|
||||
config.get('name', DEFAULT_ALARM_NAME),
|
||||
config.get('code'),
|
||||
config.get('pending_time', DEFAULT_PENDING_TIME),
|
||||
config.get('trigger_time', DEFAULT_TRIGGER_TIME),
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_PENDING_TIME, DEFAULT_PENDING_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 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."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
|
@ -51,6 +71,8 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
self._code = str(code) if code else None
|
||||
self._pending_time = datetime.timedelta(seconds=pending_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
|
||||
|
||||
@property
|
||||
|
@ -77,7 +99,10 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
return STATE_ALARM_PENDING
|
||||
elif (self._state_ts + self._pending_time +
|
||||
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
|
||||
|
||||
|
@ -125,6 +150,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command. No code needed."""
|
||||
self._pre_trigger_state = self._state
|
||||
self._state = STATE_ALARM_TRIGGERED
|
||||
self._state_ts = dt_util.utcnow()
|
||||
self.update_ha_state()
|
||||
|
|
|
@ -11,17 +11,17 @@ from homeassistant.const import HTTP_BAD_REQUEST
|
|||
from homeassistant.helpers import template, script
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_ENDPOINT = '/api/alexa'
|
||||
|
||||
CONF_INTENTS = 'intents'
|
||||
CONF_CARD = 'card'
|
||||
CONF_SPEECH = 'speech'
|
||||
CONF_ACTION = 'action'
|
||||
CONF_CARD = 'card'
|
||||
CONF_INTENTS = 'intents'
|
||||
CONF_SPEECH = 'speech'
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
|
@ -6,27 +6,34 @@ https://home-assistant.io/components/arduino/
|
|||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
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']
|
||||
BOARD = None
|
||||
|
||||
_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):
|
||||
"""Setup the Arduino component."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: ['port']},
|
||||
_LOGGER):
|
||||
return False
|
||||
|
||||
import serial
|
||||
global BOARD
|
||||
try:
|
||||
BOARD = ArduinoBoard(config[DOMAIN]['port'])
|
||||
BOARD = ArduinoBoard(config[DOMAIN][CONF_PORT])
|
||||
except (serial.serialutil.SerialException, FileNotFoundError):
|
||||
_LOGGER.exception("Your port is not accessible.")
|
||||
return False
|
||||
|
|
|
@ -6,6 +6,8 @@ https://home-assistant.io/components/binary_sensor/
|
|||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF)
|
||||
|
@ -33,6 +35,8 @@ SENSOR_CLASSES = [
|
|||
'vibration', # On means vibration detected, Off means no vibration
|
||||
]
|
||||
|
||||
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES))
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
|
|
|
@ -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']
|
|
@ -4,27 +4,41 @@ Support for EnOcean binary sensors.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
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.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):
|
||||
"""Setup the Binary Sensor platform fo EnOcean."""
|
||||
dev_id = config.get(CONF_ID, None)
|
||||
devname = config.get(CONF_NAME, "EnOcean binary sensor")
|
||||
add_devices([EnOceanBinarySensor(dev_id, devname)])
|
||||
dev_id = config.get(CONF_ID)
|
||||
devname = config.get(CONF_NAME)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
|
||||
add_devices([EnOceanBinarySensor(dev_id, devname, sensor_class)])
|
||||
|
||||
|
||||
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
"""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."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.stype = "listener"
|
||||
|
@ -32,12 +46,18 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
|||
self.which = -1
|
||||
self.onoff = -1
|
||||
self.devname = devname
|
||||
self._sensor_class = sensor_class
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The default name for the binary sensor."""
|
||||
return self.devname
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
|
||||
def value_changed(self, value, value2):
|
||||
"""Fire an event with the data that have changed.
|
||||
|
||||
|
|
|
@ -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"
|
|
@ -20,7 +20,9 @@ SENSOR_TYPES_CLASS = {
|
|||
"SmokeV2": "smoke",
|
||||
"Motion": "motion",
|
||||
"MotionV2": "motion",
|
||||
"RemoteMotion": None
|
||||
"RemoteMotion": None,
|
||||
"WeatherSensor": None,
|
||||
"TiltSensor": None,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
pres.S_MOTION: [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({
|
||||
pres.S_SPRINKLER: [set_req.V_TRIPPED],
|
||||
pres.S_WATER_LEAK: [set_req.V_TRIPPED],
|
||||
|
@ -66,7 +66,7 @@ class MySensorsBinarySensor(
|
|||
pres.S_MOTION: 'motion',
|
||||
pres.S_SMOKE: 'smoke',
|
||||
}
|
||||
if float(self.gateway.version) >= 1.5:
|
||||
if float(self.gateway.protocol_version) >= 1.5:
|
||||
class_map.update({
|
||||
pres.S_SPRINKLER: 'sprinkler',
|
||||
pres.S_WATER_LEAK: 'leak',
|
||||
|
|
|
@ -6,30 +6,42 @@ https://home-assistant.io/components/binary_sensor.rest/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
SENSOR_CLASSES)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
||||
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
|
||||
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__)
|
||||
|
||||
DEFAULT_NAME = 'REST Binary Sensor'
|
||||
DEFAULT_METHOD = 'GET'
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the REST binary sensor."""
|
||||
resource = config.get('resource', None)
|
||||
method = config.get('method', DEFAULT_METHOD)
|
||||
payload = config.get('payload', None)
|
||||
name = config.get(CONF_NAME)
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
method = config.get(CONF_METHOD)
|
||||
payload = config.get(CONF_PAYLOAD)
|
||||
verify_ssl = config.get('verify_ssl', True)
|
||||
|
||||
sensor_class = config.get('sensor_class')
|
||||
if sensor_class not in SENSOR_CLASSES:
|
||||
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
|
||||
sensor_class = None
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
rest = RestData(method, resource, payload, verify_ssl)
|
||||
rest.update()
|
||||
|
@ -39,11 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return False
|
||||
|
||||
add_devices([RestBinarySensor(
|
||||
hass,
|
||||
rest,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
sensor_class,
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
hass, rest, name, sensor_class, value_template)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
|
|
|
@ -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'
|
|
@ -5,55 +5,47 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/binary_sensor.template/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
ENTITY_ID_FORMAT,
|
||||
SENSOR_CLASSES)
|
||||
from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE,
|
||||
ATTR_ENTITY_ID, MATCH_ALL)
|
||||
PLATFORM_SCHEMA,
|
||||
SENSOR_CLASSES_SCHEMA)
|
||||
|
||||
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.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.util import slugify
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup template binary 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():
|
||||
|
||||
if device != slugify(device):
|
||||
_LOGGER.error('Found invalid key for binary_sensor.template: %s. '
|
||||
'Use %s instead', device, slugify(device))
|
||||
continue
|
||||
|
||||
if not isinstance(device_config, dict):
|
||||
_LOGGER.error('Missing configuration data for binary_sensor %s',
|
||||
device)
|
||||
continue
|
||||
|
||||
value_template = device_config[CONF_VALUE_TEMPLATE]
|
||||
entity_ids = device_config[ATTR_ENTITY_ID]
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||
sensor_class = device_config.get('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)
|
||||
sensor_class = device_config.get(CONF_SENSOR_CLASS)
|
||||
|
||||
sensors.append(
|
||||
BinarySensorTemplate(
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
|||
from homeassistant.helpers.entity import Entity
|
||||
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
|
||||
SENSOR_TYPES = {
|
||||
|
|
|
@ -11,15 +11,15 @@ import requests
|
|||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ["bloomsky"]
|
||||
DEPENDENCIES = ['bloomsky']
|
||||
|
||||
|
||||
# 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."""
|
||||
bloomsky = get_component('bloomsky')
|
||||
for device in bloomsky.BLOOMSKY.devices.values():
|
||||
add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
|
||||
add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)])
|
||||
|
||||
|
||||
class BloomSkyCamera(Camera):
|
||||
|
@ -28,8 +28,8 @@ class BloomSkyCamera(Camera):
|
|||
def __init__(self, bs, device):
|
||||
"""Setup for access to the BloomSky camera images."""
|
||||
super(BloomSkyCamera, self).__init__()
|
||||
self._name = device["DeviceName"]
|
||||
self._id = device["DeviceID"]
|
||||
self._name = device['DeviceName']
|
||||
self._id = device['DeviceID']
|
||||
self._bloomsky = bs
|
||||
self._url = ""
|
||||
self._last_url = ""
|
||||
|
@ -42,7 +42,7 @@ class BloomSkyCamera(Camera):
|
|||
def camera_image(self):
|
||||
"""Update the camera's image if it has changed."""
|
||||
try:
|
||||
self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"]
|
||||
self._url = self._bloomsky.devices[self._id]['Data']['ImageURL']
|
||||
self._bloomsky.refresh_devices()
|
||||
# If the URL hasn't changed then the image hasn't changed.
|
||||
if self._url != self._last_url:
|
||||
|
|
|
@ -9,31 +9,33 @@ from contextlib import closing
|
|||
|
||||
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
|
||||
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_FFMPEG_BIN = 'ffmpeg_bin'
|
||||
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "ffmpeg",
|
||||
vol.Optional(CONF_NAME, default="FFmpeg"): cv.string,
|
||||
DEFAULT_BINARY = 'ffmpeg'
|
||||
DEFAULT_NAME = 'FFmpeg'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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_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_callback, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
add_devices_callback([FFmpegCamera(config)])
|
||||
add_devices([FFmpegCamera(config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
|
|
|
@ -7,21 +7,33 @@ https://home-assistant.io/components/camera.foscam/
|
|||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_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
|
||||
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."""
|
||||
if not validate_config({DOMAIN: config},
|
||||
{DOMAIN: ['username', 'password', 'ip']}, _LOGGER):
|
||||
return None
|
||||
|
||||
add_devices_callback([FoscamCamera(config)])
|
||||
add_devices([FoscamCamera(config)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
@ -32,16 +44,16 @@ class FoscamCamera(Camera):
|
|||
"""Initialize a Foscam camera."""
|
||||
super(FoscamCamera, self).__init__()
|
||||
|
||||
ip_address = device_info.get('ip')
|
||||
port = device_info.get('port', 88)
|
||||
ip_address = device_info.get(CONF_IP)
|
||||
port = device_info.get(CONF_PORT)
|
||||
|
||||
self._base_url = 'http://' + ip_address + ':' + str(port) + '/'
|
||||
self._username = device_info.get('username')
|
||||
self._password = device_info.get('password')
|
||||
self._base_url = 'http://{}:{}/'.format(ip_address, port)
|
||||
self._username = device_info.get(CONF_USERNAME)
|
||||
self._password = device_info.get(CONF_PASSWORD)
|
||||
self._snap_picture_url = self._base_url \
|
||||
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \
|
||||
+ 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',
|
||||
self._name, self._snap_picture_url)
|
||||
|
|
|
@ -7,22 +7,38 @@ https://home-assistant.io/components/camera.generic/
|
|||
import logging
|
||||
|
||||
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.helpers import validate_config
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
|
||||
_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
|
||||
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."""
|
||||
if not validate_config({DOMAIN: config}, {DOMAIN: ['still_image_url']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
add_devices_callback([GenericCamera(config)])
|
||||
add_devices([GenericCamera(config)])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
@ -32,30 +48,47 @@ class GenericCamera(Camera):
|
|||
def __init__(self, device_info):
|
||||
"""Initialize a generic camera."""
|
||||
super().__init__()
|
||||
self._name = device_info.get('name', 'Generic Camera')
|
||||
self._username = device_info.get('username')
|
||||
self._password = device_info.get('password')
|
||||
self._still_image_url = device_info['still_image_url']
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
|
||||
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
|
||||
|
||||
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):
|
||||
"""Return a still image response from the camera."""
|
||||
if self._username and self._password:
|
||||
try:
|
||||
response = requests.get(
|
||||
self._still_image_url,
|
||||
auth=HTTPBasicAuth(self._username, self._password),
|
||||
timeout=10)
|
||||
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
|
||||
try:
|
||||
url = template.render(self.hass, self._still_image_url)
|
||||
except TemplateError as err:
|
||||
_LOGGER.error('Error parsing template %s: %s',
|
||||
self._still_image_url, err)
|
||||
return self._last_image
|
||||
|
||||
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
|
||||
def name(self):
|
||||
|
|
|
@ -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 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__)
|
||||
|
||||
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):
|
||||
"""Setup the Camera."""
|
||||
# check for missing required configuration variable
|
||||
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")
|
||||
}
|
||||
)
|
||||
file_path = config[CONF_FILE_PATH]
|
||||
|
||||
# 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")
|
||||
return False
|
||||
|
||||
add_devices([
|
||||
LocalFile(setup_config)
|
||||
])
|
||||
add_devices([LocalFile(config[CONF_NAME], file_path)])
|
||||
|
||||
|
||||
class LocalFile(Camera):
|
||||
"""Local camera."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
def __init__(self, name, file_path):
|
||||
"""Initialize Local File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._name = device_info["name"]
|
||||
self._config = device_info
|
||||
self._name = name
|
||||
self._file_path = file_path
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
with open(self._config["file_path"], 'rb') as file:
|
||||
with open(self._file_path, 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
@property
|
||||
|
|
|
@ -8,24 +8,36 @@ import logging
|
|||
from contextlib import closing
|
||||
|
||||
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.helpers import validate_config
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_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
|
||||
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."""
|
||||
if not validate_config({DOMAIN: config}, {DOMAIN: ['mjpeg_url']},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
add_devices_callback([MjpegCamera(config)])
|
||||
add_devices([MjpegCamera(config)])
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
|
@ -47,17 +59,21 @@ class MjpegCamera(Camera):
|
|||
def __init__(self, device_info):
|
||||
"""Initialize a MJPEG camera."""
|
||||
super().__init__()
|
||||
self._name = device_info.get('name', 'Mjpeg Camera')
|
||||
self._username = device_info.get('username')
|
||||
self._password = device_info.get('password')
|
||||
self._mjpeg_url = device_info['mjpeg_url']
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._authentication = device_info.get(CONF_AUTHENTICATION)
|
||||
self._username = device_info.get(CONF_USERNAME)
|
||||
self._password = device_info.get(CONF_PASSWORD)
|
||||
self._mjpeg_url = device_info[CONF_MJPEG_URL]
|
||||
|
||||
def camera_stream(self):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
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,
|
||||
auth=HTTPBasicAuth(self._username,
|
||||
self._password),
|
||||
auth=auth,
|
||||
stream=True, timeout=10)
|
||||
else:
|
||||
return requests.get(self._mjpeg_url, stream=True, timeout=10)
|
||||
|
|
|
@ -6,34 +6,43 @@ https://home-assistant.io/components/camera.netatmo/
|
|||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
DEPENDENCIES = ['netatmo']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_HOME = 'home'
|
||||
ATTR_CAMERAS = 'cameras'
|
||||
CONF_CAMERAS = 'cameras'
|
||||
|
||||
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
|
||||
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."""
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME, None)
|
||||
home = config.get(CONF_HOME)
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if ATTR_CAMERAS in config:
|
||||
if camera_name not in config[ATTR_CAMERAS]:
|
||||
if CONF_CAMERAS in config:
|
||||
if camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
add_devices_callback([WelcomeCamera(data, camera_name, home)])
|
||||
add_devices([WelcomeCamera(data, camera_name, home)])
|
||||
|
||||
|
||||
class WelcomeCamera(Camera):
|
||||
|
|
|
@ -9,41 +9,77 @@ import subprocess
|
|||
import logging
|
||||
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__)
|
||||
|
||||
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):
|
||||
"""Setup the Raspberry Camera."""
|
||||
if shutil.which("raspistill") is None:
|
||||
_LOGGER.error("Error: raspistill not found")
|
||||
_LOGGER.error("'raspistill' was not found")
|
||||
return False
|
||||
|
||||
setup_config = (
|
||||
{
|
||||
"name": config.get("name", "Raspberry Pi Camera"),
|
||||
"image_width": int(config.get("image_width", "640")),
|
||||
"image_height": int(config.get("image_height", "480")),
|
||||
"image_quality": int(config.get("image_quality", "7")),
|
||||
"image_rotation": int(config.get("image_rotation", "0")),
|
||||
"timelapse": int(config.get("timelapse", "2000")),
|
||||
"horizontal_flip": int(config.get("horizontal_flip", "0")),
|
||||
"vertical_flip": int(config.get("vertical_flip", "0")),
|
||||
"file_path": config.get("file_path",
|
||||
os.path.join(os.path.dirname(__file__),
|
||||
'image.jpg'))
|
||||
CONF_NAME: config.get(CONF_NAME),
|
||||
CONF_IMAGE_WIDTH: config.get(CONF_IMAGE_WIDTH),
|
||||
CONF_IMAGE_HEIGHT: config.get(CONF_IMAGE_HEIGHT),
|
||||
CONF_IMAGE_QUALITY: config.get(CONF_IMAGE_QUALITY),
|
||||
CONF_IMAGE_ROTATION: config.get(CONF_IMAGE_ROTATION),
|
||||
CONF_TIMELAPSE: config.get(CONF_TIMELAPSE),
|
||||
CONF_HORIZONTAL_FLIP: config.get(CONF_HORIZONTAL_FLIP),
|
||||
CONF_VERTICAL_FLIP: config.get(CONF_VERTICAL_FLIP),
|
||||
CONF_FILE_PATH: config.get(CONF_FILE_PATH,
|
||||
os.path.join(os.path.dirname(__file__),
|
||||
'image.jpg'))
|
||||
}
|
||||
)
|
||||
|
||||
# check filepath given is writable
|
||||
if not os.access(setup_config["file_path"], os.W_OK):
|
||||
_LOGGER.error("Error: file path is not writable")
|
||||
if not os.access(setup_config[CONF_FILE_PATH], os.W_OK):
|
||||
_LOGGER.error("File path is not writable")
|
||||
return False
|
||||
|
||||
add_devices([
|
||||
RaspberryCamera(setup_config)
|
||||
])
|
||||
add_devices([RaspberryCamera(setup_config)])
|
||||
|
||||
|
||||
class RaspberryCamera(Camera):
|
||||
|
@ -53,26 +89,26 @@ class RaspberryCamera(Camera):
|
|||
"""Initialize Raspberry Pi camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._name = device_info["name"]
|
||||
self._name = device_info[CONF_NAME]
|
||||
self._config = device_info
|
||||
|
||||
# kill if there's raspistill instance
|
||||
# Kill if there's raspistill instance
|
||||
subprocess.Popen(['killall', 'raspistill'],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT)
|
||||
|
||||
cmd_args = [
|
||||
'raspistill', '--nopreview', '-o', str(device_info["file_path"]),
|
||||
'-t', '0', '-w', str(device_info["image_width"]),
|
||||
'-h', str(device_info["image_height"]),
|
||||
'-tl', str(device_info["timelapse"]),
|
||||
'-q', str(device_info["image_quality"]),
|
||||
'-rot', str(device_info["image_rotation"])
|
||||
'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH],
|
||||
'-t', '0', '-w', str(device_info[CONF_IMAGE_WIDTH]),
|
||||
'-h', str(device_info[CONF_IMAGE_HEIGHT]),
|
||||
'-tl', str(device_info[CONF_TIMELAPSE]),
|
||||
'-q', str(device_info[CONF_IMAGE_QUALITY]),
|
||||
'-rot', str(device_info[CONF_IMAGE_ROTATION])
|
||||
]
|
||||
if device_info["horizontal_flip"]:
|
||||
if device_info[CONF_HORIZONTAL_FLIP]:
|
||||
cmd_args.append("-hf")
|
||||
|
||||
if device_info["vertical_flip"]:
|
||||
if device_info[CONF_VERTICAL_FLIP]:
|
||||
cmd_args.append("-vf")
|
||||
|
||||
subprocess.Popen(cmd_args,
|
||||
|
@ -81,7 +117,7 @@ class RaspberryCamera(Camera):
|
|||
|
||||
def camera_image(self):
|
||||
"""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()
|
||||
|
||||
@property
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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})
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -11,26 +11,26 @@ import logging
|
|||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
|
||||
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 = {}
|
||||
_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
|
||||
|
|
|
@ -15,20 +15,20 @@ from homeassistant.const import (
|
|||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
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({
|
||||
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):
|
||||
"""Register the process service."""
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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})
|
|
@ -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)
|
|
@ -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")
|
|
@ -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()
|
|
@ -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)
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
|
@ -11,17 +11,16 @@ import homeassistant.core as ha
|
|||
import homeassistant.loader as loader
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
|
||||
DOMAIN = "demo"
|
||||
|
||||
DEPENDENCIES = ['conversation', 'introduction', 'zone']
|
||||
DOMAIN = 'demo'
|
||||
|
||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'alarm_control_panel',
|
||||
'binary_sensor',
|
||||
'camera',
|
||||
'climate',
|
||||
'device_tracker',
|
||||
'garage_door',
|
||||
'hvac',
|
||||
'light',
|
||||
'lock',
|
||||
'media_player',
|
||||
|
@ -29,7 +28,6 @@ COMPONENTS_WITH_DEMO_PLATFORM = [
|
|||
'rollershutter',
|
||||
'sensor',
|
||||
'switch',
|
||||
'thermostat',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -258,7 +258,7 @@ class Device(Entity):
|
|||
_state = STATE_NOT_HOME
|
||||
|
||||
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."""
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
|
@ -280,7 +280,11 @@ class Device(Entity):
|
|||
self.config_name = name
|
||||
|
||||
# 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
|
||||
|
||||
@property
|
||||
|
@ -382,6 +386,7 @@ def load_config(path, hass, consider_home, home_range):
|
|||
Device(hass, consider_home, home_range, device.get('track', False),
|
||||
str(dev_id).lower(), str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get('gravatar'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
for dev_id, device in load_yaml_config_file(path).items()]
|
||||
except HomeAssistantError:
|
||||
|
@ -425,3 +430,10 @@ def update_config(path, dev_id, device):
|
|||
(CONF_AWAY_HIDE,
|
||||
'yes' if device.away_hide else 'no')):
|
||||
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())
|
||||
|
|
|
@ -1,126 +1,129 @@
|
|||
"""
|
||||
Support for Actiontec MI424WR (Verizon FIOS) routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.actiontec/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import telnetlib
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_LEASES_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' +
|
||||
r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' +
|
||||
r'\ssec')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an Actiontec scanner."""
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||
_LOGGER):
|
||||
return None
|
||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||
|
||||
|
||||
class ActiontecDeviceScanner(object):
|
||||
"""This class queries a an actiontec router for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.lock = threading.Lock()
|
||||
self.last_results = []
|
||||
data = self.get_actiontec_data()
|
||||
self.success_init = data is not None
|
||||
_LOGGER.info("actiontec scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client.mac for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
if client.mac == device:
|
||||
return client.ip
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info("Scanning")
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
now = dt_util.now()
|
||||
actiontec_data = self.get_actiontec_data()
|
||||
if not actiontec_data:
|
||||
return False
|
||||
self.last_results = [Device(data['mac'], name, now)
|
||||
for name, data in actiontec_data.items()
|
||||
if data['timevalid'] > -60]
|
||||
_LOGGER.info("actiontec scan successful")
|
||||
return True
|
||||
|
||||
def get_actiontec_data(self):
|
||||
"""Retrieve data from Actiontec MI424WR and return parsed result."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'Username: ')
|
||||
telnet.write((self.username + '\n').encode('ascii'))
|
||||
telnet.read_until(b'Password: ')
|
||||
telnet.write((self.password + '\n').encode('ascii'))
|
||||
prompt = telnet.read_until(
|
||||
b'Wireless Broadband Router> ').split(b'\n')[-1]
|
||||
telnet.write('firewall mac_cache_dump\n'.encode('ascii'))
|
||||
telnet.write('\n'.encode('ascii'))
|
||||
telnet.read_until(prompt)
|
||||
leases_result = telnet.read_until(prompt).split(b'\n')[1:-1]
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
return
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router," +
|
||||
" is telnet enabled?")
|
||||
return None
|
||||
|
||||
devices = {}
|
||||
for lease in leases_result:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
if match is not None:
|
||||
devices[match.group('ip')] = {
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
'timevalid': int(match.group('timevalid'))
|
||||
}
|
||||
return devices
|
||||
"""
|
||||
Support for Actiontec MI424WR (Verizon FIOS) routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.actiontec/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import telnetlib
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA)
|
||||
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)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_LEASES_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' +
|
||||
r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' +
|
||||
r'\ssec')
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return an Actiontec scanner."""
|
||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||
|
||||
|
||||
class ActiontecDeviceScanner(object):
|
||||
"""This class queries a an actiontec router for connected devices."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
self.lock = threading.Lock()
|
||||
self.last_results = []
|
||||
data = self.get_actiontec_data()
|
||||
self.success_init = data is not None
|
||||
_LOGGER.info("actiontec scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
return [client.mac for client in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
if not self.last_results:
|
||||
return None
|
||||
for client in self.last_results:
|
||||
if client.mac == device:
|
||||
return client.ip
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the router is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info("Scanning")
|
||||
if not self.success_init:
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
now = dt_util.now()
|
||||
actiontec_data = self.get_actiontec_data()
|
||||
if not actiontec_data:
|
||||
return False
|
||||
self.last_results = [Device(data['mac'], name, now)
|
||||
for name, data in actiontec_data.items()
|
||||
if data['timevalid'] > -60]
|
||||
_LOGGER.info("actiontec scan successful")
|
||||
return True
|
||||
|
||||
def get_actiontec_data(self):
|
||||
"""Retrieve data from Actiontec MI424WR and return parsed result."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host)
|
||||
telnet.read_until(b'Username: ')
|
||||
telnet.write((self.username + '\n').encode('ascii'))
|
||||
telnet.read_until(b'Password: ')
|
||||
telnet.write((self.password + '\n').encode('ascii'))
|
||||
prompt = telnet.read_until(
|
||||
b'Wireless Broadband Router> ').split(b'\n')[-1]
|
||||
telnet.write('firewall mac_cache_dump\n'.encode('ascii'))
|
||||
telnet.write('\n'.encode('ascii'))
|
||||
telnet.read_until(prompt)
|
||||
leases_result = telnet.read_until(prompt).split(b'\n')[1:-1]
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
except EOFError:
|
||||
_LOGGER.exception("Unexpected response from router")
|
||||
return
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.exception("Connection refused by router," +
|
||||
" is telnet enabled?")
|
||||
return None
|
||||
|
||||
devices = {}
|
||||
for lease in leases_result:
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
if match is not None:
|
||||
devices[match.group('ip')] = {
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
'timevalid': int(match.group('timevalid'))
|
||||
}
|
||||
return devices
|
||||
|
|
|
@ -12,14 +12,36 @@ import threading
|
|||
from collections import namedtuple
|
||||
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.helpers import validate_config
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
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__)
|
||||
REQUIREMENTS = ['pexpect==4.0.1']
|
||||
|
||||
|
@ -57,16 +79,6 @@ _IP_NEIGH_REGEX = re.compile(
|
|||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""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])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
@ -83,11 +95,11 @@ class AsusWrtDeviceScanner(object):
|
|||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = str(config[CONF_USERNAME])
|
||||
self.password = str(config.get(CONF_PASSWORD, ''))
|
||||
self.ssh_key = str(config.get('ssh_key', config.get('pub_key', '')))
|
||||
self.protocol = config.get('protocol')
|
||||
self.mode = config.get('mode')
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config.get(CONF_PASSWORD, '')
|
||||
self.ssh_key = config.get('ssh_key', config.get('pub_key', ''))
|
||||
self.protocol = config[CONF_PROTOCOL]
|
||||
self.mode = config[CONF_MODE]
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
|
|
|
@ -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
|
|
@ -29,6 +29,9 @@ LOCK = threading.Lock()
|
|||
|
||||
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
|
||||
|
||||
VALIDATE_LOCATION = 'location'
|
||||
VALIDATE_TRANSITION = 'transition'
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup an OwnTracks tracker."""
|
||||
|
@ -47,16 +50,18 @@ def setup_scanner(hass, config, see):
|
|||
'because of missing or malformatted data: %s',
|
||||
data_type, data)
|
||||
return None
|
||||
if data_type == VALIDATE_TRANSITION:
|
||||
return data
|
||||
if max_gps_accuracy is not None and \
|
||||
convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
|
||||
_LOGGER.debug('Skipping %s update because expected GPS '
|
||||
'accuracy %s is not met: %s',
|
||||
data_type, max_gps_accuracy, data)
|
||||
_LOGGER.warning('Ignoring %s update because expected GPS '
|
||||
'accuracy %s is not met: %s',
|
||||
data_type, max_gps_accuracy, payload)
|
||||
return None
|
||||
if convert(data.get('acc'), float, 1.0) == 0.0:
|
||||
_LOGGER.debug('Skipping %s update because GPS accuracy'
|
||||
'is zero',
|
||||
data_type)
|
||||
_LOGGER.warning('Ignoring %s update because GPS accuracy'
|
||||
'is zero: %s',
|
||||
data_type, payload)
|
||||
return None
|
||||
|
||||
return data
|
||||
|
@ -65,7 +70,7 @@ def setup_scanner(hass, config, see):
|
|||
"""MQTT message received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||
data = validate_payload(payload, 'location')
|
||||
data = validate_payload(payload, VALIDATE_LOCATION)
|
||||
if not data:
|
||||
return
|
||||
|
||||
|
@ -86,7 +91,7 @@ def setup_scanner(hass, config, see):
|
|||
"""MQTT event (geofences) received."""
|
||||
# Docs on available data:
|
||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||
data = validate_payload(payload, 'transition')
|
||||
data = validate_payload(payload, VALIDATE_TRANSITION)
|
||||
if not data:
|
||||
return
|
||||
|
||||
|
@ -143,14 +148,24 @@ def setup_scanner(hass, config, see):
|
|||
else:
|
||||
_LOGGER.info("Exit to GPS")
|
||||
# Check for GPS accuracy
|
||||
if not ('acc' in data and
|
||||
max_gps_accuracy is not None and
|
||||
data['acc'] > max_gps_accuracy):
|
||||
|
||||
valid_gps = True
|
||||
if 'acc' in data:
|
||||
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_beacons(dev_id, kwargs)
|
||||
else:
|
||||
_LOGGER.info("Inaccurate GPS reported")
|
||||
|
||||
beacons = MOBILE_BEACONS_ACTIVE[dev_id]
|
||||
if location in beacons:
|
||||
|
|
|
@ -313,19 +313,23 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
|
|||
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
url = 'http://{}/{}/userRpm/WlanStationRpm.htm' \
|
||||
.format(self.host, self.token)
|
||||
referer = 'http://{}'.format(self.host)
|
||||
cookie = 'Authorization=Basic {}'.format(self.credentials)
|
||||
mac_results = []
|
||||
|
||||
page = requests.get(url, headers={
|
||||
'cookie': cookie,
|
||||
'referer': referer
|
||||
})
|
||||
result = self.parse_macs.findall(page.text)
|
||||
# Check both the 2.4GHz and 5GHz client list URLs
|
||||
for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'):
|
||||
url = 'http://{}/{}/userRpm/{}' \
|
||||
.format(self.host, self.token, clients_url)
|
||||
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
|
||||
|
||||
self.last_results = [mac.replace("-", ":") for mac in result]
|
||||
self.last_results = [mac.replace("-", ":") for mac in mac_results]
|
||||
return True
|
||||
|
|
|
@ -12,13 +12,13 @@ import threading
|
|||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
DOMAIN = "discovery"
|
||||
REQUIREMENTS = ['netdisco==0.7.1']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_NETGEAR: ('device_tracker', None),
|
||||
|
|
|
@ -16,21 +16,20 @@ from homeassistant.helpers import validate_config
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import sanitize_filename
|
||||
|
||||
DOMAIN = "downloader"
|
||||
|
||||
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,
|
||||
})
|
||||
ATTR_SUBDIR = 'subdir'
|
||||
ATTR_URL = 'url'
|
||||
|
||||
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
|
||||
def setup(hass, config):
|
||||
|
|
|
@ -6,30 +6,28 @@ https://home-assistant.io/components/dweet/
|
|||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
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
|
||||
from homeassistant.helpers import state as state_helper
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['dweepy==0.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "dweet"
|
||||
DEPENDENCIES = []
|
||||
|
||||
REQUIREMENTS = ['dweepy==0.2.0']
|
||||
|
||||
CONF_NAME = 'name'
|
||||
CONF_WHITELIST = 'whitelist'
|
||||
DOMAIN = 'dweet'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
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)
|
||||
|
||||
|
@ -38,8 +36,8 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
def setup(hass, config):
|
||||
"""Setup the Dweet.io component."""
|
||||
conf = config[DOMAIN]
|
||||
name = conf[CONF_NAME]
|
||||
whitelist = conf.get(CONF_WHITELIST, [])
|
||||
name = conf.get(CONF_NAME)
|
||||
whitelist = conf.get(CONF_WHITELIST)
|
||||
json_body = {}
|
||||
|
||||
def dweet_event_listener(event):
|
||||
|
|
|
@ -7,7 +7,9 @@ https://home-assistant.io/components/ecobee/
|
|||
import logging
|
||||
import os
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.loader import get_component
|
||||
|
@ -15,12 +17,19 @@ from homeassistant.util import Throttle
|
|||
|
||||
DOMAIN = "ecobee"
|
||||
NETWORK = None
|
||||
HOLD_TEMP = 'hold_temp'
|
||||
CONF_HOLD_TEMP = 'hold_temp'
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/nkgilley/python-ecobee-api/archive/'
|
||||
'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__)
|
||||
|
||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
||||
|
@ -67,11 +76,12 @@ def setup_ecobee(hass, network, config):
|
|||
configurator = get_component('configurator')
|
||||
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)
|
||||
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
|
|
@ -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()
|
|
@ -4,23 +4,36 @@ EnOcean Component.
|
|||
For more details about this component, please refer to the documentation at
|
||||
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']
|
||||
|
||||
CONF_DEVICE = "device"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'enocean'
|
||||
|
||||
ENOCEAN_DONGLE = None
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the EnOcean component."""
|
||||
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)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -62,8 +62,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Required(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
|
||||
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=3, max=4)),
|
||||
vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE):
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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]
|
|
@ -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'
|
|
@ -14,6 +14,19 @@ URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
|
|||
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||
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)
|
||||
_REGISTERED_COMPONENTS = set()
|
||||
|
@ -94,9 +107,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
|||
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):
|
||||
"""Setup serving the frontend."""
|
||||
hass.wsgi.register_view(BootstrapView)
|
||||
hass.wsgi.register_view(ManifestJSONView)
|
||||
|
||||
if hass.wsgi.development:
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -199,3 +225,17 @@ class IndexView(HomeAssistantView):
|
|||
panel_url=panel_url, panels=PANELS)
|
||||
|
||||
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")
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<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='apple-touch-icon' sizes='180x180'
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "457d5acd123e7dc38947c07984b3a5e8",
|
||||
"frontend.html": "829ee7cb591b8a63d7f22948a7aeb07a",
|
||||
"mdi.html": "b399b5d3798f5b68b0a4fbaae3432d48",
|
||||
"core.js": "1fd10c1fcdf56a61f60cf861d5a0368c",
|
||||
"frontend.html": "88c97d278de3320278da6c32fe9e7d61",
|
||||
"mdi.html": "710b84acc99b32514f52291aba9cd8e8",
|
||||
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
|
||||
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
||||
"panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa",
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit 474366c536ec3e471da12d5f15b07b79fe9b07e2
|
||||
Subproject commit 670ba0292bfca2b65aeca70804c0856b6cabf10e
|
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
|
@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
@ -54,6 +54,9 @@ def open_door(hass, entity_id=None):
|
|||
|
||||
def setup(hass, config):
|
||||
"""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(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_GARAGE_DOORS)
|
||||
component.setup(config)
|
||||
|
|
|
@ -15,6 +15,10 @@ from homeassistant.components.garage_door import GarageDoorDevice
|
|||
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__)
|
||||
|
@ -32,32 +36,42 @@ _DOORS_SCHEMA = vol.All(
|
|||
PLATFORM_SCHEMA = vol.Schema({
|
||||
'platform': str,
|
||||
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
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the garage door platform."""
|
||||
relay_time = config.get(RELAY_TIME)
|
||||
state_pull_mode = config.get(STATE_PULL_MODE)
|
||||
doors = []
|
||||
doors_conf = config.get('doors')
|
||||
|
||||
for door in doors_conf:
|
||||
doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'],
|
||||
door['state_pin']))
|
||||
door['state_pin'],
|
||||
state_pull_mode,
|
||||
relay_time))
|
||||
add_devices(doors)
|
||||
|
||||
|
||||
class RPiGPIOGarageDoor(GarageDoorDevice):
|
||||
"""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."""
|
||||
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, 'UP')
|
||||
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
|
||||
rpi_gpio.write_output(self._relay_pin, True)
|
||||
|
||||
@property
|
||||
|
@ -82,7 +96,7 @@ class RPiGPIOGarageDoor(GarageDoorDevice):
|
|||
def _trigger(self):
|
||||
"""Trigger the door."""
|
||||
rpi_gpio.write_output(self._relay_pin, False)
|
||||
sleep(0.2)
|
||||
sleep(self._relay_time)
|
||||
rpi_gpio.write_output(self._relay_pin, True)
|
||||
|
||||
def close_door(self):
|
||||
|
|
|
@ -10,7 +10,7 @@ from homeassistant.components.garage_door import GarageDoorDevice
|
|||
from homeassistant.components.wink import WinkDevice
|
||||
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):
|
||||
|
|
|
@ -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
|
||||
https://home-assistant.io/components/graphite/
|
||||
|
@ -10,26 +10,48 @@ import socket
|
|||
import threading
|
||||
import time
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED)
|
||||
from homeassistant.helpers import state
|
||||
import voluptuous as vol
|
||||
|
||||
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__)
|
||||
|
||||
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):
|
||||
"""Setup the Graphite feeder."""
|
||||
graphite_config = config.get('graphite', {})
|
||||
host = graphite_config.get('host', 'localhost')
|
||||
prefix = graphite_config.get('prefix', 'ha')
|
||||
conf = config[DOMAIN]
|
||||
host = conf.get(CONF_HOST)
|
||||
prefix = conf.get(CONF_PREFIX)
|
||||
port = conf.get(CONF_PORT)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
port = int(graphite_config.get('port', 2003))
|
||||
except ValueError:
|
||||
_LOGGER.error('Invalid port specified')
|
||||
sock.connect((host, port))
|
||||
sock.shutdown(2)
|
||||
_LOGGER.debug('Connection to Graphite possible')
|
||||
except socket.error:
|
||||
_LOGGER.error('Not able to connect to Graphite')
|
||||
return False
|
||||
|
||||
GraphiteFeeder(hass, host, port, prefix)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -7,38 +7,49 @@ https://home-assistant.io/components/homematic/
|
|||
import os
|
||||
import time
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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_component import EntityComponent
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ["pyhomematic==0.1.11"]
|
||||
REQUIREMENTS = ["pyhomematic==0.1.13"]
|
||||
|
||||
HOMEMATIC = None
|
||||
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_LIGHTS = 'homematic.light'
|
||||
DISCOVER_SENSORS = 'homematic.sensor'
|
||||
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
|
||||
DISCOVER_ROLLERSHUTTER = 'homematic.rollershutter'
|
||||
DISCOVER_THERMOSTATS = 'homematic.thermostat'
|
||||
DISCOVER_COVER = 'homematic.cover'
|
||||
DISCOVER_CLIMATE = 'homematic.climate'
|
||||
|
||||
ATTR_DISCOVER_DEVICES = 'devices'
|
||||
ATTR_PARAM = 'param'
|
||||
ATTR_CHANNEL = 'channel'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_ADDRESS = 'address'
|
||||
ATTR_VALUE = 'value'
|
||||
|
||||
EVENT_KEYPRESS = 'homematic.keypress'
|
||||
EVENT_IMPULSE = 'homematic.impulse'
|
||||
|
||||
SERVICE_VIRTUALKEY = 'virtualkey'
|
||||
SERVICE_SET_VALUE = 'set_value'
|
||||
|
||||
HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'],
|
||||
|
@ -46,15 +57,17 @@ HM_DEVICE_TYPES = {
|
|||
DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2',
|
||||
'RemoteMotion', 'ThermostatWall', 'AreaThermostat',
|
||||
'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas',
|
||||
'LuxSensor'],
|
||||
DISCOVER_THERMOSTATS: ['Thermostat', 'ThermostatWall', 'MAXThermostat'],
|
||||
'LuxSensor', 'WeatherSensor', 'WeatherStation'],
|
||||
DISCOVER_CLIMATE: ['Thermostat', 'ThermostatWall', 'MAXThermostat'],
|
||||
DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion',
|
||||
'MotionV2', 'RemoteMotion'],
|
||||
DISCOVER_ROLLERSHUTTER: ['Blind']
|
||||
'MotionV2', 'RemoteMotion', 'WeatherSensor',
|
||||
'TiltSensor'],
|
||||
DISCOVER_COVER: ['Blind']
|
||||
}
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE = [
|
||||
'ACTUAL_TEMPERATURE'
|
||||
'ACTUAL_TEMPERATURE',
|
||||
'ACTUAL_HUMIDITY'
|
||||
]
|
||||
|
||||
HM_ATTRIBUTE_SUPPORT = {
|
||||
|
@ -66,7 +79,8 @@ HM_ATTRIBUTE_SUPPORT = {
|
|||
'CONTROL_MODE': ['Mode', {0: 'Auto', 1: 'Manual', 2: 'Away', 3: 'Boost'}],
|
||||
'POWER': ['Power', {}],
|
||||
'CURRENT': ['Current', {}],
|
||||
'VOLTAGE': ['Voltage', {}]
|
||||
'VOLTAGE': ['Voltage', {}],
|
||||
'WORKING': ['Working', {0: 'No', 1: 'Yes'}],
|
||||
}
|
||||
|
||||
HM_PRESS_EVENTS = [
|
||||
|
@ -96,51 +110,85 @@ CONF_REMOTE_PORT = 'remote_port'
|
|||
CONF_RESOLVENAMES = 'resolvenames'
|
||||
CONF_DELAY = 'delay'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_LOCAL_IP): vol.Coerce(str),
|
||||
vol.Optional(CONF_LOCAL_PORT, default=8943):
|
||||
vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1, max=65535)),
|
||||
vol.Required(CONF_REMOTE_IP): vol.Coerce(str),
|
||||
vol.Optional(CONF_REMOTE_PORT, default=2001):
|
||||
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)
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "homematic",
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_ADDRESS): cv.string,
|
||||
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
|
||||
vol.Optional(ATTR_PARAM): cv.string,
|
||||
})
|
||||
|
||||
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({
|
||||
vol.Required(ATTR_ADDRESS): vol.Coerce(str),
|
||||
vol.Required(ATTR_ADDRESS): cv.string,
|
||||
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):
|
||||
"""Setup the Homematic component."""
|
||||
global HOMEMATIC, HOMEMATIC_LINK_DELAY
|
||||
|
||||
from pyhomematic import HMConnection
|
||||
|
||||
local_ip = config[DOMAIN][0].get(CONF_LOCAL_IP)
|
||||
local_port = config[DOMAIN][0].get(CONF_LOCAL_PORT)
|
||||
remote_ip = config[DOMAIN][0].get(CONF_REMOTE_IP)
|
||||
remote_port = config[DOMAIN][0].get(CONF_REMOTE_PORT)
|
||||
resolvenames = config[DOMAIN][0].get(CONF_RESOLVENAMES)
|
||||
username = config[DOMAIN][0].get(CONF_USERNAME)
|
||||
password = config[DOMAIN][0].get(CONF_PASSWORD)
|
||||
HOMEMATIC_LINK_DELAY = config[DOMAIN][0].get(CONF_DELAY)
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
local_ip = config[DOMAIN].get(CONF_LOCAL_IP)
|
||||
local_port = config[DOMAIN].get(CONF_LOCAL_PORT)
|
||||
remote_ip = config[DOMAIN].get(CONF_REMOTE_IP)
|
||||
remote_port = config[DOMAIN].get(CONF_REMOTE_PORT)
|
||||
resolvenames = config[DOMAIN].get(CONF_RESOLVENAMES)
|
||||
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:
|
||||
_LOGGER.error("Missing remote CCU/Homegear or local address")
|
||||
return False
|
||||
|
||||
# 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,
|
||||
localport=local_port,
|
||||
remote=remote_ip,
|
||||
|
@ -165,13 +213,45 @@ def setup(hass, config):
|
|||
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY,
|
||||
_hm_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
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def system_callback_handler(hass, config, src, *args):
|
||||
def _system_callback_handler(hass, config, src, *args):
|
||||
"""Callback handler."""
|
||||
if src == 'newDevices':
|
||||
_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 (
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
('light', DISCOVER_LIGHTS),
|
||||
('rollershutter', DISCOVER_ROLLERSHUTTER),
|
||||
('rollershutter', DISCOVER_COVER),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS),
|
||||
('sensor', DISCOVER_SENSORS),
|
||||
('thermostat', DISCOVER_THERMOSTATS)):
|
||||
('climate', DISCOVER_CLIMATE)):
|
||||
# Get all devices of a specific type
|
||||
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):
|
||||
"""Get the Homematic devices."""
|
||||
# run
|
||||
device_arr = []
|
||||
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
for key in keys:
|
||||
device = HOMEMATIC.devices[key]
|
||||
class_name = device.__class__.__name__
|
||||
|
@ -247,15 +328,22 @@ def _get_devices(device_type, keys):
|
|||
name = _create_ha_name(name=device.NAME,
|
||||
channel=channel,
|
||||
param=param)
|
||||
device_dict = dict(platform="homematic",
|
||||
address=key,
|
||||
name=name,
|
||||
channel=channel)
|
||||
device_dict = {
|
||||
CONF_PLATFORM: "homematic",
|
||||
ATTR_ADDRESS: key,
|
||||
ATTR_NAME: name,
|
||||
ATTR_CHANNEL: channel
|
||||
}
|
||||
if param is not None:
|
||||
device_dict[ATTR_PARAM] = param
|
||||
device_dict.update({ATTR_PARAM: param})
|
||||
|
||||
# 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:
|
||||
_LOGGER.debug("Channel %i not in params", channel)
|
||||
else:
|
||||
|
@ -405,16 +493,99 @@ def _hm_service_virtualkey(call):
|
|||
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):
|
||||
"""The Homematic device base object."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, config):
|
||||
"""Initialize a generic Homematic device."""
|
||||
self._name = config.get(ATTR_NAME, None)
|
||||
self._address = config.get(ATTR_ADDRESS, None)
|
||||
self._channel = config.get(ATTR_CHANNEL, 1)
|
||||
self._state = config.get(ATTR_PARAM, None)
|
||||
self._name = config.get(ATTR_NAME)
|
||||
self._address = config.get(ATTR_ADDRESS)
|
||||
self._channel = config.get(ATTR_CHANNEL)
|
||||
self._state = config.get(ATTR_PARAM)
|
||||
self._data = {}
|
||||
self._hmdevice = None
|
||||
self._connected = False
|
||||
|
|
|
@ -135,6 +135,9 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
|
|||
# pylint: disable=too-many-branches
|
||||
def setup(hass, config):
|
||||
"""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.setup(config)
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue