mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
commit
e97667aea0
@ -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
|
||||||
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -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
1
.gitignore
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
86
homeassistant/components/camera/rpi_camera.py
Normal file
86
homeassistant/components/camera/rpi_camera.py
Normal 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
|
@ -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__)
|
||||||
|
@ -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 = []
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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 = {}
|
||||||
|
|
||||||
|
@ -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("-")
|
||||||
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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"
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
Copyright 2011 Google Inc. All Rights Reserved.
|
@ -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 doesn’t 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>
|
@ -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.
|
@ -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"
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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’t hesitate to change forms to better fit the constraints of a monospaced environment.
|
||||||
|
For example, narrow glyphs like ‘I’, ‘l’ and ‘i’ have added serifs for more even texture while wider glyphs are adjusted for weight.
|
||||||
|
Curved caps like ‘C’ and ‘O’ 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 ‘1’, lowercase ‘l’ and capital ‘I’ are easily differentiated as are zero and the letter ‘O’.
|
||||||
|
Punctuation important for code has also been considered.
|
||||||
|
For example, the curly braces ‘{ }’ have exaggerated points to clearly differentiate them from parenthesis ‘( )’ and braces ‘[ ]’.
|
||||||
|
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>
|
@ -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.
|
@ -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"
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
Subproject commit a4217e0f620366e47dde82f7ce2d8f7b2bb6a079
|
Subproject commit 89a1723f9f5fdaf5b144222b82b73995200ed339
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
|
||||||
|
@ -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)
|
||||||
|
@ -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))])
|
||||||
|
|
||||||
|
|
||||||
|
@ -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]:
|
||||||
|
@ -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()
|
||||||
|
@ -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}, {})
|
||||||
|
111
homeassistant/components/media_player/onkyo.py
Normal file
111
homeassistant/components/media_player/onkyo.py
Normal 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))
|
180
homeassistant/components/media_player/panasonic_viera.py
Normal file
180
homeassistant/components/media_player/panasonic_viera.py
Normal 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')
|
@ -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'
|
||||||
|
@ -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."""
|
||||||
|
@ -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:
|
||||||
|
@ -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 | \
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
263
homeassistant/components/sensor/gtfs.py
Normal file
263
homeassistant/components/sensor/gtfs.py
Normal 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")
|
129
homeassistant/components/sensor/loopenergy.py
Normal file
129
homeassistant/components/sensor/loopenergy.py
Normal 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)
|
@ -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
|
||||||
|
164
homeassistant/components/sensor/nzbget.py
Normal file
164
homeassistant/components/sensor/nzbget.py
Normal 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
|
@ -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],
|
||||||
|
@ -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
|
||||||
|
@ -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)}
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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())
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user