Merge pull request #1727 from balloob/dev

0.17
This commit is contained in:
Paulus Schoutsen 2016-04-08 21:43:15 -07:00
commit e97667aea0
161 changed files with 5979 additions and 2602 deletions

View File

@ -72,6 +72,7 @@ omit =
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
homeassistant/components/camera/generic.py homeassistant/components/camera/generic.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/asuswrt.py
@ -107,6 +108,8 @@ omit =
homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/yamaha.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/googlevoice.py homeassistant/components/notify/googlevoice.py
@ -136,7 +139,10 @@ omit =
homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/forecast.py homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/netatmo.py homeassistant/components/sensor/netatmo.py
homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/onewire.py homeassistant/components/sensor/onewire.py
homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/openweathermap.py

View File

@ -1,3 +1,5 @@
Feature requests should go in the forum: https://community.home-assistant.io/c/feature-requests
**Home Assistant release (`hass --version`):** **Home Assistant release (`hass --version`):**

1
.gitignore vendored
View File

@ -1,6 +1,5 @@
config/* config/*
!config/home-assistant.conf.default !config/home-assistant.conf.default
homeassistant/components/frontend/www_static/polymer/bower_components/*
# There is not a better solution afaik.. # There is not a better solution afaik..
!config/custom_components !config/custom_components

View File

@ -1,9 +1,9 @@
homeassistant: homeassistant:
# Omitted values in this section will be auto detected using freegeoip.net # Omitted values in this section will be auto detected using freegeoip.io
# Location required to calculate the time the sun rises and sets. # Location required to calculate the time the sun rises and sets.
# Cooridinates are also used for location for weather related components. # Coordinates are also used for location for weather related components.
# Google Maps can be used to determine more precise GPS cooridinates. # Google Maps can be used to determine more precise GPS coordinates.
latitude: 32.87336 latitude: 32.87336
longitude: 117.22743 longitude: 117.22743

View File

@ -9,8 +9,6 @@ import threading
import time import time
from multiprocessing import Process from multiprocessing import Process
import homeassistant.config as config_util
from homeassistant import bootstrap
from homeassistant.const import ( from homeassistant.const import (
__version__, __version__,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
@ -32,6 +30,7 @@ def validate_python():
def ensure_config_path(config_dir): def ensure_config_path(config_dir):
"""Validate the configuration directory.""" """Validate the configuration directory."""
import homeassistant.config as config_util
lib_dir = os.path.join(config_dir, 'lib') lib_dir = os.path.join(config_dir, 'lib')
# Test if configuration directory exists # Test if configuration directory exists
@ -60,6 +59,7 @@ def ensure_config_path(config_dir):
def ensure_config_file(config_dir): def ensure_config_file(config_dir):
"""Ensure configuration file exists.""" """Ensure configuration file exists."""
import homeassistant.config as config_util
config_path = config_util.ensure_config_exists(config_dir) config_path = config_util.ensure_config_exists(config_dir)
if config_path is None: if config_path is None:
@ -71,6 +71,7 @@ def ensure_config_file(config_dir):
def get_arguments(): def get_arguments():
"""Get parsed passed in arguments.""" """Get parsed passed in arguments."""
import homeassistant.config as config_util
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Home Assistant: Observe, Control, Automate.") description="Home Assistant: Observe, Control, Automate.")
parser.add_argument('--version', action='version', version=__version__) parser.add_argument('--version', action='version', version=__version__)
@ -225,6 +226,8 @@ def setup_and_run_hass(config_dir, args, top_process=False):
Block until stopped. Will assume it is running in a subprocess unless Block until stopped. Will assume it is running in a subprocess unless
top_process is set to true. top_process is set to true.
""" """
from homeassistant import bootstrap
if args.demo_mode: if args.demo_mode:
config = { config = {
'frontend': {}, 'frontend': {},
@ -241,6 +244,9 @@ def setup_and_run_hass(config_dir, args, top_process=False):
config_file, daemon=args.daemon, verbose=args.verbose, config_file, daemon=args.daemon, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
if hass is None:
return
if args.open_ui: if args.open_ui:
def open_browser(event): def open_browser(event):
"""Open the webinterface in a browser.""" """Open the webinterface in a browser."""

View File

@ -8,6 +8,8 @@ import sys
from collections import defaultdict from collections import defaultdict
from threading import RLock from threading import RLock
import voluptuous as vol
import homeassistant.components as core_components import homeassistant.components as core_components
import homeassistant.components.group as group import homeassistant.components.group as group
import homeassistant.config as config_util import homeassistant.config as config_util
@ -19,8 +21,9 @@ import homeassistant.util.package as pkg_util
from homeassistant.const import ( from homeassistant.const import (
CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME,
CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED, CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED,
TEMP_CELCIUS, TEMP_FAHRENHEIT, __version__) TEMP_CELCIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__)
from homeassistant.helpers import event_decorators, service from homeassistant.helpers import (
event_decorators, service, config_per_platform, extract_domain_configs)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,7 +32,6 @@ _CURRENT_SETUP = []
ATTR_COMPONENT = 'component' ATTR_COMPONENT = 'component'
PLATFORM_FORMAT = '{}.{}'
ERROR_LOG_FILENAME = 'home-assistant.log' ERROR_LOG_FILENAME = 'home-assistant.log'
@ -72,7 +74,7 @@ def _handle_requirements(hass, component, name):
def _setup_component(hass, domain, config): def _setup_component(hass, domain, config):
"""Setup a component for Home Assistant.""" """Setup a component for Home Assistant."""
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements,too-many-branches
if domain in hass.config.components: if domain in hass.config.components:
return True return True
@ -96,6 +98,56 @@ def _setup_component(hass, domain, config):
domain, ", ".join(missing_deps)) domain, ", ".join(missing_deps))
return False return False
if hasattr(component, 'CONFIG_SCHEMA'):
try:
config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex:
_LOGGER.error('Invalid config for [%s]: %s', domain, ex)
return False
elif hasattr(component, 'PLATFORM_SCHEMA'):
platforms = []
for p_name, p_config in config_per_platform(config, domain):
# Validate component specific platform schema
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex:
_LOGGER.error('Invalid platform config for [%s]: %s. %s',
domain, ex, p_config)
return False
# Not all platform components follow same pattern for platforms
# Sof if p_name is None we are not going to validate platform
# (the automation component is one of them)
if p_name is None:
platforms.append(p_validated)
continue
platform = prepare_setup_platform(hass, config, domain,
p_name)
if platform is None:
return False
# Validate platform specific schema
if hasattr(platform, 'PLATFORM_SCHEMA'):
try:
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex:
_LOGGER.error(
'Invalid platform config for [%s.%s]: %s. %s',
domain, p_name, ex, p_config)
return False
platforms.append(p_validated)
# Create a copy of the configuration with all config for current
# component removed and add validated config back in.
filter_keys = extract_domain_configs(config, domain)
config = {key: value for key, value in config.items()
if key not in filter_keys}
config[domain] = platforms
if not _handle_requirements(hass, component, domain): if not _handle_requirements(hass, component, domain):
return False return False
@ -130,7 +182,7 @@ def prepare_setup_platform(hass, config, domain, platform_name):
platform_path = PLATFORM_FORMAT.format(domain, platform_name) platform_path = PLATFORM_FORMAT.format(domain, platform_name)
platform = loader.get_component(platform_path) platform = loader.get_platform(domain, platform_name)
# Not found # Not found
if platform is None: if platform is None:
@ -176,8 +228,14 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
mount_local_lib_path(config_dir) mount_local_lib_path(config_dir)
try:
process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA(
config.get(core.DOMAIN, {})))
except vol.MultipleInvalid as ex:
_LOGGER.error('Invalid config for [homeassistant]: %s', ex)
return None
process_ha_config_upgrade(hass) process_ha_config_upgrade(hass)
process_ha_core_config(hass, config.get(core.DOMAIN, {}))
if enable_log: if enable_log:
enable_logging(hass, verbose, daemon, log_rotate_days) enable_logging(hass, verbose, daemon, log_rotate_days)
@ -262,8 +320,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
} }
)) ))
except ImportError: except ImportError:
_LOGGER.warning( pass
"Colorlog package not found, console coloring disabled")
# Log errors to a file if we have write access to file or config dir # Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path(ERROR_LOG_FILENAME) err_log_path = hass.config.path(ERROR_LOG_FILENAME)
@ -336,40 +393,28 @@ def process_ha_core_config(hass, config):
else: else:
_LOGGER.error('Received invalid time zone %s', time_zone_str) _LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr, typ in ((CONF_LATITUDE, 'latitude', float), for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude', float), (CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name', str)): (CONF_NAME, 'location_name')):
if key in config: if key in config:
try: setattr(hac, attr, config[key])
setattr(hac, attr, typ(config[key]))
except ValueError:
_LOGGER.error('Received invalid %s value for %s: %s',
typ.__name__, key, attr)
set_time_zone(config.get(CONF_TIME_ZONE)) if CONF_TIME_ZONE in config:
set_time_zone(config.get(CONF_TIME_ZONE))
customize = config.get(CONF_CUSTOMIZE) for entity_id, attrs in config.get(CONF_CUSTOMIZE).items():
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
if isinstance(customize, dict):
for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items():
if not isinstance(attrs, dict):
continue
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
if CONF_TEMPERATURE_UNIT in config: if CONF_TEMPERATURE_UNIT in config:
unit = config[CONF_TEMPERATURE_UNIT] hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
if unit == 'C':
hac.temperature_unit = TEMP_CELCIUS
elif unit == 'F':
hac.temperature_unit = TEMP_FAHRENHEIT
# If we miss some of the needed values, auto detect them # If we miss some of the needed values, auto detect them
if None not in ( if None not in (
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone): hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
return return
_LOGGER.info('Auto detecting location and temperature unit') _LOGGER.warning('Incomplete core config. Auto detecting location and '
'temperature unit')
info = loc_util.detect_location_info() info = loc_util.detect_location_info()

View File

@ -12,6 +12,7 @@ from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent

View File

@ -6,43 +6,55 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/
""" """
import logging import logging
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
CONF_NAME)
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "MQTT Alarm"
DEFAULT_QOS = 0
DEFAULT_PAYLOAD_DISARM = "DISARM"
DEFAULT_PAYLOAD_ARM_HOME = "ARM_HOME"
DEFAULT_PAYLOAD_ARM_AWAY = "ARM_AWAY"
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
CONF_PAYLOAD_DISARM = 'payload_disarm'
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
CONF_CODE = 'code'
DEFAULT_NAME = "MQTT Alarm"
DEFAULT_DISARM = "DISARM"
DEFAULT_ARM_HOME = "ARM_HOME"
DEFAULT_ARM_AWAY = "ARM_AWAY"
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
vol.Optional(CONF_CODE): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the MQTT platform.""" """Setup the MQTT platform."""
if config.get('state_topic') is None:
_LOGGER.error("Missing required variable: state_topic")
return False
if config.get('command_topic') is None:
_LOGGER.error("Missing required variable: command_topic")
return False
add_devices([MqttAlarm( add_devices([MqttAlarm(
hass, hass,
config.get('name', DEFAULT_NAME), config[CONF_NAME],
config.get('state_topic'), config[CONF_STATE_TOPIC],
config.get('command_topic'), config[CONF_COMMAND_TOPIC],
config.get('qos', DEFAULT_QOS), config[CONF_QOS],
config.get('payload_disarm', DEFAULT_PAYLOAD_DISARM), config[CONF_PAYLOAD_DISARM],
config.get('payload_arm_home', DEFAULT_PAYLOAD_ARM_HOME), config[CONF_PAYLOAD_ARM_HOME],
config.get('payload_arm_away', DEFAULT_PAYLOAD_ARM_AWAY), config[CONF_PAYLOAD_ARM_AWAY],
config.get('code'))]) config.get(CONF_CODE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes
@ -62,7 +74,7 @@ class MqttAlarm(alarm.AlarmControlPanel):
self._payload_disarm = payload_disarm self._payload_disarm = payload_disarm
self._payload_arm_home = payload_arm_home self._payload_arm_home = payload_arm_home
self._payload_arm_away = payload_arm_away self._payload_arm_away = payload_arm_away
self._code = str(code) if code else None self._code = code
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
"""A new MQTT message has been received.""" """A new MQTT message has been received."""

View File

@ -6,13 +6,15 @@ https://home-assistant.io/components/automation/
""" """
import logging import logging
import voluptuous as vol
from homeassistant.bootstrap import prepare_setup_platform from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.const import CONF_PLATFORM from homeassistant.const import CONF_PLATFORM
from homeassistant.components import logbook from homeassistant.components import logbook
from homeassistant.helpers import extract_domain_configs from homeassistant.helpers import extract_domain_configs
from homeassistant.helpers.service import (call_from_config, from homeassistant.helpers.service import call_from_config
validate_service_call) from homeassistant.loader import get_platform
import homeassistant.helpers.config_validation as cv
DOMAIN = 'automation' DOMAIN = 'automation'
@ -31,17 +33,72 @@ CONDITION_TYPE_OR = 'or'
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
METHOD_TRIGGER = 'trigger'
METHOD_IF_ACTION = 'if_action'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _platform_validator(method, schema):
"""Generate platform validator for different steps."""
def validator(config):
"""Validate it is a valid platform."""
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
if not hasattr(platform, method):
raise vol.Invalid('invalid method platform')
if not hasattr(platform, schema):
return config
print('validating config', method, config)
return getattr(platform, schema)(config)
return validator
_TRIGGER_SCHEMA = vol.All(
cv.ensure_list,
[
vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
}, extra=vol.ALLOW_EXTRA),
_platform_validator(METHOD_TRIGGER, 'TRIGGER_SCHEMA')
),
]
)
_CONDITION_SCHEMA = vol.Any(
CONDITION_USE_TRIGGER_VALUES,
vol.All(
cv.ensure_list,
[
vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN),
}, extra=vol.ALLOW_EXTRA),
_platform_validator(METHOD_IF_ACTION, 'IF_ACTION_SCHEMA'),
)
]
)
)
PLATFORM_SCHEMA = vol.Schema({
CONF_ALIAS: cv.string,
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
CONF_CONDITION: _CONDITION_SCHEMA,
vol.Required(CONF_ACTION): cv.SERVICE_SCHEMA,
})
def setup(hass, config): def setup(hass, config):
"""Setup the automation.""" """Setup the automation."""
for config_key in extract_domain_configs(config, DOMAIN): for config_key in extract_domain_configs(config, DOMAIN):
conf = config[config_key] conf = config[config_key]
if not isinstance(conf, list):
conf = [conf]
for list_no, config_block in enumerate(conf): for list_no, config_block in enumerate(conf):
name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key, name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key,
list_no)) list_no))
@ -54,10 +111,7 @@ def _setup_automation(hass, config_block, name, config):
"""Setup one instance of automation.""" """Setup one instance of automation."""
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name) action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)
if action is None: if CONF_CONDITION in config_block:
return False
if CONF_CONDITION in config_block or CONF_CONDITION_TYPE in config_block:
action = _process_if(hass, config, config_block, action) action = _process_if(hass, config, config_block, action)
if action is None: if action is None:
@ -70,11 +124,6 @@ def _setup_automation(hass, config_block, name, config):
def _get_action(hass, config, name): def _get_action(hass, config, name):
"""Return an action based on a configuration.""" """Return an action based on a configuration."""
validation_error = validate_service_call(config)
if validation_error:
_LOGGER.error(validation_error)
return None
def action(): def action():
"""Action to be executed.""" """Action to be executed."""
_LOGGER.info('Executing %s', name) _LOGGER.info('Executing %s', name)
@ -96,12 +145,9 @@ def _process_if(hass, config, p_config, action):
if use_trigger: if use_trigger:
if_configs = p_config[CONF_TRIGGER] if_configs = p_config[CONF_TRIGGER]
if isinstance(if_configs, dict):
if_configs = [if_configs]
checks = [] checks = []
for if_config in if_configs: for if_config in if_configs:
platform = _resolve_platform('if_action', hass, config, platform = _resolve_platform(METHOD_IF_ACTION, hass, config,
if_config.get(CONF_PLATFORM)) if_config.get(CONF_PLATFORM))
if platform is None: if platform is None:
continue continue
@ -134,7 +180,7 @@ def _process_trigger(hass, config, trigger_configs, name, action):
trigger_configs = [trigger_configs] trigger_configs = [trigger_configs]
for conf in trigger_configs: for conf in trigger_configs:
platform = _resolve_platform('trigger', hass, config, platform = _resolve_platform(METHOD_TRIGGER, hass, config,
conf.get(CONF_PLATFORM)) conf.get(CONF_PLATFORM))
if platform is None: if platform is None:
continue continue

View File

@ -4,26 +4,29 @@ Offer MQTT listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#mqtt-trigger at https://home-assistant.io/components/automation/#mqtt-trigger
""" """
import logging import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_PLATFORM
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
CONF_TOPIC = 'topic' CONF_TOPIC = 'topic'
CONF_PAYLOAD = 'payload' CONF_PAYLOAD = 'payload'
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): cv.string,
})
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
topic = config.get(CONF_TOPIC) topic = config[CONF_TOPIC]
payload = config.get(CONF_PAYLOAD) payload = config.get(CONF_PAYLOAD)
if topic is None:
logging.getLogger(__name__).error(
"Missing configuration key %s", CONF_TOPIC)
return False
def mqtt_automation_listener(msg_topic, msg_payload, qos): def mqtt_automation_listener(msg_topic, msg_payload, qos):
"""Listen for MQTT messages.""" """Listen for MQTT messages."""
if payload is None or payload == msg_payload: if payload is None or payload == msg_payload:

View File

@ -4,16 +4,17 @@ Offer state listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#state-trigger at https://home-assistant.io/components/automation/#state-trigger
""" """
import logging
from datetime import timedelta from datetime import timedelta
import homeassistant.util.dt as dt_util import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, CONF_PLATFORM)
from homeassistant.components.automation.time import ( from homeassistant.components.automation.time import (
CONF_HOURS, CONF_MINUTES, CONF_SECONDS) CONF_HOURS, CONF_MINUTES, CONF_SECONDS)
from homeassistant.helpers.event import track_state_change, track_point_in_time from homeassistant.helpers.event import track_state_change, track_point_in_time
import homeassistant.helpers.config_validation as cv
CONF_ENTITY_ID = "entity_id" CONF_ENTITY_ID = "entity_id"
CONF_FROM = "from" CONF_FROM = "from"
@ -21,6 +22,33 @@ CONF_TO = "to"
CONF_STATE = "state" CONF_STATE = "state"
CONF_FOR = "for" CONF_FOR = "for"
BASE_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'state',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
# These are str on purpose. Want to catch YAML conversions
CONF_STATE: str,
CONF_FOR: vol.All(vol.Schema({
CONF_HOURS: vol.Coerce(int),
CONF_MINUTES: vol.Coerce(int),
CONF_SECONDS: vol.Coerce(int),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS)),
})
TRIGGER_SCHEMA = vol.Schema(vol.All(
BASE_SCHEMA.extend({
# These are str on purpose. Want to catch YAML conversions
CONF_FROM: str,
CONF_TO: str,
}),
vol.Any(cv.key_dependency(CONF_FOR, CONF_TO),
cv.key_dependency(CONF_FOR, CONF_STATE))
))
IF_ACTION_SCHEMA = vol.Schema(vol.All(
BASE_SCHEMA,
cv.key_dependency(CONF_FOR, CONF_STATE)
))
def get_time_config(config): def get_time_config(config):
"""Helper function to extract the time specified in the configuration.""" """Helper function to extract the time specified in the configuration."""
@ -31,18 +59,6 @@ def get_time_config(config):
minutes = config[CONF_FOR].get(CONF_MINUTES) minutes = config[CONF_FOR].get(CONF_MINUTES)
seconds = config[CONF_FOR].get(CONF_SECONDS) seconds = config[CONF_FOR].get(CONF_SECONDS)
if hours is None and minutes is None and seconds is None:
logging.getLogger(__name__).error(
"Received invalid value for '%s': %s",
config[CONF_FOR], CONF_FOR)
return None
if config.get(CONF_TO) is None and config.get(CONF_STATE) is None:
logging.getLogger(__name__).error(
"For: requires a to: value'%s': %s",
config[CONF_FOR], CONF_FOR)
return None
return timedelta(hours=(hours or 0.0), return timedelta(hours=(hours or 0.0),
minutes=(minutes or 0.0), minutes=(minutes or 0.0),
seconds=(seconds or 0.0)) seconds=(seconds or 0.0))
@ -51,24 +67,10 @@ def get_time_config(config):
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
if entity_id is None:
logging.getLogger(__name__).error(
"Missing trigger configuration key %s", CONF_ENTITY_ID)
return None
from_state = config.get(CONF_FROM, MATCH_ALL) from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
time_delta = get_time_config(config) time_delta = get_time_config(config)
if isinstance(from_state, bool) or isinstance(to_state, bool):
logging.getLogger(__name__).error(
'Config error. Surround to/from values with quotes.')
return None
if CONF_FOR in config and time_delta is None:
return None
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
def state_for_listener(now): def state_for_listener(now):
@ -105,18 +107,7 @@ def if_action(hass, config):
"""Wrap action method with state based condition.""" """Wrap action method with state based condition."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
state = config.get(CONF_STATE) state = config.get(CONF_STATE)
if entity_id is None or state is None:
logging.getLogger(__name__).error(
"Missing if-condition configuration key %s or %s", CONF_ENTITY_ID,
CONF_STATE)
return None
time_delta = get_time_config(config) time_delta = get_time_config(config)
if CONF_FOR in config and time_delta is None:
return None
state = str(state)
def if_state(): def if_state():
"""Test if condition.""" """Test if condition."""

View File

@ -4,12 +4,16 @@ Offer sun based automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#sun-trigger at https://home-assistant.io/components/automation/#sun-trigger
""" """
import logging
from datetime import timedelta from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.const import CONF_PLATFORM
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components import sun from homeassistant.components import sun
from homeassistant.helpers.event import track_sunrise, track_sunset from homeassistant.helpers.event import track_sunrise, track_sunset
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['sun'] DEPENDENCIES = ['sun']
@ -26,22 +30,30 @@ EVENT_SUNRISE = 'sunrise'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_SUN_EVENT = vol.All(vol.Lower, vol.Any(EVENT_SUNRISE, EVENT_SUNSET))
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'sun',
vol.Required(CONF_EVENT): _SUN_EVENT,
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_offset,
})
IF_ACTION_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): 'sun',
CONF_BEFORE: _SUN_EVENT,
CONF_AFTER: _SUN_EVENT,
vol.Required(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_offset,
vol.Required(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_offset,
}),
cv.has_at_least_one_key(CONF_BEFORE, CONF_AFTER),
)
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
event = config.get(CONF_EVENT) event = config.get(CONF_EVENT)
offset = config.get(CONF_OFFSET)
if event is None:
_LOGGER.error("Missing configuration key %s", CONF_EVENT)
return False
event = event.lower()
if event not in (EVENT_SUNRISE, EVENT_SUNSET):
_LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event)
return False
offset = _parse_offset(config.get(CONF_OFFSET))
if offset is False:
return False
# Do something to call action # Do something to call action
if event == EVENT_SUNRISE: if event == EVENT_SUNRISE:
@ -56,26 +68,8 @@ def if_action(hass, config):
"""Wrap action method with sun based condition.""" """Wrap action method with sun based condition."""
before = config.get(CONF_BEFORE) before = config.get(CONF_BEFORE)
after = config.get(CONF_AFTER) after = config.get(CONF_AFTER)
before_offset = config.get(CONF_BEFORE_OFFSET)
# Make sure required configuration keys are present after_offset = config.get(CONF_AFTER_OFFSET)
if before is None and after is None:
logging.getLogger(__name__).error(
"Missing if-condition configuration key %s or %s",
CONF_BEFORE, CONF_AFTER)
return None
# Make sure configuration keys have the right value
if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \
after not in (None, EVENT_SUNRISE, EVENT_SUNSET):
logging.getLogger(__name__).error(
"%s and %s can only be set to %s or %s",
CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET)
return None
before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET))
after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET))
if before_offset is False or after_offset is False:
return None
if before is None: if before is None:
def before_func(): def before_func():
@ -120,27 +114,3 @@ def if_action(hass, config):
return True return True
return time_if return time_if
def _parse_offset(raw_offset):
"""Parse the offset."""
if raw_offset is None:
return timedelta(0)
negative_offset = False
if raw_offset.startswith('-'):
negative_offset = True
raw_offset = raw_offset[1:]
try:
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
except ValueError:
_LOGGER.error('Could not parse offset %s', raw_offset)
return False
offset = timedelta(hours=hour, minutes=minute, seconds=second)
if negative_offset:
offset *= -1
return offset

View File

@ -6,21 +6,27 @@ at https://home-assistant.io/components/automation/#template-trigger
""" """
import logging import logging
from homeassistant.const import CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED import voluptuous as vol
from homeassistant.const import (
CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED, CONF_PLATFORM)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'template',
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
})
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is None:
_LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE)
return False
# Local variable to keep track of if the action has already been triggered # Local variable to keep track of if the action has already been triggered
already_triggered = False already_triggered = False
@ -44,10 +50,6 @@ def if_action(hass, config):
"""Wrap action method with state based condition.""" """Wrap action method with state based condition."""
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is None:
_LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE)
return False
return lambda: _check_template(hass, value_template) return lambda: _check_template(hass, value_template)

View File

@ -4,12 +4,13 @@ Offer zone automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#zone-trigger at https://home-assistant.io/components/automation/#zone-trigger
""" """
import logging import voluptuous as vol
from homeassistant.components import zone from homeassistant.components import zone
from homeassistant.const import ( from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL) ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL, CONF_PLATFORM)
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv
CONF_ENTITY_ID = "entity_id" CONF_ENTITY_ID = "entity_id"
CONF_ZONE = "zone" CONF_ZONE = "zone"
@ -18,19 +19,26 @@ EVENT_ENTER = "enter"
EVENT_LEAVE = "leave" EVENT_LEAVE = "leave"
DEFAULT_EVENT = EVENT_ENTER DEFAULT_EVENT = EVENT_ENTER
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'zone',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ZONE): cv.entity_id,
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
vol.Any(EVENT_ENTER, EVENT_LEAVE),
})
IF_ACTION_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'zone',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ZONE): cv.entity_id,
})
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE) zone_entity_id = config.get(CONF_ZONE)
event = config.get(CONF_EVENT)
if entity_id is None or zone_entity_id is None:
logging.getLogger(__name__).error(
"Missing trigger configuration key %s or %s", CONF_ENTITY_ID,
CONF_ZONE)
return False
event = config.get(CONF_EVENT, DEFAULT_EVENT)
def zone_automation_listener(entity, from_s, to_s): def zone_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
@ -59,12 +67,6 @@ def if_action(hass, config):
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE) zone_entity_id = config.get(CONF_ZONE)
if entity_id is None or zone_entity_id is None:
logging.getLogger(__name__).error(
"Missing condition configuration key %s or %s", CONF_ENTITY_ID,
CONF_ZONE)
return False
def if_in_zone(): def if_in_zone():
"""Test if condition.""" """Test if condition."""
return _in_zone(hass, zone_entity_id, hass.states.get(entity_id)) return _in_zone(hass, zone_entity_id, hass.states.get(entity_id))

View File

@ -11,26 +11,28 @@ from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_ON, STATE_OFF) from homeassistant.const import (STATE_ON, STATE_OFF)
from homeassistant.components import ( from homeassistant.components import (
bloomsky, mysensors, zwave, vera, wemo, wink) bloomsky, mysensors, zwave, vera, wemo, wink)
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
DOMAIN = 'binary_sensor' DOMAIN = 'binary_sensor'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
SENSOR_CLASSES = [ SENSOR_CLASSES = [
None, # Generic on/off None, # Generic on/off
'opening', # Door, window, etc 'cold', # On means cold (or too cold)
'motion', # Motion sensor 'connectivity', # On means connection present, Off = no connection
'gas', # CO, CO2, etc 'gas', # CO, CO2, etc.
'smoke', # Smoke detector 'heat', # On means hot (or too hot)
'moisture', # Specifically a wetness sensor 'light', # Lightness threshold
'light', # Lightness threshold 'moisture', # Specifically a wetness sensor
'power', # Power, over-current, etc 'motion', # Motion sensor
'safety', # Generic on=unsafe, off=safe 'moving', # On means moving, Off means stopped
'heat', # On means hot (or too hot) 'opening', # Door, window, etc.
'cold', # On means cold (or too cold) 'power', # Power, over-current, etc
'moving', # On means moving, Off means stopped 'safety', # Generic on=unsafe, off=safe
'sound', # On means sound detected, Off means no sound 'smoke', # Smoke detector
'vibration', # On means vibration detected, Off means no vibration 'sound', # On means sound detected, Off means no sound
'vibration', # On means vibration detected, Off means no vibration
] ]
# Maps discovered services to their platforms # Maps discovered services to their platforms

View File

@ -9,7 +9,8 @@ from datetime import timedelta
import requests import requests
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import (BinarySensorDevice,
SENSOR_CLASSES)
from homeassistant.util import Throttle from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,6 +27,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
resource = config.get(CONF_RESOURCE) resource = config.get(CONF_RESOURCE)
pin = config.get(CONF_PIN) pin = config.get(CONF_PIN)
sensor_class = config.get('sensor_class')
if sensor_class not in SENSOR_CLASSES:
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
sensor_class = None
if None in (resource, pin): if None in (resource, pin):
_LOGGER.error('Not all required config keys present: %s', _LOGGER.error('Not all required config keys present: %s',
', '.join((CONF_RESOURCE, CONF_PIN))) ', '.join((CONF_RESOURCE, CONF_PIN)))
@ -45,21 +51,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
arest = ArestData(resource, pin) arest = ArestData(resource, pin)
add_devices([ArestBinarySensor(arest, add_devices([ArestBinarySensor(
resource, arest,
config.get('name', response['name']), resource,
pin)]) config.get('name', response['name']),
sensor_class,
pin)])
# pylint: disable=too-many-instance-attributes, too-many-arguments # pylint: disable=too-many-instance-attributes, too-many-arguments
class ArestBinarySensor(BinarySensorDevice): class ArestBinarySensor(BinarySensorDevice):
"""Implement an aREST binary sensor for a pin.""" """Implement an aREST binary sensor for a pin."""
def __init__(self, arest, resource, name, pin): def __init__(self, arest, resource, name, sensor_class, pin):
"""Initialize the aREST device.""" """Initialize the aREST device."""
self.arest = arest self.arest = arest
self._resource = resource self._resource = resource
self._name = name self._name = name
self._sensor_class = sensor_class
self._pin = pin self._pin = pin
self.update() self.update()
@ -79,6 +88,11 @@ class ArestBinarySensor(BinarySensorDevice):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
return bool(self.arest.data.get('state')) return bool(self.arest.data.get('state'))
@property
def sensor_class(self):
"""Return the class of this sensor."""
return self._sensor_class
def update(self): def update(self):
"""Get the latest data from aREST API.""" """Get the latest data from aREST API."""
self.arest.update() self.arest.update()

View File

@ -6,43 +6,50 @@ https://home-assistant.io/components/binary_sensor.mqtt/
""" """
import logging import logging
import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.binary_sensor import (BinarySensorDevice, from homeassistant.components.binary_sensor import (BinarySensorDevice,
SENSOR_CLASSES) SENSOR_CLASSES)
from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE
from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mqtt']
CONF_SENSOR_CLASS = 'sensor_class'
CONF_PAYLOAD_ON = 'payload_on'
CONF_PAYLOAD_OFF = 'payload_off'
DEFAULT_NAME = 'MQTT Binary sensor' DEFAULT_NAME = 'MQTT Binary sensor'
DEFAULT_QOS = 0
DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_OFF = 'OFF'
DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SENSOR_CLASS, default=None):
vol.Any(vol.In(SENSOR_CLASSES), vol.SetTo(None)),
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Add MQTT binary sensor.""" """Add MQTT binary sensor."""
if config.get('state_topic') is None:
_LOGGER.error('Missing required variable: state_topic')
return False
sensor_class = config.get('sensor_class')
if sensor_class not in SENSOR_CLASSES:
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
sensor_class = None
add_devices([MqttBinarySensor( add_devices([MqttBinarySensor(
hass, hass,
config.get('name', DEFAULT_NAME), config[CONF_NAME],
config.get('state_topic', None), config[CONF_STATE_TOPIC],
sensor_class, config[CONF_SENSOR_CLASS],
config.get('qos', DEFAULT_QOS), config[CONF_QOS],
config.get('payload_on', DEFAULT_PAYLOAD_ON), config[CONF_PAYLOAD_ON],
config.get('payload_off', DEFAULT_PAYLOAD_OFF), config[CONF_PAYLOAD_OFF],
config.get(CONF_VALUE_TEMPLATE))]) config.get(CONF_VALUE_TEMPLATE)
)])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes

View File

@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.tcp/
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.sensor.tcp import Sensor, DOMAIN, CONF_VALUE_ON from homeassistant.components.sensor.tcp import Sensor, CONF_VALUE_ON
DEPENDENCIES = [DOMAIN]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -14,10 +14,8 @@ import requests
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import bloomsky from homeassistant.components import bloomsky
from homeassistant.const import ( from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID
HTTP_NOT_FOUND, from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
ATTR_ENTITY_ID,
)
DOMAIN = 'camera' DOMAIN = 'camera'
@ -36,7 +34,7 @@ STATE_IDLE = 'idle'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
MULTIPART_BOUNDARY = '--jpegboundary' MULTIPART_BOUNDARY = '--jpgboundary'
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n' MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
@ -49,17 +47,6 @@ def setup(hass, config):
component.setup(config) component.setup(config)
# -------------------------------------------------------------------------
# CAMERA COMPONENT ENDPOINTS
# -------------------------------------------------------------------------
# The following defines the endpoints for serving images from the camera
# via the HA http server. This is means that you can access images from
# your camera outside of your LAN without the need for port forwards etc.
# Because the authentication header can't be added in image requests these
# endpoints are secured with session based security.
# pylint: disable=unused-argument
def _proxy_camera_image(handler, path_match, data): def _proxy_camera_image(handler, path_match, data):
"""Serve the camera image via the HA server.""" """Serve the camera image via the HA server."""
entity_id = path_match.group(ATTR_ENTITY_ID) entity_id = path_match.group(ATTR_ENTITY_ID)
@ -77,22 +64,16 @@ def setup(hass, config):
handler.end_headers() handler.end_headers()
return return
handler.wfile.write(response) handler.send_response(HTTP_OK)
handler.write_content(response)
hass.http.register_path( hass.http.register_path(
'GET', 'GET',
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'), re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_image) _proxy_camera_image)
# pylint: disable=unused-argument
def _proxy_camera_mjpeg_stream(handler, path_match, data): def _proxy_camera_mjpeg_stream(handler, path_match, data):
""" """Proxy the camera image as an mjpeg stream via the HA server."""
Proxy the camera image as an mjpeg stream via the HA server.
This function takes still images from the IP camera and turns them
into an MJPEG stream. This means that HA can return a live video
stream even with only a still image URL available.
"""
entity_id = path_match.group(ATTR_ENTITY_ID) entity_id = path_match.group(ATTR_ENTITY_ID)
camera = component.entities.get(entity_id) camera = component.entities.get(entity_id)
@ -112,8 +93,7 @@ def setup(hass, config):
hass.http.register_path( hass.http.register_path(
'GET', 'GET',
re.compile( re.compile(r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_mjpeg_stream) _proxy_camera_mjpeg_stream)
return True return True
@ -137,19 +117,16 @@ class Camera(Entity):
return ENTITY_IMAGE_URL.format(self.entity_id) return ENTITY_IMAGE_URL.format(self.entity_id)
@property @property
# pylint: disable=no-self-use
def is_recording(self): def is_recording(self):
"""Return true if the device is recording.""" """Return true if the device is recording."""
return False return False
@property @property
# pylint: disable=no-self-use
def brand(self): def brand(self):
"""Camera brand.""" """Camera brand."""
return None return None
@property @property
# pylint: disable=no-self-use
def model(self): def model(self):
"""Camera model.""" """Camera model."""
return None return None
@ -160,29 +137,28 @@ class Camera(Entity):
def mjpeg_stream(self, handler): def mjpeg_stream(self, handler):
"""Generate an HTTP MJPEG stream from camera images.""" """Generate an HTTP MJPEG stream from camera images."""
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8')) def write_string(text):
handler.request.sendall(bytes( """Helper method to write a string to the stream."""
'Content-type: multipart/x-mixed-replace; \ handler.request.sendall(bytes(text + '\r\n', 'utf-8'))
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8')) write_string('HTTP/1.1 200 OK')
write_string('Content-type: multipart/x-mixed-replace; '
'boundary={}'.format(MULTIPART_BOUNDARY))
write_string('')
write_string(MULTIPART_BOUNDARY)
# MJPEG_START_HEADER.format()
while True: while True:
img_bytes = self.camera_image() img_bytes = self.camera_image()
if img_bytes is None: if img_bytes is None:
continue continue
headers_str = '\r\n'.join((
'Content-length: {}'.format(len(img_bytes)),
'Content-type: image/jpeg',
)) + '\r\n\r\n'
handler.request.sendall( write_string('Content-length: {}'.format(len(img_bytes)))
bytes(headers_str, 'utf-8') + write_string('Content-type: image/jpeg')
img_bytes + write_string('')
bytes('\r\n', 'utf-8')) handler.request.sendall(img_bytes)
write_string('')
handler.request.sendall( write_string(MULTIPART_BOUNDARY)
bytes('--jpgboundary\r\n', 'utf-8'))
time.sleep(0.5) time.sleep(0.5)

View File

@ -0,0 +1,86 @@
"""Camera platform that has a Raspberry Pi camera."""
import os
import subprocess
import logging
import shutil
from homeassistant.components.camera import Camera
_LOGGER = logging.getLogger(__name__)
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")
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'))
}
)
# check filepath given is writable
if not os.access(setup_config["file_path"], os.W_OK):
_LOGGER.error("Error: file path is not writable")
return False
add_devices([
RaspberryCamera(setup_config)
])
class RaspberryCamera(Camera):
"""Raspberry Pi camera."""
def __init__(self, device_info):
"""Initialize Raspberry Pi camera component."""
super().__init__()
self._name = device_info["name"]
self._config = device_info
# 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"])
]
if device_info["horizontal_flip"]:
cmd_args.append("-hf")
if device_info["vertical_flip"]:
cmd_args.append("-vf")
subprocess.Popen(cmd_args,
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)
def camera_image(self):
"""Return raspstill image response."""
with open(self._config["file_path"], 'rb') as file:
return file.read()
@property
def name(self):
"""Return the name of this camera."""
return self._name

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/conversation/
""" """
import logging import logging
import re import re
import warnings
from homeassistant import core from homeassistant import core
from homeassistant.const import ( from homeassistant.const import (
@ -24,6 +25,7 @@ REQUIREMENTS = ['fuzzywuzzy==0.8.0']
def setup(hass, config): def setup(hass, config):
"""Register the process service.""" """Register the process service."""
warnings.filterwarnings('ignore', module='fuzzywuzzy')
from fuzzywuzzy import process as fuzzyExtract from fuzzywuzzy import process as fuzzyExtract
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -95,18 +95,18 @@ def setup(hass, config):
'demo': { 'demo': {
'alias': 'Toggle {}'.format(lights[0].split('.')[1]), 'alias': 'Toggle {}'.format(lights[0].split('.')[1]),
'sequence': [{ 'sequence': [{
'execute_service': 'light.turn_off', 'service': 'light.turn_off',
'service_data': {ATTR_ENTITY_ID: lights[0]} 'data': {ATTR_ENTITY_ID: lights[0]}
}, { }, {
'delay': {'seconds': 5} 'delay': {'seconds': 5}
}, { }, {
'execute_service': 'light.turn_on', 'service': 'light.turn_on',
'service_data': {ATTR_ENTITY_ID: lights[0]} 'data': {ATTR_ENTITY_ID: lights[0]}
}, { }, {
'delay': {'seconds': 5} 'delay': {'seconds': 5}
}, { }, {
'execute_service': 'light.turn_off', 'service': 'light.turn_off',
'service_data': {ATTR_ENTITY_ID: lights[0]} 'data': {ATTR_ENTITY_ID: lights[0]}
}] }]
}}}) }}})
@ -136,7 +136,7 @@ def setup(hass, config):
'Home Alone']}, 'Home Alone']},
'who_cooks': {'icon': 'mdi:panda', 'who_cooks': {'icon': 'mdi:panda',
'initial': 'Anne Therese', 'initial': 'Anne Therese',
'name': 'Who cooks today', 'name': 'Cook today',
'options': ['Paulus', 'Anne Therese']}}}) 'options': ['Paulus', 'Anne Therese']}}})
# Set up input boolean # Set up input boolean
bootstrap.setup_component( bootstrap.setup_component(
@ -144,6 +144,11 @@ def setup(hass, config):
{'input_boolean': {'notify': {'icon': 'mdi:car', {'input_boolean': {'notify': {'icon': 'mdi:car',
'initial': False, 'initial': False,
'name': 'Notify Anne Therese is home'}}}) 'name': 'Notify Anne Therese is home'}}})
# Set up weblink
bootstrap.setup_component(
hass, 'weblink',
{'weblink': {'entities': [{'name': 'Router',
'url': 'http://192.168.1.1'}]}})
# Setup configurator # Setup configurator
configurator_ids = [] configurator_ids = []

View File

@ -17,6 +17,7 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -129,8 +130,7 @@ def setup(hass, config):
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error setting up platform %s', p_type) _LOGGER.exception('Error setting up platform %s', p_type)
for p_type, p_config in \ for p_type, p_config in config_per_platform(config, DOMAIN):
config_per_platform(config, DOMAIN, _LOGGER):
setup_platform(p_type, p_config) setup_platform(p_type, p_config)
def device_tracker_discovered(service, info): def device_tracker_discovered(service, info):

View File

@ -6,28 +6,27 @@ https://home-assistant.io/components/device_tracker.mqtt/
""" """
import logging import logging
import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant import util from homeassistant.components.mqtt import CONF_QOS
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
CONF_QOS = 'qos'
CONF_DEVICES = 'devices' CONF_DEVICES = 'devices'
DEFAULT_QOS = 0
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic},
})
def setup_scanner(hass, config, see): def setup_scanner(hass, config, see):
"""Setup the MQTT tracker.""" """Setup the MQTT tracker."""
devices = config.get(CONF_DEVICES) devices = config[CONF_DEVICES]
qos = util.convert(config.get(CONF_QOS), int, DEFAULT_QOS) qos = config[CONF_QOS]
if not isinstance(devices, dict):
_LOGGER.error('Expected %s to be a dict, found %s', CONF_DEVICES,
devices)
return False
dev_id_lookup = {} dev_id_lookup = {}

View File

@ -11,6 +11,7 @@ from collections import defaultdict
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME from homeassistant.const import STATE_HOME
from homeassistant.util import convert
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
@ -46,8 +47,8 @@ def setup_scanner(hass, config, see):
return return
if (not isinstance(data, dict) or data.get('_type') != 'location') or ( if (not isinstance(data, dict) or data.get('_type') != 'location') or (
'acc' in data and max_gps_accuracy is not None and data[ max_gps_accuracy is not None and
'acc'] > max_gps_accuracy): convert(data.get('acc'), float, 0.0) > max_gps_accuracy):
return return
dev_id, kwargs = _parse_see_args(topic, data) dev_id, kwargs = _parse_see_args(topic, data)
@ -79,6 +80,11 @@ def setup_scanner(hass, config, see):
if not isinstance(data, dict) or data.get('_type') != 'transition': if not isinstance(data, dict) or data.get('_type') != 'transition':
return return
if data.get('desc') is None:
_LOGGER.error(
"Location missing from `enter/exit` message - "
"please turn `Share` on in OwnTracks app")
return
# OwnTracks uses - at the start of a beacon zone # OwnTracks uses - at the start of a beacon zone
# to switch on 'hold mode' - ignore this # to switch on 'hold mode' - ignore this
location = data['desc'].lstrip("-") location = data['desc'].lstrip("-")

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED) EVENT_PLATFORM_DISCOVERED)
DOMAIN = "discovery" DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.5.5'] REQUIREMENTS = ['netdisco==0.6.1']
SCAN_INTERVAL = 300 # seconds SCAN_INTERVAL = 300 # seconds
@ -26,6 +26,7 @@ SERVICE_NETGEAR = 'netgear_router'
SERVICE_SONOS = 'sonos' SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver' SERVICE_PLEX = 'plex_mediaserver'
SERVICE_SQUEEZEBOX = 'logitech_mediaserver' SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
SERVICE_PANASONIC_VIERA = 'panasonic_viera'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_WEMO: "wemo", SERVICE_WEMO: "wemo",
@ -35,6 +36,7 @@ SERVICE_HANDLERS = {
SERVICE_SONOS: 'media_player', SERVICE_SONOS: 'media_player',
SERVICE_PLEX: 'media_player', SERVICE_PLEX: 'media_player',
SERVICE_SQUEEZEBOX: 'media_player', SERVICE_SQUEEZEBOX: 'media_player',
SERVICE_PANASONIC_VIERA: 'media_player',
} }

View File

@ -66,10 +66,6 @@ def _handle_get_api_bootstrap(handler, path_match, data):
def _handle_get_root(handler, path_match, data): def _handle_get_root(handler, path_match, data):
"""Render the frontend.""" """Render the frontend."""
handler.send_response(HTTP_OK)
handler.send_header('Content-type', 'text/html; charset=utf-8')
handler.end_headers()
if handler.server.development: if handler.server.development:
app_url = "home-assistant-polymer/src/home-assistant.html" app_url = "home-assistant-polymer/src/home-assistant.html"
else: else:
@ -86,7 +82,9 @@ def _handle_get_root(handler, path_match, data):
template_html = template_html.replace('{{ auth }}', auth) template_html = template_html.replace('{{ auth }}', auth)
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION) template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
handler.wfile.write(template_html.encode("UTF-8")) handler.send_response(HTTP_OK)
handler.write_content(template_html.encode("UTF-8"),
'text/html; charset=utf-8')
def _handle_get_service_worker(handler, path_match, data): def _handle_get_service_worker(handler, path_match, data):

View File

@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script.""" """DO NOT MODIFY. Auto-generated by build_frontend script."""
VERSION = "49974cb3bb443751f7548e4e3b353304" VERSION = "4062ab87999fd5e382074ba9d7a880d7"

View File

@ -0,0 +1 @@
Copyright 2011 Google Inc. All Rights Reserved.

View File

@ -0,0 +1,17 @@
<p>Roboto has a dual nature. It has a mechanical skeleton and the forms are
largely geometric. At the same time, the font features friendly and open
curves. While some grotesks distort their letterforms to force a rigid rhythm,
Roboto doesnt compromise, allowing letters to be settled into their natural
width. This makes for a more natural reading rhythm more commonly found in
humanist and serif types.</p>
<p>This is the normal family, which can be used alongside the
<a href="http://www.google.com/fonts/specimen/Roboto+Condensed">Roboto Condensed</a> family and the
<a href="http://www.google.com/fonts/specimen/Roboto+Slab">Roboto Slab</a> family.</p>
<p>
<b>Updated January 14 2015:</b>
Christian Robertson and the Material Design team unveiled the latest version of Roboto at Google I/O last year, and it is now available from Google Fonts.
Existing websites using Roboto via Google Fonts will start using the latest version automatically.
If you have installed the fonts on your computer, please download them again and re-install.
</p>

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,129 @@
{
"name": "Roboto",
"designer": "Christian Robertson",
"license": "Apache2",
"visibility": "External",
"category": "Sans Serif",
"size": 86523,
"fonts": [
{
"name": "Roboto",
"style": "normal",
"weight": 100,
"filename": "Roboto-Thin.ttf",
"postScriptName": "Roboto-Thin",
"fullName": "Roboto Thin",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "italic",
"weight": 100,
"filename": "Roboto-ThinItalic.ttf",
"postScriptName": "Roboto-ThinItalic",
"fullName": "Roboto Thin Italic",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "normal",
"weight": 300,
"filename": "Roboto-Light.ttf",
"postScriptName": "Roboto-Light",
"fullName": "Roboto Light",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "italic",
"weight": 300,
"filename": "Roboto-LightItalic.ttf",
"postScriptName": "Roboto-LightItalic",
"fullName": "Roboto Light Italic",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "normal",
"weight": 400,
"filename": "Roboto-Regular.ttf",
"postScriptName": "Roboto-Regular",
"fullName": "Roboto",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "italic",
"weight": 400,
"filename": "Roboto-Italic.ttf",
"postScriptName": "Roboto-Italic",
"fullName": "Roboto Italic",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "normal",
"weight": 500,
"filename": "Roboto-Medium.ttf",
"postScriptName": "Roboto-Medium",
"fullName": "Roboto Medium",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "italic",
"weight": 500,
"filename": "Roboto-MediumItalic.ttf",
"postScriptName": "Roboto-MediumItalic",
"fullName": "Roboto Medium Italic",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "normal",
"weight": 700,
"filename": "Roboto-Bold.ttf",
"postScriptName": "Roboto-Bold",
"fullName": "Roboto Bold",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "italic",
"weight": 700,
"filename": "Roboto-BoldItalic.ttf",
"postScriptName": "Roboto-BoldItalic",
"fullName": "Roboto Bold Italic",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "normal",
"weight": 900,
"filename": "Roboto-Black.ttf",
"postScriptName": "Roboto-Black",
"fullName": "Roboto Black",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
},
{
"name": "Roboto",
"style": "italic",
"weight": 900,
"filename": "Roboto-BlackItalic.ttf",
"postScriptName": "Roboto-BlackItalic",
"fullName": "Roboto Black Italic",
"copyright": "Copyright 2011 Google Inc. All Rights Reserved."
}
],
"subsets": [
"cyrillic",
"cyrillic-ext",
"greek",
"greek-ext",
"latin",
"latin-ext",
"menu",
"vietnamese"
],
"dateAdded": "2013-01-09"
}

View File

@ -0,0 +1,17 @@
<p>
Roboto Mono is a monospaced addition to the <a href="https://www.google.com/fonts/specimen/Roboto">Roboto</a> type family.
Like the other members of the Roboto family, the fonts are optimized for readability on screens across a wide variety of devices and reading environments.
While the monospaced version is related to its variable width cousin, it doesn&#8217;t hesitate to change forms to better fit the constraints of a monospaced environment.
For example, narrow glyphs like &#8216;I&#8217;, &#8216;l&#8217; and &#8216;i&#8217; have added serifs for more even texture while wider glyphs are adjusted for weight.
Curved caps like &#8216;C&#8217; and &#8216;O&#8217; take on the straighter sides from Roboto Condensed.
</p>
<p>
Special consideration is given to glyphs important for reading and writing software source code.
Letters with similar shapes are easy to tell apart.
Digit &#8216;1&#8217;, lowercase &#8216;l&#8217; and capital &#8216;I&#8217; are easily differentiated as are zero and the letter &#8216;O&#8217;.
Punctuation important for code has also been considered.
For example, the curly braces &#8216;{&nbsp;}&#8217; have exaggerated points to clearly differentiate them from parenthesis &#8216;(&nbsp;)&#8217; and braces &#8216;[&nbsp;]&#8217;.
Periods and commas are also exaggerated to identify them more quickly.
The scale and weight of symbols commonly used as operators have also been optimized.
</p>

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,111 @@
{
"name": "Roboto Mono",
"designer": "Christian Robertson",
"license": "Apache2",
"visibility": "External",
"category": "Monospace",
"size": 51290,
"fonts": [
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-Thin",
"fullName": "Roboto Mono Thin",
"style": "normal",
"weight": 100,
"filename": "RobotoMono-Thin.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
},
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-ThinItalic",
"fullName": "Roboto Mono Thin Italic",
"style": "italic",
"weight": 100,
"filename": "RobotoMono-ThinItalic.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
},
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-Light",
"fullName": "Roboto Mono Light",
"style": "normal",
"weight": 300,
"filename": "RobotoMono-Light.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
},
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-LightItalic",
"fullName": "Roboto Mono Light Italic",
"style": "italic",
"weight": 300,
"filename": "RobotoMono-LightItalic.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
},
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-Regular",
"fullName": "Roboto Mono",
"style": "normal",
"weight": 400,
"filename": "RobotoMono-Regular.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
},
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-Italic",
"fullName": "Roboto Mono Italic",
"style": "italic",
"weight": 400,
"filename": "RobotoMono-Italic.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
},
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-Medium",
"fullName": "Roboto Mono Medium",
"style": "normal",
"weight": 500,
"filename": "RobotoMono-Medium.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
},
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-MediumItalic",
"fullName": "Roboto Mono Medium Italic",
"style": "italic",
"weight": 500,
"filename": "RobotoMono-MediumItalic.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
},
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-Bold",
"fullName": "Roboto Mono Bold",
"style": "normal",
"weight": 700,
"filename": "RobotoMono-Bold.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
},
{
"name": "Roboto Mono",
"postScriptName": "RobotoMono-BoldItalic",
"fullName": "Roboto Mono Bold Italic",
"style": "italic",
"weight": 700,
"filename": "RobotoMono-BoldItalic.ttf",
"copyright": "Copyright 2015 Google Inc. All Rights Reserved."
}
],
"subsets": [
"cyrillic",
"cyrillic-ext",
"greek",
"greek-ext",
"latin",
"latin-ext",
"menu",
"vietnamese"
],
"dateAdded": "2015-05-13"
}

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit a4217e0f620366e47dde82f7ce2d8f7b2bb6a079 Subproject commit 89a1723f9f5fdaf5b144222b82b73995200ed339

View File

@ -10,7 +10,7 @@ import os
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import ( from homeassistant.const import (
STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN, STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)

View File

@ -5,6 +5,9 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/group/ https://home-assistant.io/components/group/
""" """
import threading import threading
from collections import OrderedDict
import voluptuous as vol
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.const import ( from homeassistant.const import (
@ -14,6 +17,7 @@ from homeassistant.const import (
from homeassistant.helpers.entity import ( from homeassistant.helpers.entity import (
Entity, generate_entity_id, split_entity_id) Entity, generate_entity_id, split_entity_id)
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv
DOMAIN = 'group' DOMAIN = 'group'
@ -26,6 +30,38 @@ ATTR_AUTO = 'auto'
ATTR_ORDER = 'order' ATTR_ORDER = 'order'
ATTR_VIEW = 'view' ATTR_VIEW = 'view'
def _conf_preprocess(value):
"""Preprocess alternative configuration formats."""
if isinstance(value, (str, list)):
value = {CONF_ENTITIES: value}
return value
_SINGLE_GROUP_CONFIG = vol.Schema(vol.All(_conf_preprocess, {
vol.Required(CONF_ENTITIES): cv.entity_ids,
CONF_VIEW: bool,
CONF_NAME: str,
CONF_ICON: cv.icon,
}))
def _group_dict(value):
"""Validate a dictionary of group definitions."""
config = OrderedDict()
for key, group in value.items():
try:
config[key] = _SINGLE_GROUP_CONFIG(group)
except vol.MultipleInvalid as ex:
raise vol.Invalid('Group {} is invalid: {}'.format(key, ex))
return config
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(dict, _group_dict)
}, extra=vol.ALLOW_EXTRA)
# List of ON/OFF state tuples for groupable states # List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME), _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
(STATE_OPEN, STATE_CLOSED)] (STATE_OPEN, STATE_CLOSED)]
@ -108,17 +144,11 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
def setup(hass, config): def setup(hass, config):
"""Setup all groups found definded in the configuration.""" """Setup all groups found definded in the configuration."""
for object_id, conf in config.get(DOMAIN, {}).items(): for object_id, conf in config.get(DOMAIN, {}).items():
if not isinstance(conf, dict):
conf = {CONF_ENTITIES: conf}
name = conf.get(CONF_NAME, object_id) name = conf.get(CONF_NAME, object_id)
entity_ids = conf.get(CONF_ENTITIES) entity_ids = conf[CONF_ENTITIES]
icon = conf.get(CONF_ICON) icon = conf.get(CONF_ICON)
view = conf.get(CONF_VIEW) view = conf.get(CONF_VIEW)
if isinstance(entity_ids, str):
entity_ids = [ent.strip() for ent in entity_ids.split(",")]
Group(hass, name, entity_ids, icon=icon, view=view, Group(hass, name, entity_ids, icon=icon, view=view,
object_id=object_id) object_id=object_id)

View File

@ -7,7 +7,6 @@ https://home-assistant.io/developers/api/
import gzip import gzip
import json import json
import logging import logging
import os
import ssl import ssl
import threading import threading
import time import time
@ -78,7 +77,9 @@ def setup(hass, config):
name='HTTP-server').start()) name='HTTP-server').start())
hass.http = server hass.http = server
hass.config.api = rem.API(util.get_local_ip(), api_password, server_port, hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
else util.get_local_ip(),
api_password, server_port,
ssl_certificate is not None) ssl_certificate is not None)
return True return True
@ -164,6 +165,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
# Track if this was an authenticated request # Track if this was an authenticated request
self.authenticated = False self.authenticated = False
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server) SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
self.protocol_version = 'HTTP/1.1'
def log_message(self, fmt, *arguments): def log_message(self, fmt, *arguments):
"""Redirect built-in log to HA logging.""" """Redirect built-in log to HA logging."""
@ -240,7 +242,9 @@ class RequestHandler(SimpleHTTPRequestHandler):
msg = "API password missing or incorrect." msg = "API password missing or incorrect."
if require_auth and not self.authenticated: if require_auth and not self.authenticated:
self.write_json_message(msg, HTTP_UNAUTHORIZED) self.write_json_message(msg, HTTP_UNAUTHORIZED)
_LOGGER.warning(msg) _LOGGER.warning('%s Source IP: %s',
msg,
self.client_address[0])
return return
handle_request_method(self, path_match, data) handle_request_method(self, path_match, data)
@ -282,31 +286,21 @@ class RequestHandler(SimpleHTTPRequestHandler):
json_data = json.dumps(data, indent=4, sort_keys=True, json_data = json.dumps(data, indent=4, sort_keys=True,
cls=rem.JSONEncoder).encode('UTF-8') cls=rem.JSONEncoder).encode('UTF-8')
self.send_response(status_code) self.send_response(status_code)
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON)
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(json_data)))
if location: if location:
self.send_header('Location', location) self.send_header('Location', location)
self.set_session_cookie_header() self.set_session_cookie_header()
self.end_headers() self.write_content(json_data, CONTENT_TYPE_JSON)
if data is not None:
self.wfile.write(json_data)
def write_text(self, message, status_code=HTTP_OK): def write_text(self, message, status_code=HTTP_OK):
"""Helper method to return a text message to the caller.""" """Helper method to return a text message to the caller."""
msg_data = message.encode('UTF-8') msg_data = message.encode('UTF-8')
self.send_response(status_code) self.send_response(status_code)
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(msg_data)))
self.set_session_cookie_header() self.set_session_cookie_header()
self.end_headers() self.write_content(msg_data, CONTENT_TYPE_TEXT_PLAIN)
self.wfile.write(msg_data)
def write_file(self, path, cache_headers=True): def write_file(self, path, cache_headers=True):
"""Return a file to the user.""" """Return a file to the user."""
@ -322,36 +316,32 @@ class RequestHandler(SimpleHTTPRequestHandler):
def write_file_pointer(self, content_type, inp, cache_headers=True): def write_file_pointer(self, content_type, inp, cache_headers=True):
"""Helper function to write a file pointer to the user.""" """Helper function to write a file pointer to the user."""
do_gzip = 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, '')
self.send_response(HTTP_OK) self.send_response(HTTP_OK)
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
if cache_headers: if cache_headers:
self.set_cache_header() self.set_cache_header()
self.set_session_cookie_header() self.set_session_cookie_header()
if do_gzip: self.write_content(inp.read(), content_type)
gzip_data = gzip.compress(inp.read())
def write_content(self, content, content_type=None):
"""Helper method to write content bytes to output stream."""
if content_type is not None:
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
if 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, ''):
content = gzip.compress(content)
self.send_header(HTTP_HEADER_CONTENT_ENCODING, "gzip") self.send_header(HTTP_HEADER_CONTENT_ENCODING, "gzip")
self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING) self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING)
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(gzip_data)))
else:
fst = os.fstat(inp.fileno())
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(fst[6]))
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(content)))
self.end_headers() self.end_headers()
if self.command == 'HEAD': if self.command == 'HEAD':
return return
elif do_gzip: self.wfile.write(content)
self.wfile.write(gzip_data)
else:
self.copyfile(inp, self.wfile)
def set_cache_header(self): def set_cache_header(self):
"""Add cache headers if not in development.""" """Add cache headers if not in development."""

View File

@ -8,6 +8,8 @@ import logging
import os import os
import csv import csv
import voluptuous as vol
from homeassistant.components import ( from homeassistant.components import (
group, discovery, wemo, wink, isy994, group, discovery, wemo, wink, isy994,
zwave, insteon_hub, mysensors, tellstick, vera) zwave, insteon_hub, mysensors, tellstick, vera)
@ -17,7 +19,8 @@ from homeassistant.const import (
ATTR_ENTITY_ID) ATTR_ENTITY_ID)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
import homeassistant.util as util from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
@ -76,6 +79,37 @@ PROP_TO_ATTR = {
'xy_color': ATTR_XY_COLOR, 'xy_color': ATTR_XY_COLOR,
} }
# Service call validation schemas
VALID_TRANSITION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900))
LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_PROFILE: str,
ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: cv.byte,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=154, max=500)),
ATTR_FLASH: [FLASH_SHORT, FLASH_LONG],
ATTR_EFFECT: [EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE],
})
LIGHT_TURN_OFF_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_TRANSITION: VALID_TRANSITION,
})
LIGHT_TOGGLE_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_TRANSITION: VALID_TRANSITION,
})
PROFILE_SCHEMA = vol.Schema(
vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte))
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -155,31 +189,22 @@ def setup(hass, config):
next(reader, None) next(reader, None)
try: try:
for profile_id, color_x, color_y, brightness in reader: for rec in reader:
profiles[profile_id] = (float(color_x), float(color_y), profile, color_x, color_y, brightness = PROFILE_SCHEMA(rec)
int(brightness)) profiles[profile] = (color_x, color_y, brightness)
except ValueError: except vol.MultipleInvalid as ex:
# ValueError if not 4 values per row _LOGGER.error("Error parsing light profile from %s: %s",
# ValueError if convert to float/int failed profile_path, ex)
_LOGGER.error(
"Error parsing light profiles from %s", profile_path)
return False return False
def handle_light_service(service): def handle_light_service(service):
"""Hande a turn light on or off service call.""" """Hande a turn light on or off service call."""
# Get and validate data # Get the validated data
dat = service.data params = service.data.copy()
# Convert the entity ids to valid light ids # Convert the entity ids to valid light ids
target_lights = component.extract_from_service(service) target_lights = component.extract_from_service(service)
params.pop(ATTR_ENTITY_ID, None)
params = {}
transition = util.convert(dat.get(ATTR_TRANSITION), int)
if transition is not None:
params[ATTR_TRANSITION] = transition
service_fun = None service_fun = None
if service.service == SERVICE_TURN_OFF: if service.service == SERVICE_TURN_OFF:
@ -197,63 +222,11 @@ def setup(hass, config):
return return
# Processing extra data for turn light on request. # Processing extra data for turn light on request.
profile = profiles.get(params.pop(ATTR_PROFILE, None))
# We process the profile first so that we get the desired
# behavior that extra service data attributes overwrite
# profile values.
profile = profiles.get(dat.get(ATTR_PROFILE))
if profile: if profile:
*params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, profile[2])
if ATTR_BRIGHTNESS in dat:
# We pass in the old value as the default parameter if parsing
# of the new one goes wrong.
params[ATTR_BRIGHTNESS] = util.convert(
dat.get(ATTR_BRIGHTNESS), int, params.get(ATTR_BRIGHTNESS))
if ATTR_XY_COLOR in dat:
try:
# xy_color should be a list containing 2 floats.
xycolor = dat.get(ATTR_XY_COLOR)
# Without this check, a xycolor with value '99' would work.
if not isinstance(xycolor, str):
params[ATTR_XY_COLOR] = [float(val) for val in xycolor]
except (TypeError, ValueError):
# TypeError if xy_color is not iterable
# ValueError if value could not be converted to float
pass
if ATTR_COLOR_TEMP in dat:
# color_temp should be an int of mireds value
colortemp = dat.get(ATTR_COLOR_TEMP)
# Without this check, a ctcolor with value '99' would work
# These values are based on Philips Hue, may need ajustment later
if isinstance(colortemp, int) and 154 <= colortemp <= 500:
params[ATTR_COLOR_TEMP] = colortemp
if ATTR_RGB_COLOR in dat:
try:
# rgb_color should be a list containing 3 ints
rgb_color = dat.get(ATTR_RGB_COLOR)
if len(rgb_color) == 3:
params[ATTR_RGB_COLOR] = [int(val) for val in rgb_color]
except (TypeError, ValueError):
# TypeError if rgb_color is not iterable
# ValueError if not all values can be converted to int
pass
if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG):
params[ATTR_FLASH] = dat[ATTR_FLASH]
if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE,
EFFECT_RANDOM):
params[ATTR_EFFECT] = dat[ATTR_EFFECT]
for light in target_lights: for light in target_lights:
light.turn_on(**params) light.turn_on(**params)
@ -266,13 +239,16 @@ def setup(hass, config):
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service, hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service,
descriptions.get(SERVICE_TURN_ON)) descriptions.get(SERVICE_TURN_ON),
schema=LIGHT_TURN_ON_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service, hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
descriptions.get(SERVICE_TURN_OFF)) descriptions.get(SERVICE_TURN_OFF),
schema=LIGHT_TURN_OFF_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_light_service, hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_light_service,
descriptions.get(SERVICE_TOGGLE)) descriptions.get(SERVICE_TOGGLE),
schema=LIGHT_TOGGLE_SCHEMA)
return True return True

View File

@ -7,46 +7,85 @@ https://home-assistant.io/components/light.mqtt/
import logging import logging
from functools import partial from functools import partial
import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, Light) ATTR_BRIGHTNESS, ATTR_RGB_COLOR, Light)
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.template import render_with_possible_json_value from homeassistant.helpers.template import render_with_possible_json_value
from homeassistant.util import convert
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mqtt']
CONF_STATE_VALUE_TEMPLATE = 'state_value_template'
CONF_BRIGHTNESS_STATE_TOPIC = 'brightness_state_topic'
CONF_BRIGHTNESS_COMMAND_TOPIC = 'brightness_command_topic'
CONF_BRIGHTNESS_VALUE_TEMPLATE = 'brightness_value_template'
CONF_RGB_STATE_TOPIC = 'rgb_state_topic'
CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic'
CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template'
CONF_PAYLOAD_ON = 'payload_on'
CONF_PAYLOAD_OFF = 'payload_off'
CONF_BRIGHTNESS_SCALE = 'brightness_scale'
DEFAULT_NAME = 'MQTT Light' DEFAULT_NAME = 'MQTT Light'
DEFAULT_QOS = 0
DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_OPTIMISTIC = False DEFAULT_OPTIMISTIC = False
DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_BRIGHTNESS_SCALE = 255
DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE):
vol.All(vol.Coerce(int), vol.Range(min=1)),
})
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Add MQTT Light.""" """Add MQTT Light."""
if config.get('command_topic') is None: config.setdefault(CONF_STATE_VALUE_TEMPLATE,
_LOGGER.error("Missing required variable: command_topic") config.get(CONF_VALUE_TEMPLATE))
return False
add_devices_callback([MqttLight( add_devices_callback([MqttLight(
hass, hass,
convert(config.get('name'), str, DEFAULT_NAME), config[CONF_NAME],
{key: convert(config.get(key), str) for key in
(typ + topic
for typ in ('', 'brightness_', 'rgb_')
for topic in ('state_topic', 'command_topic'))},
{key: convert(config.get(key + '_value_template'), str)
for key in ('state', 'brightness', 'rgb')},
convert(config.get('qos'), int, DEFAULT_QOS),
{ {
'on': convert(config.get('payload_on'), str, DEFAULT_PAYLOAD_ON), key: config.get(key) for key in (
'off': convert(config.get('payload_off'), str, DEFAULT_PAYLOAD_OFF) CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC,
CONF_BRIGHTNESS_STATE_TOPIC,
CONF_BRIGHTNESS_COMMAND_TOPIC,
CONF_RGB_STATE_TOPIC,
CONF_RGB_COMMAND_TOPIC,
)
}, },
convert(config.get('optimistic'), bool, DEFAULT_OPTIMISTIC), {
convert(config.get('brightness_scale'), int, DEFAULT_BRIGHTNESS_SCALE) 'state': config.get(CONF_STATE_VALUE_TEMPLATE),
'brightness': config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE),
'rgb': config.get(CONF_RGB_VALUE_TEMPLATE)
},
config[CONF_QOS],
config[CONF_RETAIN],
{
'on': config[CONF_PAYLOAD_ON],
'off': config[CONF_PAYLOAD_OFF],
},
config[CONF_OPTIMISTIC],
config[CONF_BRIGHTNESS_SCALE],
)]) )])
@ -54,13 +93,14 @@ class MqttLight(Light):
"""MQTT light.""" """MQTT light."""
# pylint: disable=too-many-arguments,too-many-instance-attributes # pylint: disable=too-many-arguments,too-many-instance-attributes
def __init__(self, hass, name, topic, templates, qos, payload, optimistic, def __init__(self, hass, name, topic, templates, qos, retain, payload,
brightness_scale): optimistic, brightness_scale):
"""Initialize MQTT light.""" """Initialize MQTT light."""
self._hass = hass self._hass = hass
self._name = name self._name = name
self._topic = topic self._topic = topic
self._qos = qos self._qos = qos
self._retain = retain
self._payload = payload self._payload = payload
self._optimistic = optimistic or topic["state_topic"] is None self._optimistic = optimistic or topic["state_topic"] is None
self._optimistic_rgb = optimistic or topic["rgb_state_topic"] is None self._optimistic_rgb = optimistic or topic["rgb_state_topic"] is None
@ -156,7 +196,8 @@ class MqttLight(Light):
self._topic["rgb_command_topic"] is not None: self._topic["rgb_command_topic"] is not None:
mqtt.publish(self._hass, self._topic["rgb_command_topic"], mqtt.publish(self._hass, self._topic["rgb_command_topic"],
"{},{},{}".format(*kwargs[ATTR_RGB_COLOR]), self._qos) "{},{},{}".format(*kwargs[ATTR_RGB_COLOR]),
self._qos, self._retain)
if self._optimistic_rgb: if self._optimistic_rgb:
self._rgb = kwargs[ATTR_RGB_COLOR] self._rgb = kwargs[ATTR_RGB_COLOR]
@ -167,14 +208,14 @@ class MqttLight(Light):
percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255
device_brightness = int(percent_bright * self._brightness_scale) device_brightness = int(percent_bright * self._brightness_scale)
mqtt.publish(self._hass, self._topic["brightness_command_topic"], mqtt.publish(self._hass, self._topic["brightness_command_topic"],
device_brightness, self._qos) device_brightness, self._qos, self._retain)
if self._optimistic_brightness: if self._optimistic_brightness:
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]
should_update = True should_update = True
mqtt.publish(self._hass, self._topic["command_topic"], mqtt.publish(self._hass, self._topic["command_topic"],
self._payload["on"], self._qos) self._payload["on"], self._qos, self._retain)
if self._optimistic: if self._optimistic:
# Optimistically assume that switch has changed state. # Optimistically assume that switch has changed state.
@ -187,7 +228,7 @@ class MqttLight(Light):
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
mqtt.publish(self._hass, self._topic["command_topic"], mqtt.publish(self._hass, self._topic["command_topic"],
self._payload["off"], self._qos) self._payload["off"], self._qos, self._retain)
if self._optimistic: if self._optimistic:
# Optimistically assume that switch has changed state. # Optimistically assume that switch has changed state.

View File

@ -8,40 +8,19 @@ import logging
import homeassistant.components.rfxtrx as rfxtrx import homeassistant.components.rfxtrx as rfxtrx
from homeassistant.components.light import ATTR_BRIGHTNESS, Light from homeassistant.components.light import ATTR_BRIGHTNESS, Light
from homeassistant.components.rfxtrx import (
ATTR_FIREEVENT, ATTR_NAME, ATTR_PACKETID, ATTR_STATE, EVENT_BUTTON_PRESSED)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.util import slugify
DEPENDENCIES = ['rfxtrx'] DEPENDENCIES = ['rfxtrx']
SIGNAL_REPETITIONS = 1
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the RFXtrx platform.""" """Setup the RFXtrx platform."""
import RFXtrx as rfxtrxmod import RFXtrx as rfxtrxmod
lights = [] lights = rfxtrx.get_devices_from_config(config, RfxtrxLight)
signal_repetitions = config.get('signal_repetitions', SIGNAL_REPETITIONS)
for device_id, entity_info in config.get('devices', {}).items():
if device_id in rfxtrx.RFX_DEVICES:
continue
_LOGGER.info("Add %s rfxtrx.light", entity_info[ATTR_NAME])
# Check if i must fire event
fire_event = entity_info.get(ATTR_FIREEVENT, False)
datas = {ATTR_STATE: False, ATTR_FIREEVENT: fire_event}
rfxobject = rfxtrx.get_rfx_object(entity_info[ATTR_PACKETID])
new_light = RfxtrxLight(
entity_info[ATTR_NAME], rfxobject, datas,
signal_repetitions)
rfxtrx.RFX_DEVICES[device_id] = new_light
lights.append(new_light)
add_devices_callback(lights) add_devices_callback(lights)
def light_update(event): def light_update(event):
@ -50,141 +29,32 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
not event.device.known_to_be_dimmable: not event.device.known_to_be_dimmable:
return return
# Add entity if not exist and the automatic_add is True new_device = rfxtrx.get_new_device(event, config, RfxtrxLight)
device_id = slugify(event.device.id_string.lower()) if new_device:
if device_id not in rfxtrx.RFX_DEVICES: add_devices_callback([new_device])
automatic_add = config.get('automatic_add', False)
if not automatic_add:
return
_LOGGER.info( rfxtrx.apply_received_command(event)
"Automatic add %s rfxtrx.light (Class: %s Sub: %s)",
device_id,
event.device.__class__.__name__,
event.device.subtype
)
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
entity_name = "%s : %s" % (device_id, pkt_id)
datas = {ATTR_STATE: False, ATTR_FIREEVENT: False}
signal_repetitions = config.get('signal_repetitions',
SIGNAL_REPETITIONS)
new_light = RfxtrxLight(entity_name, event, datas,
signal_repetitions)
rfxtrx.RFX_DEVICES[device_id] = new_light
add_devices_callback([new_light])
# Check if entity exists or previously added automatically
if device_id in rfxtrx.RFX_DEVICES:
_LOGGER.debug(
"EntityID: %s light_update. Command: %s",
device_id,
event.values['Command']
)
if event.values['Command'] == 'On'\
or event.values['Command'] == 'Off':
# Update the rfxtrx device state
is_on = event.values['Command'] == 'On'
# pylint: disable=protected-access
rfxtrx.RFX_DEVICES[device_id]._state = is_on
rfxtrx.RFX_DEVICES[device_id].update_ha_state()
elif event.values['Command'] == 'Set level':
# pylint: disable=protected-access
rfxtrx.RFX_DEVICES[device_id]._brightness = \
(event.values['Dim level'] * 255 // 100)
# Update the rfxtrx device state
is_on = rfxtrx.RFX_DEVICES[device_id]._brightness > 0
rfxtrx.RFX_DEVICES[device_id]._state = is_on
rfxtrx.RFX_DEVICES[device_id].update_ha_state()
else:
return
# Fire event
if rfxtrx.RFX_DEVICES[device_id].should_fire_event:
rfxtrx.RFX_DEVICES[device_id].hass.bus.fire(
EVENT_BUTTON_PRESSED, {
ATTR_ENTITY_ID:
rfxtrx.RFX_DEVICES[device_id].device_id,
ATTR_STATE: event.values['Command'].lower()
}
)
# Subscribe to main rfxtrx events # Subscribe to main rfxtrx events
if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update) rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update)
class RfxtrxLight(Light): class RfxtrxLight(rfxtrx.RfxtrxDevice, Light):
"""Represenation of a RFXtrx light.""" """Represenation of a RFXtrx light."""
def __init__(self, name, event, datas, signal_repetitions):
"""Initialize the light."""
self._name = name
self._event = event
self._state = datas[ATTR_STATE]
self._should_fire_event = datas[ATTR_FIREEVENT]
self.signal_repetitions = signal_repetitions
self._brightness = 0
@property
def should_poll(self):
"""No polling needed for a light."""
return False
@property
def name(self):
"""Return the name of the light if any."""
return self._name
@property
def should_fire_event(self):
"""Return true if the device must fire event."""
return self._should_fire_event
@property
def is_on(self):
"""Return true if light is on."""
return self._state
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return self._brightness return self._brightness
@property
def assumed_state(self):
"""Return True if unable to access real state of entity."""
return True
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the light on.""" """Turn the light on."""
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = kwargs.get(ATTR_BRIGHTNESS)
if not self._event:
return
if brightness is None: if brightness is None:
self._brightness = 255 self._brightness = 255
for _ in range(self.signal_repetitions): self._send_command("turn_on")
self._event.device.send_on(rfxtrx.RFXOBJECT.transport)
else: else:
self._brightness = brightness self._brightness = brightness
_brightness = (brightness * 100 // 255) _brightness = (brightness * 100 // 255)
for _ in range(self.signal_repetitions): self._send_command("dim", _brightness)
self._event.device.send_dim(rfxtrx.RFXOBJECT.transport,
_brightness)
self._state = True
self.update_ha_state()
def turn_off(self, **kwargs):
"""Turn the light off."""
if not self._event:
return
for _ in range(self.signal_repetitions):
self._event.device.send_off(rfxtrx.RFXOBJECT.transport)
self._brightness = 0
self._state = False
self.update_ha_state()

View File

@ -11,7 +11,7 @@ import os
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)

View File

@ -6,40 +6,51 @@ https://home-assistant.io/components/lock.mqtt/
""" """
import logging import logging
import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice
from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mqtt']
CONF_PAYLOAD_LOCK = 'payload_lock'
CONF_PAYLOAD_UNLOCK = 'payload_unlock'
DEFAULT_NAME = "MQTT Lock" DEFAULT_NAME = "MQTT Lock"
DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_LOCK = "LOCK"
DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK"
DEFAULT_QOS = 0
DEFAULT_OPTIMISTIC = False DEFAULT_OPTIMISTIC = False
DEFAULT_RETAIN = False
DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK):
cv.string,
vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK):
cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the MQTT lock.""" """Setup the MQTT lock."""
if config.get('command_topic') is None:
_LOGGER.error("Missing required variable: command_topic")
return False
add_devices_callback([MqttLock( add_devices_callback([MqttLock(
hass, hass,
config.get('name', DEFAULT_NAME), config[CONF_NAME],
config.get('state_topic'), config.get(CONF_STATE_TOPIC),
config.get('command_topic'), config[CONF_COMMAND_TOPIC],
config.get('qos', DEFAULT_QOS), config[CONF_QOS],
config.get('retain', DEFAULT_RETAIN), config[CONF_RETAIN],
config.get('payload_lock', DEFAULT_PAYLOAD_LOCK), config[CONF_PAYLOAD_LOCK],
config.get('payload_unlock', DEFAULT_PAYLOAD_UNLOCK), config[CONF_PAYLOAD_UNLOCK],
config.get('optimistic', DEFAULT_OPTIMISTIC), config[CONF_OPTIMISTIC],
config.get(CONF_VALUE_TEMPLATE))]) config.get(CONF_VALUE_TEMPLATE))])

View File

@ -7,10 +7,14 @@ https://home-assistant.io/components/media_player/
import logging import logging
import os import os
import voluptuous as vol
from homeassistant.components import discovery from homeassistant.components import discovery
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE, STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
@ -31,9 +35,11 @@ DISCOVERY_PLATFORMS = {
discovery.SERVICE_SONOS: 'sonos', discovery.SERVICE_SONOS: 'sonos',
discovery.SERVICE_PLEX: 'plex', discovery.SERVICE_PLEX: 'plex',
discovery.SERVICE_SQUEEZEBOX: 'squeezebox', discovery.SERVICE_SQUEEZEBOX: 'squeezebox',
discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera',
} }
SERVICE_PLAY_MEDIA = 'play_media' SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted' ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
@ -54,6 +60,7 @@ ATTR_MEDIA_PLAYLIST = 'media_playlist'
ATTR_APP_ID = 'app_id' ATTR_APP_ID = 'app_id'
ATTR_APP_NAME = 'app_name' ATTR_APP_NAME = 'app_name'
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands' ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
ATTR_INPUT_SOURCE = 'source'
MEDIA_TYPE_MUSIC = 'music' MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow' MEDIA_TYPE_TVSHOW = 'tvshow'
@ -73,7 +80,9 @@ SUPPORT_TURN_ON = 128
SUPPORT_TURN_OFF = 256 SUPPORT_TURN_OFF = 256
SUPPORT_PLAY_MEDIA = 512 SUPPORT_PLAY_MEDIA = 512
SUPPORT_VOLUME_STEP = 1024 SUPPORT_VOLUME_STEP = 1024
SUPPORT_SELECT_SOURCE = 2048
# simple services that only take entity_id(s) as optional argument
SERVICE_TO_METHOD = { SERVICE_TO_METHOD = {
SERVICE_TURN_ON: 'turn_on', SERVICE_TURN_ON: 'turn_on',
SERVICE_TURN_OFF: 'turn_off', SERVICE_TURN_OFF: 'turn_off',
@ -85,7 +94,6 @@ SERVICE_TO_METHOD = {
SERVICE_MEDIA_PAUSE: 'media_pause', SERVICE_MEDIA_PAUSE: 'media_pause',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track', SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track', SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
SERVICE_PLAY_MEDIA: 'play_media',
} }
ATTR_TO_PROPERTY = [ ATTR_TO_PROPERTY = [
@ -107,8 +115,36 @@ ATTR_TO_PROPERTY = [
ATTR_APP_ID, ATTR_APP_ID,
ATTR_APP_NAME, ATTR_APP_NAME,
ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_SUPPORTED_MEDIA_COMMANDS,
ATTR_INPUT_SOURCE,
] ]
# Service call validation schemas
MEDIA_PLAYER_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
})
MEDIA_PLAYER_MUTE_VOLUME_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean,
})
MEDIA_PLAYER_SET_VOLUME_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float,
})
MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_SEEK_POSITION):
vol.All(vol.Coerce(float), vol.Range(min=0)),
})
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
})
MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_INPUT_SOURCE): cv.string,
})
def is_on(hass, entity_id=None): def is_on(hass, entity_id=None):
""" """
@ -219,6 +255,16 @@ def play_media(hass, media_type, media_id, entity_id=None):
hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data) hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data)
def select_source(hass, source, entity_id=None):
"""Send the media player the command to select input source."""
data = {ATTR_INPUT_SOURCE: source}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data)
def setup(hass, config): def setup(hass, config):
"""Track states and offer events for media_players.""" """Track states and offer events for media_players."""
component = EntityComponent( component = EntityComponent(
@ -242,18 +288,13 @@ def setup(hass, config):
for service in SERVICE_TO_METHOD: for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, media_player_service_handler, hass.services.register(DOMAIN, service, media_player_service_handler,
descriptions.get(service)) descriptions.get(service),
schema=MEDIA_PLAYER_SCHEMA)
def volume_set_service(service): def volume_set_service(service):
"""Set specified volume on the media player.""" """Set specified volume on the media player."""
volume = service.data.get(ATTR_MEDIA_VOLUME_LEVEL) volume = service.data.get(ATTR_MEDIA_VOLUME_LEVEL)
if volume is None:
_LOGGER.error(
'Received call to %s without attribute %s',
service.service, ATTR_MEDIA_VOLUME_LEVEL)
return
for player in component.extract_from_service(service): for player in component.extract_from_service(service):
player.set_volume_level(volume) player.set_volume_level(volume)
@ -261,18 +302,13 @@ def setup(hass, config):
player.update_ha_state(True) player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service, hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service,
descriptions.get(SERVICE_VOLUME_SET)) descriptions.get(SERVICE_VOLUME_SET),
schema=MEDIA_PLAYER_SET_VOLUME_SCHEMA)
def volume_mute_service(service): def volume_mute_service(service):
"""Mute (true) or unmute (false) the media player.""" """Mute (true) or unmute (false) the media player."""
mute = service.data.get(ATTR_MEDIA_VOLUME_MUTED) mute = service.data.get(ATTR_MEDIA_VOLUME_MUTED)
if mute is None:
_LOGGER.error(
'Received call to %s without attribute %s',
service.service, ATTR_MEDIA_VOLUME_MUTED)
return
for player in component.extract_from_service(service): for player in component.extract_from_service(service):
player.mute_volume(mute) player.mute_volume(mute)
@ -280,18 +316,13 @@ def setup(hass, config):
player.update_ha_state(True) player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service, hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service,
descriptions.get(SERVICE_VOLUME_MUTE)) descriptions.get(SERVICE_VOLUME_MUTE),
schema=MEDIA_PLAYER_MUTE_VOLUME_SCHEMA)
def media_seek_service(service): def media_seek_service(service):
"""Seek to a position.""" """Seek to a position."""
position = service.data.get(ATTR_MEDIA_SEEK_POSITION) position = service.data.get(ATTR_MEDIA_SEEK_POSITION)
if position is None:
_LOGGER.error(
'Received call to %s without attribute %s',
service.service, ATTR_MEDIA_SEEK_POSITION)
return
for player in component.extract_from_service(service): for player in component.extract_from_service(service):
player.media_seek(position) player.media_seek(position)
@ -299,30 +330,38 @@ def setup(hass, config):
player.update_ha_state(True) player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service, hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service,
descriptions.get(SERVICE_MEDIA_SEEK)) descriptions.get(SERVICE_MEDIA_SEEK),
schema=MEDIA_PLAYER_MEDIA_SEEK_SCHEMA)
def select_source_service(service):
"""Change input to selected source."""
input_source = service.data.get(ATTR_INPUT_SOURCE)
for player in component.extract_from_service(service):
player.select_source(input_source)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_SELECT_SOURCE,
select_source_service,
descriptions.get(SERVICE_SELECT_SOURCE),
schema=MEDIA_PLAYER_SELECT_SOURCE_SCHEMA)
def play_media_service(service): def play_media_service(service):
"""Play specified media_id on the media player.""" """Play specified media_id on the media player."""
media_type = service.data.get(ATTR_MEDIA_CONTENT_TYPE) media_type = service.data.get(ATTR_MEDIA_CONTENT_TYPE)
media_id = service.data.get(ATTR_MEDIA_CONTENT_ID) media_id = service.data.get(ATTR_MEDIA_CONTENT_ID)
if media_type is None or media_id is None:
missing_attr = (ATTR_MEDIA_CONTENT_TYPE if media_type is None
else ATTR_MEDIA_CONTENT_ID)
_LOGGER.error(
'Received call to %s without attribute %s',
service.service, missing_attr)
return
for player in component.extract_from_service(service): for player in component.extract_from_service(service):
player.play_media(media_type, media_id) player.play_media(media_type, media_id)
if player.should_poll: if player.should_poll:
player.update_ha_state(True) player.update_ha_state(True)
hass.services.register( hass.services.register(DOMAIN, SERVICE_PLAY_MEDIA, play_media_service,
DOMAIN, SERVICE_PLAY_MEDIA, play_media_service, descriptions.get(SERVICE_PLAY_MEDIA),
descriptions.get(SERVICE_PLAY_MEDIA)) schema=MEDIA_PLAYER_PLAY_MEDIA_SCHEMA)
return True return True
@ -429,6 +468,11 @@ class MediaPlayerDevice(Entity):
"""Name of the current running app.""" """Name of the current running app."""
return None return None
@property
def source(self):
"""Name of the current input source."""
return None
@property @property
def supported_media_commands(self): def supported_media_commands(self):
"""Flag media commands that are supported.""" """Flag media commands that are supported."""
@ -474,6 +518,10 @@ class MediaPlayerDevice(Entity):
"""Play a piece of media.""" """Play a piece of media."""
raise NotImplementedError() raise NotImplementedError()
def select_source(self, source):
"""Select input source."""
raise NotImplementedError()
# No need to overwrite these. # No need to overwrite these.
@property @property
def support_pause(self): def support_pause(self):
@ -510,6 +558,11 @@ class MediaPlayerDevice(Entity):
"""Boolean if play media command supported.""" """Boolean if play media command supported."""
return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA) return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA)
@property
def support_select_source(self):
"""Boolean if select source command supported."""
return bool(self.supported_media_commands & SUPPORT_SELECT_SOURCE)
def toggle(self): def toggle(self):
"""Toggle the power on the media player.""" """Toggle the power on the media player."""
if self.state in [STATE_OFF, STATE_IDLE]: if self.state in [STATE_OFF, STATE_IDLE]:

View File

@ -8,7 +8,7 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice) SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
@ -35,7 +35,7 @@ MUSIC_PLAYER_SUPPORT = \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF SUPPORT_TURN_ON | SUPPORT_TURN_OFF
NETFLIX_PLAYER_SUPPORT = \ NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
class AbstractDemoPlayer(MediaPlayerDevice): class AbstractDemoPlayer(MediaPlayerDevice):
@ -267,6 +267,7 @@ class DemoTVShowPlayer(AbstractDemoPlayer):
super().__init__('Lounge room') super().__init__('Lounge room')
self._cur_episode = 1 self._cur_episode = 1
self._episode_count = 13 self._episode_count = 13
self._source = 'dvd'
@property @property
def media_content_id(self): def media_content_id(self):
@ -313,6 +314,11 @@ class DemoTVShowPlayer(AbstractDemoPlayer):
"""Return the current running application.""" """Return the current running application."""
return "Netflix" return "Netflix"
@property
def source(self):
"""Return the current input source."""
return self._source
@property @property
def supported_media_commands(self): def supported_media_commands(self):
"""Flag of media commands that are supported.""" """Flag of media commands that are supported."""
@ -337,3 +343,8 @@ class DemoTVShowPlayer(AbstractDemoPlayer):
if self._cur_episode < self._episode_count: if self._cur_episode < self._episode_count:
self._cur_episode += 1 self._cur_episode += 1
self.update_ha_state() self.update_ha_state()
def select_source(self, source):
"""Set the input source."""
self._source = source
self.update_ha_state()

View File

@ -9,7 +9,8 @@ import urllib
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
@ -17,7 +18,8 @@ _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['jsonrpc-requests==0.1'] REQUIREMENTS = ['jsonrpc-requests==0.1']
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
SUPPORT_PLAY_MEDIA
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -268,3 +270,7 @@ class KodiDevice(MediaPlayerDevice):
self._server.Player.Seek(players[0]['playerid'], time) self._server.Player.Seek(players[0]['playerid'], time)
self.update_ha_state() self.update_ha_state()
def play_media(self, media_type, media_id):
"""Send the play_media command to the media player."""
self._server.Player.Open({media_type: media_id}, {})

View File

@ -0,0 +1,111 @@
"""
Support for Onkyo Receivers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.onkyo/
"""
import logging
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_ON
REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/'
'python3.zip#onkyo-eiscp==0.9.2']
_LOGGER = logging.getLogger(__name__)
SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Onkyo platform."""
from eiscp import eISCP
add_devices(OnkyoDevice(receiver)
for receiver in eISCP.discover())
class OnkyoDevice(MediaPlayerDevice):
"""Representation of a Onkyo device."""
# pylint: disable=too-many-public-methods, abstract-method
def __init__(self, receiver):
"""Initialize the Onkyo Receiver."""
self._receiver = receiver
self._muted = False
self._volume = 0
self._pwstate = STATE_OFF
self.update()
self._name = '{}_{}'.format(
receiver.info['model_name'], receiver.info['identifier'])
self._current_source = None
def update(self):
"""Get the latest details from the device."""
status = self._receiver.command('system-power query')
if status[1] == 'on':
self._pwstate = STATE_ON
else:
self._pwstate = STATE_OFF
return
volume_raw = self._receiver.command('volume query')
mute_raw = self._receiver.command('audio-muting query')
current_source_raw = self._receiver.command('input-selector query')
self._current_source = '_'.join('_'.join(
[i for i in current_source_raw[1]]))
self._muted = bool(mute_raw[1] == 'on')
self._volume = int(volume_raw[1], 16)/80.0
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._pwstate
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_ONKYO
@property
def source(self):
""""Return the current input source of the device."""
return self._current_source
def turn_off(self):
"""Turn off media player."""
self._receiver.command('system-power standby')
def set_volume_level(self, volume):
"""Set volume level, input is range 0..1. Onkyo ranges from 1-80."""
self._receiver.command('volume {}'.format(int(volume*80)))
def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player."""
if mute:
self._receiver.command('audio-muting on')
else:
self._receiver.command('audio-muting off')
def turn_on(self):
"""Turn the media player on."""
self._receiver.power_on()
def select_source(self, source):
"""Set the input source."""
self._receiver.command('input-selector {}'.format(source))

View File

@ -0,0 +1,180 @@
"""
Support for interface with a Panasonic Viera TV.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.panasonic_viera/
"""
import logging
import socket
from homeassistant.components.media_player import (
DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
from homeassistant.helpers import validate_config
CONF_PORT = "port"
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['panasonic_viera==0.2']
SUPPORT_VIERATV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_TURN_OFF
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Panasonic Viera TV platform."""
from panasonic_viera import DEFAULT_PORT, RemoteControl
name = config.get(CONF_NAME, 'Panasonic Viera TV')
port = config.get(CONF_PORT, DEFAULT_PORT)
if discovery_info:
_LOGGER.debug('%s', discovery_info)
vals = discovery_info.split(':')
if len(vals) > 1:
port = vals[1]
host = vals[0]
remote = RemoteControl(host, port)
add_devices([PanasonicVieraTVDevice(name, remote)])
return True
# Validate that all required config options are given
if not validate_config({DOMAIN: config}, {DOMAIN: [CONF_HOST]}, _LOGGER):
return False
host = config.get(CONF_HOST, None)
remote = RemoteControl(host, port)
try:
remote.get_mute()
except (socket.timeout, TimeoutError, OSError):
_LOGGER.error('Panasonic Viera TV is not available at %s:%d',
host, port)
return False
add_devices([PanasonicVieraTVDevice(name, remote)])
return True
# pylint: disable=abstract-method
class PanasonicVieraTVDevice(MediaPlayerDevice):
"""Representation of a Panasonic Viera TV."""
# pylint: disable=too-many-public-methods
def __init__(self, name, remote):
"""Initialize the samsung device."""
# Save a reference to the imported class
self._name = name
self._muted = False
self._playing = True
self._state = STATE_UNKNOWN
self._remote = remote
def update(self):
"""Retrieve the latest data."""
try:
self._muted = self._remote.get_mute()
self._state = STATE_ON
except (socket.timeout, TimeoutError, OSError):
self._state = STATE_OFF
return False
return True
def send_key(self, key):
"""Send a key to the tv and handles exceptions."""
try:
self._remote.send_key(key)
self._state = STATE_ON
except (socket.timeout, TimeoutError, OSError):
self._state = STATE_OFF
return False
return True
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
volume = 0
try:
volume = self._remote.get_volume() / 100
self._state = STATE_ON
except (socket.timeout, TimeoutError, OSError):
self._state = STATE_OFF
return volume
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_VIERATV
def turn_off(self):
"""Turn off media player."""
self.send_key('NRC_POWER-ONOFF')
def volume_up(self):
"""Volume up the media player."""
self.send_key('NRC_VOLUP-ONOFF')
def volume_down(self):
"""Volume down media player."""
self.send_key('NRC_VOLDOWN-ONOFF')
def mute_volume(self, mute):
"""Send mute command."""
self.send_key('NRC_MUTE-ONOFF')
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
volume = int(volume * 100)
try:
self._remote.set_volume(volume)
self._state = STATE_ON
except (socket.timeout, TimeoutError, OSError):
self._state = STATE_OFF
def media_play_pause(self):
"""Simulate play pause media player."""
if self._playing:
self.media_pause()
else:
self.media_play()
def media_play(self):
"""Send play command."""
self._playing = True
self.send_key('NRC_PLAY-ONOFF')
def media_pause(self):
"""Send media pause command to media player."""
self._playing = False
self.send_key('NRC_PAUSE-ONOFF')
def media_next_track(self):
"""Send next track command."""
self.send_key('NRC_FF-ONOFF')
def media_previous_track(self):
"""Send the previous track command."""
self.send_key('NRC_REW-ONOFF')

View File

@ -126,3 +126,14 @@ play_media:
media_content_type: media_content_type:
description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST
example: 'MUSIC' example: 'MUSIC'
select_source:
description: Send the media player the command to change input source.
fields:
entity_id:
description: Name(s) of entites to change source on
example: 'media_player.media_player.txnr535_0009b0d81f82'
source:
description: Name of the source to switch to. Platform dependent.
example: 'video1'

View File

@ -94,6 +94,7 @@ class SonosDevice(MediaPlayerDevice):
def __init__(self, hass, player): def __init__(self, hass, player):
"""Initialize the Sonos device.""" """Initialize the Sonos device."""
self.hass = hass self.hass = hass
self.volume_increment = 5
super(SonosDevice, self).__init__() super(SonosDevice, self).__init__()
self._player = player self._player = player
self.update() self.update()
@ -197,31 +198,27 @@ class SonosDevice(MediaPlayerDevice):
"""Flag of media commands that are supported.""" """Flag of media commands that are supported."""
return SUPPORT_SONOS return SUPPORT_SONOS
@only_if_coordinator
def turn_off(self):
"""Turn off media player."""
self._player.pause()
@only_if_coordinator
def volume_up(self): def volume_up(self):
"""Volume up media player.""" """Volume up media player."""
self._player.volume += 1 self._player.volume += self.volume_increment
@only_if_coordinator
def volume_down(self): def volume_down(self):
"""Volume down media player.""" """Volume down media player."""
self._player.volume -= 1 self._player.volume -= self.volume_increment
@only_if_coordinator
def set_volume_level(self, volume): def set_volume_level(self, volume):
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
self._player.volume = str(int(volume * 100)) self._player.volume = str(int(volume * 100))
@only_if_coordinator
def mute_volume(self, mute): def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player.""" """Mute (true) or unmute (false) media player."""
self._player.mute = mute self._player.mute = mute
@only_if_coordinator
def turn_off(self):
"""Turn off media player."""
self._player.pause()
@only_if_coordinator @only_if_coordinator
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""

View File

@ -18,7 +18,8 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED,
ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA, ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, MediaPlayerDevice) SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, ATTR_INPUT_SOURCE,
SERVICE_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK, ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
@ -321,6 +322,11 @@ class UniversalMediaPlayer(MediaPlayerDevice):
"""Name of the current running app.""" """Name of the current running app."""
return self._child_attr(ATTR_APP_NAME) return self._child_attr(ATTR_APP_NAME)
@property
def current_source(self):
""""Return the current input source of the device."""
return self._child_attr(ATTR_INPUT_SOURCE)
@property @property
def supported_media_commands(self): def supported_media_commands(self):
"""Flag media commands that are supported.""" """Flag media commands that are supported."""
@ -340,6 +346,9 @@ class UniversalMediaPlayer(MediaPlayerDevice):
ATTR_MEDIA_VOLUME_MUTED in self._attrs: ATTR_MEDIA_VOLUME_MUTED in self._attrs:
flags |= SUPPORT_VOLUME_MUTE flags |= SUPPORT_VOLUME_MUTE
if SUPPORT_SELECT_SOURCE in self._cmds:
flags |= SUPPORT_SELECT_SOURCE
return flags return flags
@property @property
@ -406,6 +415,11 @@ class UniversalMediaPlayer(MediaPlayerDevice):
"""Play or pause the media player.""" """Play or pause the media player."""
self._call_service(SERVICE_MEDIA_PLAY_PAUSE) self._call_service(SERVICE_MEDIA_PLAY_PAUSE)
def select_source(self, source):
"""Set the input source."""
data = {ATTR_INPUT_SOURCE: source}
self._call_service(SERVICE_SELECT_SOURCE, data)
def update(self): def update(self):
"""Update state in HA.""" """Update state in HA."""
for child_name in self._children: for child_name in self._children:

View File

@ -10,7 +10,7 @@ from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice) MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
REQUIREMENTS = ['rxv==0.1.9'] REQUIREMENTS = ['rxv==0.1.11']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \

View File

@ -9,14 +9,16 @@ import os
import socket import socket
import time import time
import voluptuous as vol
from homeassistant.bootstrap import prepare_setup_platform from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.util as util
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_VALUE_TEMPLATE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -39,6 +41,11 @@ CONF_PASSWORD = 'password'
CONF_CERTIFICATE = 'certificate' CONF_CERTIFICATE = 'certificate'
CONF_PROTOCOL = 'protocol' CONF_PROTOCOL = 'protocol'
CONF_STATE_TOPIC = 'state_topic'
CONF_COMMAND_TOPIC = 'command_topic'
CONF_QOS = 'qos'
CONF_RETAIN = 'retain'
PROTOCOL_31 = '3.1' PROTOCOL_31 = '3.1'
PROTOCOL_311 = '3.1.1' PROTOCOL_311 = '3.1.1'
@ -51,12 +58,75 @@ DEFAULT_PROTOCOL = PROTOCOL_311
ATTR_TOPIC = 'topic' ATTR_TOPIC = 'topic'
ATTR_PAYLOAD = 'payload' ATTR_PAYLOAD = 'payload'
ATTR_PAYLOAD_TEMPLATE = 'payload_template' ATTR_PAYLOAD_TEMPLATE = 'payload_template'
ATTR_QOS = 'qos' ATTR_QOS = CONF_QOS
ATTR_RETAIN = 'retain' ATTR_RETAIN = CONF_RETAIN
MAX_RECONNECT_WAIT = 300 # seconds MAX_RECONNECT_WAIT = 300 # seconds
def valid_subscribe_topic(value, invalid_chars='\0'):
"""Validate that we can subscribe using this MQTT topic."""
if isinstance(value, str) and all(c not in value for c in invalid_chars):
return vol.Length(min=1, max=65535)(value)
raise vol.Invalid('Invalid MQTT topic name')
def valid_publish_topic(value):
"""Validate that we can publish using this MQTT topic."""
return valid_subscribe_topic(value, invalid_chars='#+\0')
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
_HBMQTT_CONFIG_SCHEMA = vol.Schema(dict)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE):
vol.All(vol.Coerce(int), vol.Range(min=15)),
vol.Optional(CONF_BROKER): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_CERTIFICATE): cv.isfile,
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
[PROTOCOL_31, PROTOCOL_311],
vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
MQTT_BASE_PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): DOMAIN,
vol.Optional(CONF_SCAN_INTERVAL):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
})
# Sensor type platforms subscribe to mqtt events
MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
})
# Switch type platforms publish to mqtt and may subscribe
MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
})
# Service call validation schema
MQTT_PUBLISH_SCHEMA = vol.Schema({
vol.Required(ATTR_TOPIC): valid_publish_topic,
vol.Exclusive(ATTR_PAYLOAD, 'payload'): object,
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, 'payload'): cv.string,
vol.Required(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
vol.Required(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
}, required=True)
def _build_publish_data(topic, qos, retain): def _build_publish_data(topic, qos, retain):
"""Build the arguments for the publish service without the payload.""" """Build the arguments for the publish service without the payload."""
data = {ATTR_TOPIC: topic} data = {ATTR_TOPIC: topic}
@ -117,8 +187,8 @@ def setup(hass, config):
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN, {})
client_id = util.convert(conf.get(CONF_CLIENT_ID), str) client_id = conf.get(CONF_CLIENT_ID)
keepalive = util.convert(conf.get(CONF_KEEPALIVE), int, DEFAULT_KEEPALIVE) keepalive = conf.get(CONF_KEEPALIVE)
broker_config = _setup_server(hass, config) broker_config = _setup_server(hass, config)
@ -132,16 +202,11 @@ def setup(hass, config):
if CONF_BROKER in conf: if CONF_BROKER in conf:
broker = conf[CONF_BROKER] broker = conf[CONF_BROKER]
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) port = conf[CONF_PORT]
username = util.convert(conf.get(CONF_USERNAME), str) username = conf.get(CONF_USERNAME)
password = util.convert(conf.get(CONF_PASSWORD), str) password = conf.get(CONF_PASSWORD)
certificate = util.convert(conf.get(CONF_CERTIFICATE), str) certificate = conf.get(CONF_CERTIFICATE)
protocol = util.convert(conf.get(CONF_PROTOCOL), str, DEFAULT_PROTOCOL) protocol = conf[CONF_PROTOCOL]
if protocol not in (PROTOCOL_31, PROTOCOL_311):
_LOGGER.error('Invalid protocol specified: %s. Allowed values: %s, %s',
protocol, PROTOCOL_31, PROTOCOL_311)
return False
# For cloudmqtt.com, secured connection, auto fill in certificate # For cloudmqtt.com, secured connection, auto fill in certificate
if certificate is None and 19999 < port < 30000 and \ if certificate is None and 19999 < port < 30000 and \
@ -170,26 +235,19 @@ def setup(hass, config):
def publish_service(call): def publish_service(call):
"""Handle MQTT publish service calls.""" """Handle MQTT publish service calls."""
msg_topic = call.data.get(ATTR_TOPIC) msg_topic = call.data[ATTR_TOPIC]
payload = call.data.get(ATTR_PAYLOAD) payload = call.data.get(ATTR_PAYLOAD)
payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE) payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE)
qos = call.data.get(ATTR_QOS, DEFAULT_QOS) qos = call.data[ATTR_QOS]
retain = call.data.get(ATTR_RETAIN, DEFAULT_RETAIN) retain = call.data[ATTR_RETAIN]
if payload is None: try:
if payload_template is None: payload = (payload if payload_template is None else
_LOGGER.error( template.render(hass, payload_template)) or ''
"You must set either '%s' or '%s' to use this service", except template.jinja2.TemplateError as exc:
ATTR_PAYLOAD, ATTR_PAYLOAD_TEMPLATE) _LOGGER.error(
return "Unable to publish to '%s': rendering payload template of "
try: "'%s' failed because %s.",
payload = template.render(hass, payload_template) msg_topic, payload_template, exc)
except template.jinja2.TemplateError as exc:
_LOGGER.error(
"Unable to publish to '%s': rendering payload template of "
"'%s' failed because %s.",
msg_topic, payload_template, exc)
return
if msg_topic is None or payload is None:
return return
MQTT_CLIENT.publish(msg_topic, payload, qos, retain) MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
@ -199,7 +257,8 @@ def setup(hass, config):
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service, hass.services.register(DOMAIN, SERVICE_PUBLISH, publish_service,
descriptions.get(SERVICE_PUBLISH)) descriptions.get(SERVICE_PUBLISH),
schema=MQTT_PUBLISH_SCHEMA)
return True return True

View File

@ -6,9 +6,11 @@ https://home-assistant.io/components/mqtt_eventstream/
""" """
import json import json
import voluptuous as vol
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt import (
from homeassistant.components.mqtt import SERVICE_PUBLISH as MQTT_SVC_PUBLISH valid_publish_topic, valid_subscribe_topic)
from homeassistant.const import ( from homeassistant.const import (
ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, EVENT_SERVICE_EXECUTED, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, EVENT_SERVICE_EXECUTED,
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL)
@ -18,12 +20,23 @@ from homeassistant.remote import JSONEncoder
DOMAIN = "mqtt_eventstream" DOMAIN = "mqtt_eventstream"
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
CONF_PUBLISH_TOPIC = 'publish_topic'
CONF_SUBSCRIBE_TOPIC = 'subscribe_topic'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_PUBLISH_TOPIC): valid_publish_topic,
vol.Optional(CONF_SUBSCRIBE_TOPIC): valid_subscribe_topic,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config): def setup(hass, config):
"""Setup th MQTT eventstream component.""" """Setup the MQTT eventstream component."""
mqtt = loader.get_component('mqtt') mqtt = loader.get_component('mqtt')
pub_topic = config[DOMAIN].get('publish_topic', None) conf = config.get(DOMAIN, {})
sub_topic = config[DOMAIN].get('subscribe_topic', None) pub_topic = conf.get(CONF_PUBLISH_TOPIC)
sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC)
def _event_publisher(event): def _event_publisher(event):
"""Handle events by publishing them on the MQTT queue.""" """Handle events by publishing them on the MQTT queue."""
@ -36,8 +49,8 @@ def setup(hass, config):
# to the MQTT topic, or you will end up in an infinite loop. # to the MQTT topic, or you will end up in an infinite loop.
if event.event_type == EVENT_CALL_SERVICE: if event.event_type == EVENT_CALL_SERVICE:
if ( if (
event.data.get('domain') == MQTT_DOMAIN and event.data.get('domain') == mqtt.DOMAIN and
event.data.get('service') == MQTT_SVC_PUBLISH and event.data.get('service') == mqtt.SERVICE_PUBLISH and
event.data[ATTR_SERVICE_DATA].get('topic') == pub_topic event.data[ATTR_SERVICE_DATA].get('topic') == pub_topic
): ):
return return

View File

@ -9,7 +9,8 @@ import logging
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
from homeassistant.const import ( from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_HOMEASSISTANT_START, ATTR_DISCOVERED, ATTR_SERVICE, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED, TEMP_CELCIUS) EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED, TEMP_CELCIUS,
CONF_OPTIMISTIC)
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
CONF_GATEWAYS = 'gateways' CONF_GATEWAYS = 'gateways'
@ -19,7 +20,6 @@ CONF_PERSISTENCE = 'persistence'
CONF_PERSISTENCE_FILE = 'persistence_file' CONF_PERSISTENCE_FILE = 'persistence_file'
CONF_VERSION = 'version' CONF_VERSION = 'version'
CONF_BAUD_RATE = 'baud_rate' CONF_BAUD_RATE = 'baud_rate'
CONF_OPTIMISTIC = 'optimistic'
DEFAULT_VERSION = '1.4' DEFAULT_VERSION = '1.4'
DEFAULT_BAUD_RATE = 115200 DEFAULT_BAUD_RATE = 115200

View File

@ -4,7 +4,7 @@ Support for Nest thermostats.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.nest/ https://home-assistant.io/components/thermostat.nest/
""" """
import logging import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
@ -13,21 +13,22 @@ DOMAIN = 'nest'
NEST = None NEST = None
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str
})
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup(hass, config): def setup(hass, config):
"""Setup the Nest thermostat component.""" """Setup the Nest thermostat component."""
global NEST global NEST
logger = logging.getLogger(__name__)
username = config[DOMAIN].get(CONF_USERNAME) username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD) password = config[DOMAIN].get(CONF_PASSWORD)
if username is None or password is None:
logger.error("Missing required configuration items %s or %s",
CONF_USERNAME, CONF_PASSWORD)
return
import nest import nest
NEST = nest.Nest(username, password) NEST = nest.Nest(username, password)

View File

@ -10,8 +10,8 @@ import os
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_per_platform from homeassistant.helpers import config_per_platform, template
from homeassistant.helpers import template from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
@ -27,6 +27,9 @@ ATTR_TARGET = 'target'
# Text to notify user of # Text to notify user of
ATTR_MESSAGE = "message" ATTR_MESSAGE = "message"
# Platform specific data
ATTR_DATA = 'data'
SERVICE_NOTIFY = "notify" SERVICE_NOTIFY = "notify"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -51,7 +54,7 @@ def setup(hass, config):
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
for platform, p_config in config_per_platform(config, DOMAIN, _LOGGER): for platform, p_config in config_per_platform(config, DOMAIN):
notify_implementation = bootstrap.prepare_setup_platform( notify_implementation = bootstrap.prepare_setup_platform(
hass, config, DOMAIN, platform) hass, config, DOMAIN, platform)
@ -80,8 +83,10 @@ def setup(hass, config):
hass, call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)) hass, call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT))
target = call.data.get(ATTR_TARGET) target = call.data.get(ATTR_TARGET)
message = template.render(hass, message) message = template.render(hass, message)
data = call.data.get(ATTR_DATA)
notify_service.send_message(message, title=title, target=target) notify_service.send_message(message, title=title, target=target,
data=data)
service_call_handler = partial(notify_message, notify_service) service_call_handler = partial(notify_message, notify_service)
service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY) service_notify = p_config.get(CONF_NAME, SERVICE_NOTIFY)

View File

@ -63,7 +63,7 @@ class RestNotificationService(BaseNotificationService):
data[self._title_param_name] = kwargs.get(ATTR_TITLE) data[self._title_param_name] = kwargs.get(ATTR_TITLE)
if self._target_param_name is not None: if self._target_param_name is not None:
data[self._title_param_name] = kwargs.get(ATTR_TARGET) data[self._target_param_name] = kwargs.get(ATTR_TARGET)
if self._method == 'POST': if self._method == 'POST':
response = requests.post(self._resource, data=data, timeout=10) response = requests.post(self._resource, data=data, timeout=10)

View File

@ -168,6 +168,7 @@ class RecorderRun(object):
class Recorder(threading.Thread): class Recorder(threading.Thread):
"""A threaded recorder class.""" """A threaded recorder class."""
# pylint: disable=too-many-instance-attributes
def __init__(self, hass): def __init__(self, hass):
"""Initialize the recorder.""" """Initialize the recorder."""
threading.Thread.__init__(self) threading.Thread.__init__(self)
@ -179,6 +180,7 @@ class Recorder(threading.Thread):
self.lock = threading.Lock() self.lock = threading.Lock()
self.recording_start = dt_util.utcnow() self.recording_start = dt_util.utcnow()
self.utc_offset = dt_util.now().utcoffset().total_seconds() self.utc_offset = dt_util.now().utcoffset().total_seconds()
self.db_path = self.hass.config.path(DB_FILE)
def start_recording(event): def start_recording(event):
"""Start recording.""" """Start recording."""
@ -302,8 +304,7 @@ class Recorder(threading.Thread):
def _setup_connection(self): def _setup_connection(self):
"""Ensure database is ready to fly.""" """Ensure database is ready to fly."""
db_path = self.hass.config.path(DB_FILE) self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row self.conn.row_factory = sqlite3.Row
# Make sure the database is closed whenever Python exits # Make sure the database is closed whenever Python exits

View File

@ -5,14 +5,19 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/rfxtrx/ https://home-assistant.io/components/rfxtrx/
""" """
import logging import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
from homeassistant.const import ATTR_ENTITY_ID
REQUIREMENTS = ['pyRFXtrx==0.6.5'] REQUIREMENTS = ['pyRFXtrx==0.6.5']
DOMAIN = "rfxtrx" DOMAIN = "rfxtrx"
ATTR_AUTOMATIC_ADD = 'automatic_add'
ATTR_DEVICE = 'device' ATTR_DEVICE = 'device'
ATTR_DEBUG = 'debug' ATTR_DEBUG = 'debug'
ATTR_STATE = 'state' ATTR_STATE = 'state'
@ -20,7 +25,10 @@ ATTR_NAME = 'name'
ATTR_PACKETID = 'packetid' ATTR_PACKETID = 'packetid'
ATTR_FIREEVENT = 'fire_event' ATTR_FIREEVENT = 'fire_event'
ATTR_DATA_TYPE = 'data_type' ATTR_DATA_TYPE = 'data_type'
ATTR_DUMMY = "dummy" ATTR_DUMMY = 'dummy'
CONF_SIGNAL_REPETITIONS = 'signal_repetitions'
CONF_DEVICES = 'devices'
DEFAULT_SIGNAL_REPETITIONS = 1
EVENT_BUTTON_PRESSED = 'button_pressed' EVENT_BUTTON_PRESSED = 'button_pressed'
@ -30,6 +38,36 @@ _LOGGER = logging.getLogger(__name__)
RFXOBJECT = None RFXOBJECT = None
def _validate_packetid(value):
if get_rfx_object(value):
return value
else:
raise vol.Invalid('invalid packet id for {}'.format(value))
DEVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_PACKETID): _validate_packetid,
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
})
DEFAULT_SCHEMA = vol.Schema({
vol.Required("platform"): DOMAIN,
vol.Required(CONF_DEVICES): {cv.slug: DEVICE_SCHEMA},
vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean,
vol.Optional(CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS):
vol.Coerce(int),
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(ATTR_DEVICE): cv.string,
vol.Optional(ATTR_DEBUG, default=False): cv.boolean,
vol.Optional(ATTR_DUMMY, default=False): cv.boolean,
}),
})
def setup(hass, config): def setup(hass, config):
"""Setup the RFXtrx component.""" """Setup the RFXtrx component."""
# Declare the Handle event # Declare the Handle event
@ -54,16 +92,9 @@ def setup(hass, config):
# Init the rfxtrx module. # Init the rfxtrx module.
global RFXOBJECT global RFXOBJECT
if ATTR_DEVICE not in config[DOMAIN]:
_LOGGER.error(
"can not find device parameter in %s YAML configuration section",
DOMAIN
)
return False
device = config[DOMAIN][ATTR_DEVICE] device = config[DOMAIN][ATTR_DEVICE]
debug = config[DOMAIN].get(ATTR_DEBUG, False) debug = config[DOMAIN][ATTR_DEBUG]
dummy_connection = config[DOMAIN].get(ATTR_DUMMY, False) dummy_connection = config[DOMAIN][ATTR_DUMMY]
if dummy_connection: if dummy_connection:
RFXOBJECT =\ RFXOBJECT =\
@ -97,3 +128,160 @@ def get_rfx_object(packetid):
return obj return obj
return None return None
def get_devices_from_config(config, device):
"""Read rfxtrx configuration."""
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
devices = []
for device_id, entity_info in config[CONF_DEVICES].items():
if device_id in RFX_DEVICES:
continue
_LOGGER.info("Add %s rfxtrx", entity_info[ATTR_NAME])
# Check if i must fire event
fire_event = entity_info[ATTR_FIREEVENT]
datas = {ATTR_STATE: False, ATTR_FIREEVENT: fire_event}
rfxobject = get_rfx_object(entity_info[ATTR_PACKETID])
new_device = device(entity_info[ATTR_NAME], rfxobject, datas,
signal_repetitions)
RFX_DEVICES[device_id] = new_device
devices.append(new_device)
return devices
def get_new_device(event, config, device):
"""Add entity if not exist and the automatic_add is True."""
device_id = slugify(event.device.id_string.lower())
if device_id not in RFX_DEVICES:
automatic_add = config[ATTR_AUTOMATIC_ADD]
if not automatic_add:
return
_LOGGER.info(
"Automatic add %s rfxtrx device (Class: %s Sub: %s)",
device_id,
event.device.__class__.__name__,
event.device.subtype
)
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
entity_name = "%s : %s" % (device_id, pkt_id)
datas = {ATTR_STATE: False, ATTR_FIREEVENT: False}
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
new_device = device(entity_name, event, datas,
signal_repetitions)
RFX_DEVICES[device_id] = new_device
return new_device
def apply_received_command(event):
"""Apply command from rfxtrx."""
device_id = slugify(event.device.id_string.lower())
# Check if entity exists or previously added automatically
if device_id in RFX_DEVICES:
_LOGGER.debug(
"EntityID: %s light_update. Command: %s",
device_id,
event.values['Command']
)
if event.values['Command'] == 'On'\
or event.values['Command'] == 'Off':
# Update the rfxtrx device state
is_on = event.values['Command'] == 'On'
# pylint: disable=protected-access
RFX_DEVICES[device_id]._state = is_on
RFX_DEVICES[device_id].update_ha_state()
elif hasattr(RFX_DEVICES[device_id], 'brightness')\
and event.values['Command'] == 'Set level':
# pylint: disable=protected-access
RFX_DEVICES[device_id]._brightness = \
(event.values['Dim level'] * 255 // 100)
# Update the rfxtrx device state
is_on = RFX_DEVICES[device_id]._brightness > 0
RFX_DEVICES[device_id]._state = is_on
RFX_DEVICES[device_id].update_ha_state()
# Fire event
if RFX_DEVICES[device_id].should_fire_event:
RFX_DEVICES[device_id].hass.bus.fire(
EVENT_BUTTON_PRESSED, {
ATTR_ENTITY_ID:
RFX_DEVICES[device_id].entity_id,
ATTR_STATE: event.values['Command'].lower()
}
)
class RfxtrxDevice(Entity):
"""Represents a Rfxtrx device.
Contains the common logic for all Rfxtrx devices.
"""
def __init__(self, name, event, datas, signal_repetitions):
"""Initialize the device."""
self._name = name
self._event = event
self._state = datas[ATTR_STATE]
self._should_fire_event = datas[ATTR_FIREEVENT]
self.signal_repetitions = signal_repetitions
self._brightness = 0
@property
def should_poll(self):
"""No polling needed for a RFXtrx switch."""
return False
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def should_fire_event(self):
"""Return is the device must fire event."""
return self._should_fire_event
@property
def is_on(self):
"""Return true if light is on."""
return self._state
@property
def assumed_state(self):
"""Return true if unable to access real state of entity."""
return True
def turn_off(self, **kwargs):
"""Turn the light off."""
self._send_command("turn_off")
def _send_command(self, command, brightness=0):
if not self._event:
return
if command == "turn_on":
for _ in range(self.signal_repetitions):
self._event.device.send_on(RFXOBJECT.transport)
self._state = True
elif command == "dim":
for _ in range(self.signal_repetitions):
self._event.device.send_dim(RFXOBJECT.transport,
brightness)
self._state = True
elif command == 'turn_off':
for _ in range(self.signal_repetitions):
self._event.device.send_off(RFXOBJECT.transport)
self._state = False
self._brightness = 0
self.update_ha_state()

View File

@ -10,6 +10,7 @@ import logging
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import group from homeassistant.components import group
from homeassistant.const import ( from homeassistant.const import (
SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_STOP, SERVICE_MOVE_UP, SERVICE_MOVE_DOWN, SERVICE_STOP,

View File

@ -6,38 +6,50 @@ https://home-assistant.io/components/rollershutter.mqtt/
""" """
import logging import logging
import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS)
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
CONF_PAYLOAD_UP = 'payload_up'
CONF_PAYLOAD_DOWN = 'payload_down'
CONF_PAYLOAD_STOP = 'payload_stop'
DEFAULT_NAME = "MQTT Rollershutter" DEFAULT_NAME = "MQTT Rollershutter"
DEFAULT_QOS = 0
DEFAULT_PAYLOAD_UP = "UP" DEFAULT_PAYLOAD_UP = "UP"
DEFAULT_PAYLOAD_DOWN = "DOWN" DEFAULT_PAYLOAD_DOWN = "DOWN"
DEFAULT_PAYLOAD_STOP = "STOP" DEFAULT_PAYLOAD_STOP = "STOP"
PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_UP, default=DEFAULT_PAYLOAD_UP): cv.string,
vol.Optional(CONF_PAYLOAD_DOWN, default=DEFAULT_PAYLOAD_DOWN): cv.string,
vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string,
})
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Add MQTT Rollershutter.""" """Add MQTT Rollershutter."""
if config.get('command_topic') is None:
_LOGGER.error("Missing required variable: command_topic")
return False
add_devices_callback([MqttRollershutter( add_devices_callback([MqttRollershutter(
hass, hass,
config.get('name', DEFAULT_NAME), config[CONF_NAME],
config.get('state_topic'), config.get(CONF_STATE_TOPIC),
config.get('command_topic'), config[CONF_COMMAND_TOPIC],
config.get('qos', DEFAULT_QOS), config[CONF_QOS],
config.get('payload_up', DEFAULT_PAYLOAD_UP), config[CONF_PAYLOAD_UP],
config.get('payload_down', DEFAULT_PAYLOAD_DOWN), config[CONF_PAYLOAD_DOWN],
config.get('payload_stop', DEFAULT_PAYLOAD_STOP), config[CONF_PAYLOAD_STOP],
config.get(CONF_VALUE_TEMPLATE))]) config.get(CONF_VALUE_TEMPLATE)
)])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes

View File

@ -6,7 +6,8 @@ https://home-assistant.io/components/scene/
""" """
import logging import logging
from homeassistant.components.scene import Scene from homeassistant.components.scene import Scene, DOMAIN
from homeassistant.helpers.entity import generate_entity_id
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = [ REQUIREMENTS = [
@ -46,6 +47,10 @@ class PowerViewScene(Scene):
self.hass = hass self.hass = hass
self.scene_data = scene_data self.scene_data = scene_data
self._sync_room_data(room_data) self._sync_room_data(room_data)
self.entity_id_format = DOMAIN + '.{}'
self.entity_id = generate_entity_id(self.entity_id_format,
str(self.scene_data["id"]),
hass=hass)
def _sync_room_data(self, room_data): def _sync_room_data(self, room_data):
"""Sync the room data.""" """Sync the room data."""
@ -57,7 +62,7 @@ class PowerViewScene(Scene):
@property @property
def name(self): def name(self):
"""Return the name of the scene.""" """Return the name of the scene."""
return self.scene_data["name"] return str(self.scene_data["name"])
@property @property
def device_state_attributes(self): def device_state_attributes(self):

View File

@ -12,6 +12,8 @@ import threading
from datetime import timedelta from datetime import timedelta
from itertools import islice from itertools import islice
import voluptuous as vol
import homeassistant.util.dt as date_util import homeassistant.util.dt as date_util
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, EVENT_TIME_CHANGED, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID, EVENT_TIME_CHANGED, SERVICE_TURN_OFF, SERVICE_TURN_ON,
@ -21,7 +23,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.service import (call_from_config, from homeassistant.helpers.service import (call_from_config,
validate_service_call) validate_service_call)
from homeassistant.util import slugify import homeassistant.helpers.config_validation as cv
DOMAIN = "script" DOMAIN = "script"
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -42,6 +44,71 @@ ATTR_CAN_CANCEL = 'can_cancel'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_ALIAS_VALIDATOR = vol.Schema(cv.string)
def _alias_stripper(validator):
"""Strip alias from object for validation."""
def validate(value):
"""Validate without alias value."""
value = value.copy()
alias = value.pop(CONF_ALIAS, None)
if alias is not None:
alias = _ALIAS_VALIDATOR(alias)
value = validator(value)
if alias is not None:
value[CONF_ALIAS] = alias
return value
return validate
_TIMESPEC = vol.Schema({
'days': cv.positive_int,
'hours': cv.positive_int,
'minutes': cv.positive_int,
'seconds': cv.positive_int,
'milliseconds': cv.positive_int,
})
_TIMESPEC_REQ = cv.has_at_least_one_key(
'days', 'hours', 'minutes', 'seconds', 'milliseconds',
)
_DELAY_SCHEMA = vol.Any(
vol.Schema({
vol.Required(CONF_DELAY): vol.All(_TIMESPEC.extend({
vol.Optional(CONF_ALIAS): cv.string
}), _TIMESPEC_REQ)
}),
# Alternative format in case people forgot to indent after 'delay:'
vol.All(_TIMESPEC.extend({
vol.Required(CONF_DELAY): None,
vol.Optional(CONF_ALIAS): cv.string,
}), _TIMESPEC_REQ)
)
_EVENT_SCHEMA = cv.EVENT_SCHEMA.extend({
CONF_ALIAS: cv.string,
})
_SCRIPT_ENTRY_SCHEMA = vol.Schema({
CONF_ALIAS: cv.string,
vol.Required(CONF_SEQUENCE): vol.All(vol.Length(min=1), [vol.Any(
_EVENT_SCHEMA,
_DELAY_SCHEMA,
# Can't extend SERVICE_SCHEMA because it is an vol.All
_alias_stripper(cv.SERVICE_SCHEMA),
)]),
})
CONFIG_SCHEMA = vol.Schema({
vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA}
}, extra=vol.ALLOW_EXTRA)
def is_on(hass, entity_id): def is_on(hass, entity_id):
"""Return if the switch is on based on the statemachine.""" """Return if the switch is on based on the statemachine."""
@ -73,22 +140,12 @@ def setup(hass, config):
"""Execute a service call to script.<script name>.""" """Execute a service call to script.<script name>."""
entity_id = ENTITY_ID_FORMAT.format(service.service) entity_id = ENTITY_ID_FORMAT.format(service.service)
script = component.entities.get(entity_id) script = component.entities.get(entity_id)
if not script:
return
if script.is_on: if script.is_on:
_LOGGER.warning("Script %s already running.", entity_id) _LOGGER.warning("Script %s already running.", entity_id)
return return
script.turn_on() script.turn_on()
for object_id, cfg in config[DOMAIN].items(): for object_id, cfg in config[DOMAIN].items():
if object_id != slugify(object_id):
_LOGGER.warning("Found invalid key for script: %s. Use %s instead",
object_id, slugify(object_id))
continue
if not isinstance(cfg.get(CONF_SEQUENCE), list):
_LOGGER.warning("Key 'sequence' for script %s should be a list",
object_id)
continue
alias = cfg.get(CONF_ALIAS, object_id) alias = cfg.get(CONF_ALIAS, object_id)
script = Script(object_id, alias, cfg[CONF_SEQUENCE]) script = Script(object_id, alias, cfg[CONF_SEQUENCE])
component.add_entities((script,)) component.add_entities((script,))
@ -186,7 +243,9 @@ class Script(ToggleEntity):
self._listener = None self._listener = None
self.turn_on() self.turn_on()
delay = timedelta(**action[CONF_DELAY]) timespec = action[CONF_DELAY] or action.copy()
timespec.pop(CONF_DELAY, None)
delay = timedelta(**timespec)
self._listener = track_point_in_utc_time( self._listener = track_point_in_utc_time(
self.hass, script_delay, date_util.utcnow() + delay) self.hass, script_delay, date_util.utcnow() + delay)
self._cur = cur + 1 self._cur = cur + 1

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/sensor/
import logging import logging
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import ( from homeassistant.components import (
wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors, wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors,
bloomsky, vera) bloomsky, vera)

View File

@ -0,0 +1,263 @@
"""
Support for GTFS (Google/General Transport Format Schema).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.gtfs/
"""
import os
import logging
import datetime
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["https://github.com/robbiet480/pygtfs/archive/"
"6b40d5fb30fd410cfaf637c901b5ed5a08c33e4c.zip#"
"pygtfs==0.1.2"]
ICON = "mdi:train"
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
# pylint: disable=too-many-locals
def get_next_departure(sched, start_station_id, end_station_id):
"""Get the next departure for the given sched."""
origin_station = sched.stops_by_id(start_station_id)[0]
destination_station = sched.stops_by_id(end_station_id)[0]
now = datetime.datetime.now()
day_name = now.strftime("%A").lower()
now_str = now.strftime("%H:%M:%S")
from sqlalchemy.sql import text
sql_query = text("""
SELECT trip.trip_id, trip.route_id,
time(origin_stop_time.departure_time),
time(destination_stop_time.arrival_time),
time(origin_stop_time.arrival_time),
time(origin_stop_time.departure_time),
origin_stop_time.drop_off_type,
origin_stop_time.pickup_type,
origin_stop_time.shape_dist_traveled,
origin_stop_time.stop_headsign,
origin_stop_time.stop_sequence,
time(destination_stop_time.arrival_time),
time(destination_stop_time.departure_time),
destination_stop_time.drop_off_type,
destination_stop_time.pickup_type,
destination_stop_time.shape_dist_traveled,
destination_stop_time.stop_headsign,
destination_stop_time.stop_sequence
FROM trips trip
INNER JOIN calendar calendar
ON trip.service_id = calendar.service_id
INNER JOIN stop_times origin_stop_time
ON trip.trip_id = origin_stop_time.trip_id
INNER JOIN stops start_station
ON origin_stop_time.stop_id = start_station.stop_id
INNER JOIN stop_times destination_stop_time
ON trip.trip_id = destination_stop_time.trip_id
INNER JOIN stops end_station
ON destination_stop_time.stop_id = end_station.stop_id
WHERE calendar.{day_name} = 1
AND time(origin_stop_time.departure_time) > time(:now_str)
AND start_station.stop_id = :origin_station_id
AND end_station.stop_id = :end_station_id
ORDER BY origin_stop_time.departure_time LIMIT 1;
""".format(day_name=day_name))
result = sched.engine.execute(sql_query, now_str=now_str,
origin_station_id=origin_station.id,
end_station_id=destination_station.id)
item = {}
for row in result:
item = row
today = datetime.datetime.today().strftime("%Y-%m-%d")
departure_time_string = "{} {}".format(today, item[2])
arrival_time_string = "{} {}".format(today, item[3])
departure_time = datetime.datetime.strptime(departure_time_string,
TIME_FORMAT)
arrival_time = datetime.datetime.strptime(arrival_time_string,
TIME_FORMAT)
seconds_until = (departure_time-datetime.datetime.now()).total_seconds()
minutes_until = int(seconds_until / 60)
route = sched.routes_by_id(item[1])[0]
origin_stoptime_arrival_time = "{} {}".format(today, item[4])
origin_stoptime_departure_time = "{} {}".format(today, item[5])
dest_stoptime_arrival_time = "{} {}".format(today, item[11])
dest_stoptime_depart_time = "{} {}".format(today, item[12])
origin_stop_time_dict = {
"Arrival Time": origin_stoptime_arrival_time,
"Departure Time": origin_stoptime_departure_time,
"Drop Off Type": item[6], "Pickup Type": item[7],
"Shape Dist Traveled": item[8], "Headsign": item[9],
"Sequence": item[10]
}
destination_stop_time_dict = {
"Arrival Time": dest_stoptime_arrival_time,
"Departure Time": dest_stoptime_depart_time,
"Drop Off Type": item[13], "Pickup Type": item[14],
"Shape Dist Traveled": item[15], "Headsign": item[16],
"Sequence": item[17]
}
return {
"trip_id": item[0],
"trip": sched.trips_by_id(item[0])[0],
"route": route,
"agency": sched.agencies_by_id(route.agency_id)[0],
"origin_station": origin_station,
"departure_time": departure_time,
"destination_station": destination_station,
"arrival_time": arrival_time,
"seconds_until_departure": seconds_until,
"minutes_until_departure": minutes_until,
"origin_stop_time": origin_stop_time_dict,
"destination_stop_time": destination_stop_time_dict
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Get the GTFS sensor."""
if config.get("origin") is None:
_LOGGER.error("Origin must be set in the GTFS configuration!")
return False
if config.get("destination") is None:
_LOGGER.error("Destination must be set in the GTFS configuration!")
return False
if config.get("data") is None:
_LOGGER.error("Data must be set in the GTFS configuration!")
return False
gtfs_dir = hass.config.path("gtfs")
if not os.path.exists(gtfs_dir):
os.makedirs(gtfs_dir)
if not os.path.exists(os.path.join(gtfs_dir, config["data"])):
_LOGGER.error("The given GTFS data file/folder was not found!")
return False
dev = []
dev.append(GTFSDepartureSensor(config["data"], gtfs_dir,
config["origin"], config["destination"]))
add_devices(dev)
# pylint: disable=too-many-instance-attributes,too-few-public-methods
class GTFSDepartureSensor(Entity):
"""Implementation of an GTFS departures sensor."""
def __init__(self, data_source, gtfs_folder, origin, destination):
"""Initialize the sensor."""
self._data_source = data_source
self._gtfs_folder = gtfs_folder
self.origin = origin
self.destination = destination
self._name = "GTFS Sensor"
self._unit_of_measurement = "min"
self._state = 0
self._attributes = {}
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._attributes
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
def update(self):
"""Get the latest data from GTFS and update the states."""
import pygtfs
split_file_name = os.path.splitext(self._data_source)
sqlite_file = "{}.sqlite".format(split_file_name[0])
gtfs = pygtfs.Schedule(os.path.join(self._gtfs_folder, sqlite_file))
# pylint: disable=no-member
if len(gtfs.feeds) < 1:
pygtfs.append_feed(gtfs, os.path.join(self._gtfs_folder,
self._data_source))
self._departure = get_next_departure(gtfs, self.origin,
self.destination)
self._state = self._departure["minutes_until_departure"]
origin_station = self._departure["origin_station"]
destination_station = self._departure["destination_station"]
origin_stop_time = self._departure["origin_stop_time"]
destination_stop_time = self._departure["destination_stop_time"]
agency = self._departure["agency"]
route = self._departure["route"]
trip = self._departure["trip"]
name = "{} {} to {} next departure"
self._name = name.format(agency.agency_name,
origin_station.stop_id,
destination_station.stop_id)
# Build attributes
self._attributes = {}
def dict_for_table(resource):
"""Return a dict for the SQLAlchemy resource given."""
return dict((col, getattr(resource, col))
for col in resource.__table__.columns.keys())
def append_keys(resource, prefix=None):
"""Properly format key val pairs to append to attributes."""
for key, val in resource.items():
if val == "" or val is None or key == "feed_id":
continue
pretty_key = key.replace("_", " ")
pretty_key = pretty_key.title()
pretty_key = pretty_key.replace("Id", "ID")
pretty_key = pretty_key.replace("Url", "URL")
if prefix is not None and \
pretty_key.startswith(prefix) is False:
pretty_key = "{} {}".format(prefix, pretty_key)
self._attributes[pretty_key] = val
append_keys(dict_for_table(agency), "Agency")
append_keys(dict_for_table(route), "Route")
append_keys(dict_for_table(trip), "Trip")
append_keys(dict_for_table(origin_station), "Origin Station")
append_keys(dict_for_table(destination_station), "Destination Station")
append_keys(origin_stop_time, "Origin Stop")
append_keys(destination_stop_time, "Destination Stop")

View File

@ -0,0 +1,129 @@
"""
Support for Loop Energy sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.loop_energy/
"""
import logging
from homeassistant.helpers.entity import Entity
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
_LOGGER = logging.getLogger(__name__)
DOMAIN = "loopenergy"
REQUIREMENTS = ['pyloopenergy==0.0.7']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Loop Energy sensors."""
import pyloopenergy
elec_serial = config.get('electricity_serial')
elec_secret = config.get('electricity_secret')
gas_serial = config.get('gas_serial')
gas_secret = config.get('gas_secret')
if not (elec_serial and elec_secret):
_LOGGER.error(
"Configuration Error, "
"please make sure you have configured electricity "
"serial and secret tokens")
return None
if (gas_serial or gas_secret) and not (gas_serial and gas_secret):
_LOGGER.error(
"Configuration Error, "
"please make sure you have configured gas "
"serial and secret tokens")
return None
controller = pyloopenergy.LoopEnergy(
elec_serial,
elec_secret,
gas_serial,
gas_secret
)
def stop_loopenergy(event):
"""Shutdown loopenergy thread on exit."""
_LOGGER.info("Shutting down loopenergy.")
controller.terminate()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_loopenergy)
sensors = [LoopEnergyElec(controller)]
if gas_serial:
sensors.append(LoopEnergyGas(controller))
add_devices(sensors)
# pylint: disable=too-many-instance-attributes
class LoopEnergyDevice(Entity):
"""Implementation of an Loop Energy base sensor."""
# pylint: disable=too-many-arguments
def __init__(self, controller):
"""Initialize the sensor."""
self._state = None
self._unit_of_measurement = 'kW'
self._controller = controller
self._name = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
def _callback(self):
self.update_ha_state(True)
# pylint: disable=too-many-instance-attributes
class LoopEnergyElec(LoopEnergyDevice):
"""Implementation of an Loop Energy Electricity sensor."""
# pylint: disable=too-many-arguments
def __init__(self, controller):
"""Initialize the sensor."""
super(LoopEnergyElec, self).__init__(controller)
self._name = 'Power Usage'
self._controller.subscribe_elecricity(self._callback)
def update(self):
"""Get the cached Loop energy."""
self._state = round(self._controller.electricity_useage, 2)
# pylint: disable=too-many-instance-attributes
class LoopEnergyGas(LoopEnergyDevice):
"""Implementation of an Loop Energy Gas sensor."""
# pylint: disable=too-many-arguments
def __init__(self, controller):
"""Initialize the sensor."""
super(LoopEnergyGas, self).__init__(controller)
self._name = 'Gas Usage'
self._controller.subscribe_gas(self._callback)
def update(self):
"""Get the cached Loop energy."""
self._state = round(self._controller.gas_useage, 2)

View File

@ -6,33 +6,40 @@ https://home-assistant.io/components/sensor.mqtt/
""" """
import logging import logging
import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_VALUE_TEMPLATE, STATE_UNKNOWN from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN
from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers import template from homeassistant.helpers import template
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "MQTT Sensor"
DEFAULT_QOS = 0
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement'
DEFAULT_NAME = "MQTT Sensor"
PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup MQTT Sensor.""" """Setup MQTT Sensor."""
if config.get('state_topic') is None:
_LOGGER.error("Missing required variable: state_topic")
return False
add_devices_callback([MqttSensor( add_devices_callback([MqttSensor(
hass, hass,
config.get('name', DEFAULT_NAME), config[CONF_NAME],
config.get('state_topic'), config[CONF_STATE_TOPIC],
config.get('qos', DEFAULT_QOS), config[CONF_QOS],
config.get('unit_of_measurement'), config.get(CONF_UNIT_OF_MEASUREMENT),
config.get(CONF_VALUE_TEMPLATE))]) config.get(CONF_VALUE_TEMPLATE),
)])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes

View File

@ -0,0 +1,164 @@
"""
Support for monitoring NZBGet nzb client.
Uses NZBGet's JSON-RPC API to query for monitored variables.
"""
import logging
from datetime import timedelta
import requests
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = []
SENSOR_TYPES = {
"ArticleCacheMB": ("Article Cache", "MB"),
"AverageDownloadRate": ("Average Speed", "MB/s"),
"DownloadRate": ("Speed", "MB/s"),
"DownloadPaused": ("Download Paused", None),
"FreeDiskSpaceMB": ("Disk Free", "MB"),
"PostPaused": ("Post Processing Paused", None),
"RemainingSizeMB": ("Queue Size", "MB"),
}
DEFAULT_TYPES = [
"DownloadRate",
"DownloadPaused",
"RemainingSizeMB",
]
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up nzbget sensors."""
base_url = config.get("base_url")
name = config.get("name", "NZBGet")
username = config.get("username")
password = config.get("password")
monitored_types = config.get("monitored_variables", DEFAULT_TYPES)
if not base_url:
_LOGGER.error("Missing base_url config for NzbGet")
return False
url = "{}/jsonrpc".format(base_url)
try:
nzbgetapi = NZBGetAPI(api_url=url,
username=username,
password=password)
nzbgetapi.update()
except (requests.exceptions.ConnectionError,
requests.exceptions.HTTPError) as conn_err:
_LOGGER.error("Error setting up NZBGet API: %r", conn_err)
return False
devices = []
for ng_type in monitored_types:
if ng_type in SENSOR_TYPES:
new_sensor = NZBGetSensor(api=nzbgetapi,
sensor_type=ng_type,
client_name=name)
devices.append(new_sensor)
else:
_LOGGER.error("Unknown nzbget sensor type: %s", ng_type)
add_devices(devices)
class NZBGetAPI(object):
"""Simple json-rpc wrapper for nzbget's api."""
def __init__(self, api_url, username=None, password=None):
"""Initialize NZBGet API and set headers needed later."""
self.api_url = api_url
self.status = None
self.headers = {'content-type': 'application/json'}
if username is not None and password is not None:
self.auth = (username, password)
else:
self.auth = None
# set the intial state
self.update()
def post(self, method, params=None):
"""Send a post request, and return the response as a dict."""
payload = {"method": method}
if params:
payload['params'] = params
try:
response = requests.post(self.api_url,
json=payload,
auth=self.auth,
headers=self.headers,
timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.ConnectionError as conn_exc:
_LOGGER.error("Failed to update nzbget status from %s. Error: %s",
self.api_url, conn_exc)
raise
@Throttle(timedelta(seconds=5))
def update(self):
"""Update cached response."""
try:
self.status = self.post('status')['result']
except requests.exceptions.ConnectionError:
# failed to update status - exception already logged in self.post
raise
class NZBGetSensor(Entity):
"""Represents an NZBGet sensor."""
def __init__(self, api, sensor_type, client_name):
"""Initialize a new NZBGet sensor."""
self._name = client_name + ' ' + SENSOR_TYPES[sensor_type][0]
self.type = sensor_type
self.client_name = client_name
self.api = api
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
# Set initial state
self.update()
_LOGGER.debug("created nzbget sensor %r", self)
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Unit of measurement of this entity, if any."""
return self._unit_of_measurement
def update(self):
"""Update state of sensor."""
try:
self.api.update()
except requests.exceptions.ConnectionError:
# Error calling the api, already logged in api.update()
return
if self.api.status is None:
_LOGGER.debug("update of %s requested, but no status is available",
self._name)
return
value = self.api.status.get(self.type)
if value is None:
_LOGGER.warning("unable to locate value for %s", self.type)
return
if "DownloadRate" in self.type and value > 0:
# Convert download rate from bytes/s to mb/s
self._state = value / 1024 / 1024
else:
self._state = value

View File

@ -11,7 +11,7 @@ from homeassistant.const import CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['pyowm==2.3.0'] REQUIREMENTS = ['pyowm==2.3.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = { SENSOR_TYPES = {
'weather': ['Condition', None], 'weather': ['Condition', None],

View File

@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
event = rfxtrx.get_rfx_object(entity_info[ATTR_PACKETID]) event = rfxtrx.get_rfx_object(entity_info[ATTR_PACKETID])
new_sensor = RfxtrxSensor(event, entity_info[ATTR_NAME], new_sensor = RfxtrxSensor(event, entity_info[ATTR_NAME],
entity_info.get(ATTR_DATA_TYPE, None)) entity_info.get(ATTR_DATA_TYPE, None))
rfxtrx.RFX_DEVICES[device_id] = new_sensor rfxtrx.RFX_DEVICES[slugify(device_id)] = new_sensor
sensors.append(new_sensor) sensors.append(new_sensor)
add_devices_callback(sensors) add_devices_callback(sensors)
@ -53,6 +53,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if device_id in rfxtrx.RFX_DEVICES: if device_id in rfxtrx.RFX_DEVICES:
rfxtrx.RFX_DEVICES[device_id].event = event rfxtrx.RFX_DEVICES[device_id].event = event
k = 2
_device_id = device_id + "_" + str(k)
while _device_id in rfxtrx.RFX_DEVICES:
rfxtrx.RFX_DEVICES[_device_id].event = event
k = k + 1
_device_id = device_id + "_" + str(k)
return return
# Add entity if not exist and the automatic_add is True # Add entity if not exist and the automatic_add is True

View File

@ -10,10 +10,7 @@ import sys
from datetime import timedelta from datetime import timedelta
from subprocess import check_output from subprocess import check_output
import homeassistant.util.dt as dt_util
from homeassistant.components.sensor import DOMAIN
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_change
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['speedtest-cli==0.3.4'] REQUIREMENTS = ['speedtest-cli==0.3.4']
@ -34,12 +31,12 @@ SENSOR_TYPES = {
} }
# Return cached results if last scan was less then this time ago # Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Speedtest sensor.""" """Setup the Speedtest sensor."""
data = SpeedtestData(hass, config) data = SpeedtestData()
dev = [] dev = []
for sensor in config[CONF_MONITORED_CONDITIONS]: for sensor in config[CONF_MONITORED_CONDITIONS]:
if sensor not in SENSOR_TYPES: if sensor not in SENSOR_TYPES:
@ -49,14 +46,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(dev) add_devices(dev)
def update(call=None):
"""Update service for manual updates."""
data.update(dt_util.now())
for sensor in dev:
sensor.update()
hass.services.register(DOMAIN, 'update_speedtest', update)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class SpeedtestSensor(Entity): class SpeedtestSensor(Entity):
@ -87,36 +76,35 @@ class SpeedtestSensor(Entity):
def update(self): def update(self):
"""Get the latest data and update the states.""" """Get the latest data and update the states."""
self.speedtest_client.update()
data = self.speedtest_client.data data = self.speedtest_client.data
if data is not None: if data is None:
if self.type == 'ping': return
self._state = data['ping']
elif self.type == 'download': elif self.type == 'ping':
self._state = data['download'] self._state = data['ping']
elif self.type == 'upload': elif self.type == 'download':
self._state = data['upload'] self._state = data['download']
elif self.type == 'upload':
self._state = data['upload']
class SpeedtestData(object): class SpeedtestData(object):
"""Get the latest data from speedtest.net.""" """Get the latest data from speedtest.net."""
def __init__(self, hass, config): def __init__(self):
"""Initialize the data object.""" """Initialize the data object."""
self.data = None self.data = None
self.hass = hass
self.path = hass.config.path
track_time_change(self.hass, self.update,
minute=config.get(CONF_MINUTE, 0),
hour=config.get(CONF_HOUR, None),
day=config.get(CONF_DAY, None))
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self, now): def update(self):
"""Get the latest data from speedtest.net.""" """Get the latest data from speedtest.net."""
import speedtest_cli
_LOGGER.info('Executing speedtest') _LOGGER.info('Executing speedtest')
re_output = _SPEEDTEST_REGEX.split( re_output = _SPEEDTEST_REGEX.split(
check_output([sys.executable, self.path( check_output([sys.executable, speedtest_cli.__file__,
'lib', 'speedtest_cli.py'), '--simple']).decode("utf-8")) '--simple']).decode("utf-8"))
self.data = {'ping': round(float(re_output[1]), 2), self.data = {'ping': round(float(re_output[1]), 2),
'download': round(float(re_output[2]), 2), 'download': round(float(re_output[2]), 2),
'upload': round(float(re_output[3]), 2)} 'upload': round(float(re_output[3]), 2)}

View File

@ -13,8 +13,6 @@ from homeassistant.helpers import template
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
DOMAIN = "tcp"
CONF_PORT = "port" CONF_PORT = "port"
CONF_TIMEOUT = "timeout" CONF_TIMEOUT = "timeout"
CONF_PAYLOAD = "payload" CONF_PAYLOAD = "payload"

View File

@ -11,9 +11,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/denismakogon/rides-python-sdk/archive/' REQUIREMENTS = ['uber_rides==0.2.1']
'py3-support.zip#'
'uber_rides==0.1.2-dev']
ICON = 'mdi:taxi' ICON = 'mdi:taxi'
@ -49,7 +47,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
(product_id not in wanted_product_ids): (product_id not in wanted_product_ids):
continue continue
dev.append(UberSensor('time', timeandpriceest, product_id, product)) dev.append(UberSensor('time', timeandpriceest, product_id, product))
dev.append(UberSensor('price', timeandpriceest, product_id, product)) if 'price_details' in product:
dev.append(UberSensor('price', timeandpriceest,
product_id, product))
add_devices(dev) add_devices(dev)
@ -70,13 +70,18 @@ class UberSensor(Entity):
time_estimate = self._product.get('time_estimate_seconds', 0) time_estimate = self._product.get('time_estimate_seconds', 0)
self._state = int(time_estimate / 60) self._state = int(time_estimate / 60)
elif self._sensortype == "price": elif self._sensortype == "price":
price_details = self._product['price_details'] if 'price_details' in self._product:
if price_details['low_estimate'] is None: price_details = self._product['price_details']
self._unit_of_measurement = price_details['currency_code'] self._unit_of_measurement = price_details.get('currency_code',
self._state = int(price_details['minimum']) 'N/A')
if 'low_estimate' in price_details:
statekey = 'minimum'
else:
statekey = 'low_estimate'
self._state = int(price_details.get(statekey, 0))
else: else:
self._unit_of_measurement = price_details['currency_code'] self._unit_of_measurement = 'N/A'
self._state = int(price_details['low_estimate']) self._state = 0
self.update() self.update()
@property @property
@ -97,37 +102,44 @@ class UberSensor(Entity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
price_details = self._product['price_details'] time_estimate = self._product.get('time_estimate_seconds', 'N/A')
distance_key = 'Trip distance (in {}s)'.format(price_details[ params = {
'distance_unit'])
distance_val = self._product.get('distance')
if (price_details.get('distance_unit') is None) or \
(self._product.get('distance') is None):
distance_key = 'Trip distance'
distance_val = 'N/A'
time_estimate = self._product['time_estimate_seconds']
return {
'Product ID': self._product['product_id'], 'Product ID': self._product['product_id'],
'Product short description': self._product['short_description'], 'Product short description': self._product['short_description'],
'Product display name': self._product['display_name'], 'Product display name': self._product['display_name'],
'Product description': self._product['description'], 'Product description': self._product['description'],
'Pickup time estimate (in seconds)': time_estimate, 'Pickup time estimate (in seconds)': time_estimate,
'Trip duration (in seconds)': self._product.get('duration', 'N/A'), 'Trip duration (in seconds)': self._product.get('duration', 'N/A'),
distance_key: distance_val, 'Vehicle Capacity': self._product['capacity']
'Vehicle Capacity': self._product['capacity'],
'Minimum price': price_details['minimum'],
'Cost per minute': price_details['cost_per_minute'],
'Distance units': price_details['distance_unit'],
'Cancellation fee': price_details['cancellation_fee'],
'Cost per distance unit': price_details['cost_per_distance'],
'Base price': price_details['base'],
'Price estimate': price_details.get('estimate', 'N/A'),
'Price currency code': price_details.get('currency_code'),
'High price estimate': price_details.get('high_estimate', 'N/A'),
'Low price estimate': price_details.get('low_estimate', 'N/A'),
'Surge multiplier': price_details.get('surge_multiplier', 'N/A')
} }
if 'price_details' in self._product:
price_details = self._product['price_details']
distance_key = 'Trip distance (in {}s)'.format(price_details[
'distance_unit'])
distance_val = self._product.get('distance')
params['Minimum price'] = price_details['minimum'],
params['Cost per minute'] = price_details['cost_per_minute'],
params['Distance units'] = price_details['distance_unit'],
params['Cancellation fee'] = price_details['cancellation_fee'],
params['Cost per distance'] = price_details['cost_per_distance'],
params['Base price'] = price_details['base'],
params['Price estimate'] = price_details.get('estimate', 'N/A'),
params['Price currency code'] = price_details.get('currency_code'),
params['High price estimate'] = price_details.get('high_estimate',
'N/A'),
params['Low price estimate'] = price_details.get('low_estimate',
'N/A'),
params['Surge multiplier'] = price_details.get('surge_multiplier',
'N/A')
else:
distance_key = 'Trip distance (in miles)'
distance_val = self._product.get('distance', 'N/A')
params[distance_key] = distance_val
return params
@property @property
def icon(self): def icon(self):
"""Icon to use in the frontend, if any.""" """Icon to use in the frontend, if any."""
@ -142,9 +154,12 @@ class UberSensor(Entity):
time_estimate = self._product.get('time_estimate_seconds', 0) time_estimate = self._product.get('time_estimate_seconds', 0)
self._state = int(time_estimate / 60) self._state = int(time_estimate / 60)
elif self._sensortype == "price": elif self._sensortype == "price":
price_details = self._product['price_details'] price_details = self._product.get('price_details')
min_price = price_details['minimum'] if price_details is not None:
self._state = int(price_details.get('low_estimate', min_price)) min_price = price_details.get('minimum')
self._state = int(price_details.get('low_estimate', min_price))
else:
self._state = 0
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -186,17 +201,22 @@ class UberEstimate(object):
self.end_latitude, self.end_latitude,
self.end_longitude) self.end_longitude)
prices = price_response.json.get('prices') prices = price_response.json.get('prices', [])
for price in prices: for price in prices:
product = self.products[price['product_id']] product = self.products[price['product_id']]
price_details = product["price_details"] price_details = product.get("price_details")
product["duration"] = price['duration'] product["duration"] = price.get('duration', '0')
product["distance"] = price['distance'] product["distance"] = price.get('distance', '0')
price_details["estimate"] = price['estimate'] if price_details is not None:
price_details["high_estimate"] = price['high_estimate'] price_details["estimate"] = price.get('estimate',
price_details["low_estimate"] = price['low_estimate'] '0')
price_details["surge_multiplier"] = price['surge_multiplier'] price_details["high_estimate"] = price.get('high_estimate',
'0')
price_details["low_estimate"] = price.get('low_estimate',
'0')
surge_multiplier = price.get('surge_multiplier', '0')
price_details["surge_multiplier"] = surge_multiplier
estimate_response = client.get_pickup_time_estimates( estimate_response = client.get_pickup_time_estimates(
self.start_latitude, self.start_longitude) self.start_latitude, self.start_longitude)

View File

@ -14,7 +14,7 @@ from homeassistant.helpers.event import (
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util import location as location_util from homeassistant.util import location as location_util
REQUIREMENTS = ['astral==0.9'] REQUIREMENTS = ['astral==1.0']
DOMAIN = "sun" DOMAIN = "sun"
ENTITY_ID = "sun.sun" ENTITY_ID = "sun.sun"
@ -113,8 +113,8 @@ def setup(hass, config):
from astral import Location from astral import Location
location = Location(('', '', latitude, longitude, hass.config.time_zone, location = Location(('', '', latitude, longitude,
elevation)) hass.config.time_zone.zone, elevation))
sun = Sun(hass, location) sun = Sun(hass, location)
sun.point_in_time_listener(dt_util.utcnow()) sun.point_in_time_listener(dt_util.utcnow())

View File

@ -11,7 +11,7 @@ import os
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)

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