commit
0270ae05e9
171 changed files with 4272 additions and 830 deletions
14
.coveragerc
14
.coveragerc
|
@ -4,6 +4,7 @@ source = homeassistant
|
|||
omit =
|
||||
homeassistant/__main__.py
|
||||
homeassistant/scripts/*.py
|
||||
homeassistant/helpers/typing.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/apcupsd.py
|
||||
|
@ -88,6 +89,9 @@ omit =
|
|||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/switch/knx.py
|
||||
homeassistant/components/binary_sensor/knx.py
|
||||
|
@ -100,6 +104,7 @@ omit =
|
|||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/generic.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
|
@ -123,8 +128,9 @@ omit =
|
|||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
homeassistant/components/foursquare.py
|
||||
homeassistant/components/garage_door/rpi_gpio.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/joaoapps_join.py
|
||||
|
@ -192,23 +198,27 @@ omit =
|
|||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/ohmconnect.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
|
|
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
.tox
|
||||
.git
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -7,7 +7,9 @@ config/custom_components/*
|
|||
!config/custom_components/example.py
|
||||
!config/custom_components/hello_world.py
|
||||
!config/custom_components/mqtt_example.py
|
||||
!config/custom_components/react_panel
|
||||
!config/panels
|
||||
config/panels/*
|
||||
!config/panels/react.html
|
||||
|
||||
tests/testing_config/deps
|
||||
tests/testing_config/home-assistant.log
|
||||
|
@ -52,7 +54,8 @@ develop-eggs
|
|||
lib
|
||||
lib64
|
||||
|
||||
# Installer logs
|
||||
# Logs
|
||||
*.log
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
|
@ -91,3 +94,6 @@ ctags.tmp
|
|||
virtualization/vagrant/setup_done
|
||||
virtualization/vagrant/.vagrant
|
||||
virtualization/vagrant/config
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
|
@ -10,8 +10,8 @@ homeassistant:
|
|||
# Impacts weather/sunrise data
|
||||
elevation: 665
|
||||
|
||||
# C for Celsius, F for Fahrenheit
|
||||
temperature_unit: C
|
||||
# 'metric' for Metric System, 'imperial' for imperial system
|
||||
unit_system: metric
|
||||
|
||||
# Pick yours from here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
"""
|
||||
Custom panel example showing TodoMVC using React.
|
||||
|
||||
Will add a panel to control lights and switches using React. Allows configuring
|
||||
the title via configuration.yaml:
|
||||
|
||||
react_panel:
|
||||
title: 'home'
|
||||
|
||||
"""
|
||||
import os
|
||||
|
||||
from homeassistant.components.frontend import register_panel
|
||||
|
||||
DOMAIN = 'react_panel'
|
||||
DEPENDENCIES = ['frontend']
|
||||
|
||||
PANEL_PATH = os.path.join(os.path.dirname(__file__), 'panel.html')
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Initialize custom panel."""
|
||||
title = config.get(DOMAIN, {}).get('title')
|
||||
|
||||
config = None if title is None else {'title': title}
|
||||
|
||||
register_panel(hass, 'react', PANEL_PATH,
|
||||
title='TodoMVC', icon='mdi:checkbox-marked-outline',
|
||||
config=config)
|
||||
return True
|
|
@ -1,3 +1,20 @@
|
|||
<!--
|
||||
Custom Home Assistant panel example.
|
||||
|
||||
Currently only works in Firefox and Chrome because it uses ES6.
|
||||
|
||||
Make sure this file is in <config>/panels/react.html
|
||||
|
||||
Add to your configuration.yaml:
|
||||
|
||||
panel_custom:
|
||||
- name: react
|
||||
sidebar_title: TodoMVC
|
||||
sidebar_icon: mdi:checkbox-marked-outline
|
||||
config:
|
||||
title: Wow hello!
|
||||
-->
|
||||
|
||||
<script src="https://fb.me/react-15.2.1.min.js"></script>
|
||||
<script src="https://fb.me/react-dom-15.2.1.min.js"></script>
|
||||
|
|
@ -419,8 +419,9 @@ definitions:
|
|||
description: Longitude of Home Assistant server
|
||||
location_name:
|
||||
type: string
|
||||
temperature_unit:
|
||||
unit_system:
|
||||
type: string
|
||||
description: The system for measurement units
|
||||
time_zone:
|
||||
type: string
|
||||
version:
|
||||
|
|
|
@ -274,7 +274,7 @@ def try_to_restart() -> None:
|
|||
# thread left (which is us). Nothing we really do with it, but it might be
|
||||
# useful when debugging shutdown/restart issues.
|
||||
try:
|
||||
nthreads = sum(thread.isAlive() and not thread.isDaemon()
|
||||
nthreads = sum(thread.is_alive() and not thread.daemon
|
||||
for thread in threading.enumerate())
|
||||
if nthreads > 1:
|
||||
sys.stderr.write(
|
||||
|
|
|
@ -11,12 +11,12 @@ from types import ModuleType
|
|||
from typing import Any, Optional, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import group, persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
|
@ -103,7 +103,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
|||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, domain, config)
|
||||
_log_exception(ex, domain, config)
|
||||
return False
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
|
@ -113,7 +113,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
|||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.MultipleInvalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, domain, p_config)
|
||||
_log_exception(ex, domain, p_config)
|
||||
return False
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
|
@ -134,8 +134,8 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
|||
try:
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.MultipleInvalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, '{}.{}'
|
||||
.format(domain, p_name), p_validated)
|
||||
_log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated)
|
||||
return False
|
||||
|
||||
platforms.append(p_validated)
|
||||
|
@ -232,14 +232,14 @@ def from_config_dict(config: Dict[str, Any],
|
|||
if config_dir is not None:
|
||||
config_dir = os.path.abspath(config_dir)
|
||||
hass.config.config_dir = config_dir
|
||||
_mount_local_lib_path(config_dir)
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
conf_util.process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
|
||||
_log_exception(ex, 'homeassistant', core_config)
|
||||
return None
|
||||
|
||||
conf_util.process_ha_config_upgrade(hass)
|
||||
|
@ -300,7 +300,7 @@ def from_config_file(config_path: str,
|
|||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
_mount_local_lib_path(config_dir)
|
||||
mount_local_lib_path(config_dir)
|
||||
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
|
@ -371,6 +371,26 @@ def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
|||
loader.prepare(hass)
|
||||
|
||||
|
||||
def _mount_local_lib_path(config_dir: str) -> None:
|
||||
def _log_exception(ex, domain, config):
|
||||
"""Generate log exception for config validation."""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||
.format(ex.path[-1], domain, domain,
|
||||
'->'.join('%s' % m for m in ex.path))
|
||||
else:
|
||||
message += humanize_error(config, ex)
|
||||
|
||||
if hasattr(config, '__line__'):
|
||||
message += " (See {}:{})".format(config.__config_file__,
|
||||
config.__line__ or '?')
|
||||
|
||||
_LOGGER.error(message)
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir: str) -> str:
|
||||
"""Add local library to Python Path."""
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
if deps_dir not in sys.path:
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
return deps_dir
|
||||
|
|
|
@ -11,7 +11,6 @@ import itertools as it
|
|||
import logging
|
||||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
|
@ -35,7 +34,7 @@ def is_on(hass, entity_id=None):
|
|||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = split_entity_id(entity_id)[0]
|
||||
domain = ha.split_entity_id(entity_id)[0]
|
||||
|
||||
module = get_component(domain)
|
||||
|
||||
|
@ -95,7 +94,7 @@ def setup(hass, config):
|
|||
|
||||
# Group entity_ids by domain. groupby requires sorted data.
|
||||
by_domain = it.groupby(sorted(entity_ids),
|
||||
lambda item: split_entity_id(item)[0])
|
||||
lambda item: ha.split_entity_id(item)[0])
|
||||
|
||||
for domain, ent_ids in by_domain:
|
||||
# We want to block for all calls and only return when all calls
|
||||
|
|
|
@ -20,6 +20,7 @@ from homeassistant.helpers.entity_component import EntityComponent
|
|||
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
SCAN_INTERVAL = 30
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
|
@ -124,6 +125,11 @@ class AlarmControlPanel(Entity):
|
|||
"""Regex for code format or None if no code is required."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Last change triggered by."""
|
||||
return None
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
raise NotImplementedError()
|
||||
|
@ -145,5 +151,6 @@ class AlarmControlPanel(Entity):
|
|||
"""Return the state attributes."""
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
ATTR_CHANGED_BY: self.changed_by
|
||||
}
|
||||
return state_attr
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
alarm_disarm:
|
||||
description: Send the alarm the command for disarm
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to disarm
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to disarm the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
alarm_arm_home:
|
||||
description: Send the alarm the command for arm home
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm home
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm home the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
alarm_arm_away:
|
||||
description: Send the alarm the command for arm away
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to arm away
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to arm away the alarm control panel with
|
||||
example: 1234
|
||||
|
||||
alarm_trigger:
|
||||
description: Send the alarm the command for trigger
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of alarm control panel to trigger
|
||||
example: 'alarm_control_panel.downstairs'
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with
|
||||
example: 1234
|
|
@ -37,6 +37,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
self._id = device_id
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = int(hub.config.get('code_digits', '4'))
|
||||
self._changed_by = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -58,6 +59,11 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
"""The code format as regex."""
|
||||
return '^\\d{%s}$' % self._digits
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Last change triggered by."""
|
||||
return self._changed_by
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
hub.update_alarms()
|
||||
|
@ -72,6 +78,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
_LOGGER.error(
|
||||
'Unknown alarm state %s',
|
||||
hub.alarm_status[self._id].status)
|
||||
self._changed_by = hub.alarm_status[self._id].name
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
|
75
homeassistant/components/camera/ffmpeg.py
Normal file
75
homeassistant/components/camera/ffmpeg.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
"""
|
||||
Support for Cameras with FFmpeg as decoder.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.ffmpeg/
|
||||
"""
|
||||
import logging
|
||||
from contextlib import closing
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.camera.mjpeg import extract_image_from_mjpeg
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
||||
|
||||
REQUIREMENTS = ["ha-ffmpeg==0.4"]
|
||||
|
||||
CONF_INPUT = 'input'
|
||||
CONF_FFMPEG_BIN = 'ffmpeg_bin'
|
||||
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "ffmpeg",
|
||||
vol.Optional(CONF_NAME, default="FFmpeg"): cv.string,
|
||||
vol.Required(CONF_INPUT): cv.string,
|
||||
vol.Optional(CONF_FFMPEG_BIN, default="ffmpeg"): cv.string,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup a FFmpeg Camera."""
|
||||
add_devices_callback([FFmpegCamera(config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
"""An implementation of an FFmpeg camera."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize a FFmpeg camera."""
|
||||
super().__init__()
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._input = config.get(CONF_INPUT)
|
||||
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
self._ffmpeg_bin = config.get(CONF_FFMPEG_BIN)
|
||||
|
||||
def _ffmpeg_stream(self):
|
||||
"""Return a FFmpeg process object."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
ffmpeg = CameraMjpeg(self._ffmpeg_bin)
|
||||
ffmpeg.open_camera(self._input, extra_cmd=self._extra_arguments)
|
||||
return ffmpeg
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
with closing(self._ffmpeg_stream()) as stream:
|
||||
return extract_image_from_mjpeg(stream)
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
stream = self._ffmpeg_stream()
|
||||
return response(
|
||||
stream,
|
||||
mimetype='multipart/x-mixed-replace;boundary=ffserver',
|
||||
direct_passthrough=True
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
|
@ -28,6 +28,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
add_devices_callback([MjpegCamera(config)])
|
||||
|
||||
|
||||
def extract_image_from_mjpeg(stream):
|
||||
"""Take in a MJPEG stream object, return the jpg from it."""
|
||||
data = b''
|
||||
for chunk in stream:
|
||||
data += chunk
|
||||
jpg_start = data.find(b'\xff\xd8')
|
||||
jpg_end = data.find(b'\xff\xd9')
|
||||
if jpg_start != -1 and jpg_end != -1:
|
||||
jpg = data[jpg_start:jpg_end + 2]
|
||||
return jpg
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class MjpegCamera(Camera):
|
||||
"""An implementation of an IP camera that is reachable over a URL."""
|
||||
|
@ -52,19 +64,8 @@ class MjpegCamera(Camera):
|
|||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
def process_response(response):
|
||||
"""Take in a response object, return the jpg from it."""
|
||||
data = b''
|
||||
for chunk in response.iter_content(1024):
|
||||
data += chunk
|
||||
jpg_start = data.find(b'\xff\xd8')
|
||||
jpg_end = data.find(b'\xff\xd9')
|
||||
if jpg_start != -1 and jpg_end != -1:
|
||||
jpg = data[jpg_start:jpg_end + 2]
|
||||
return jpg
|
||||
|
||||
with closing(self.camera_stream()) as response:
|
||||
return process_response(response)
|
||||
return extract_image_from_mjpeg(response.iter_content(1024))
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
|
|
|
@ -27,7 +27,7 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
|
|||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.11.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.11.1']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
|
@ -21,10 +21,10 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# interval in minutes to exclude devices from a scan while they are home
|
||||
# Interval in minutes to exclude devices from a scan while they are home
|
||||
CONF_HOME_INTERVAL = "home_interval"
|
||||
|
||||
REQUIREMENTS = ['python-nmap==0.6.0']
|
||||
REQUIREMENTS = ['python-nmap==0.6.1']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START
|
|||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
DOMAIN = "discovery"
|
||||
REQUIREMENTS = ['netdisco==0.7.0']
|
||||
REQUIREMENTS = ['netdisco==0.7.1']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
|
|
99
homeassistant/components/foursquare.py
Normal file
99
homeassistant/components/foursquare.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
"""
|
||||
Allows utilizing the Foursquare (Swarm) API.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/foursquare/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = "foursquare"
|
||||
|
||||
SERVICE_CHECKIN = "checkin"
|
||||
|
||||
EVENT_PUSH = "foursquare.push"
|
||||
EVENT_CHECKIN = "foursquare.checkin"
|
||||
|
||||
CHECKIN_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required("venueId"): cv.string,
|
||||
vol.Optional("eventId"): cv.string,
|
||||
vol.Optional("shout"): cv.string,
|
||||
vol.Optional("mentions"): cv.string,
|
||||
vol.Optional("broadcast"): cv.string,
|
||||
vol.Optional("ll"): cv.string,
|
||||
vol.Optional("llAcc"): cv.string,
|
||||
vol.Optional("alt"): cv.string,
|
||||
vol.Optional("altAcc"): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["http"]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Foursquare component."""
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), "services.yaml"))
|
||||
|
||||
config = config[DOMAIN]
|
||||
|
||||
def checkin_user(call):
|
||||
"""Check a user in on Swarm."""
|
||||
url = ("https://api.foursquare.com/v2/checkins/add"
|
||||
"?oauth_token={}"
|
||||
"&v=20160802"
|
||||
"&m=swarm").format(config["access_token"])
|
||||
response = requests.post(url, data=call.data, timeout=10)
|
||||
|
||||
if response.status_code not in (200, 201):
|
||||
_LOGGER.exception(
|
||||
"Error checking in user. Response %d: %s:",
|
||||
response.status_code, response.reason)
|
||||
|
||||
hass.bus.fire(EVENT_CHECKIN, response.text)
|
||||
|
||||
# Register our service with Home Assistant.
|
||||
hass.services.register(DOMAIN, "checkin", checkin_user,
|
||||
descriptions[DOMAIN][SERVICE_CHECKIN],
|
||||
schema=CHECKIN_SERVICE_SCHEMA)
|
||||
|
||||
hass.wsgi.register_view(FoursquarePushReceiver(hass,
|
||||
config["push_secret"]))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class FoursquarePushReceiver(HomeAssistantView):
|
||||
"""Handle pushes from the Foursquare API."""
|
||||
|
||||
requires_auth = False
|
||||
url = "/api/foursquare"
|
||||
name = "foursquare"
|
||||
|
||||
def __init__(self, hass, push_secret):
|
||||
"""Initialize the OAuth callback view."""
|
||||
super().__init__(hass)
|
||||
self.push_secret = push_secret
|
||||
|
||||
def post(self, request):
|
||||
"""Accept the POST from Foursquare."""
|
||||
raw_data = request.form
|
||||
_LOGGER.debug("Received Foursquare push: %s", raw_data)
|
||||
if self.push_secret != raw_data["secret"]:
|
||||
_LOGGER.error("Received Foursquare push with invalid"
|
||||
"push secret! Data: %s", raw_data)
|
||||
return
|
||||
parsed_payload = {
|
||||
key: json.loads(val) for key, val in raw_data.items()
|
||||
if key != "secret"
|
||||
}
|
||||
self.hass.bus.fire(EVENT_PUSH, parsed_payload)
|
|
@ -20,8 +20,8 @@ _REGISTERED_COMPONENTS = set()
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_built_in_panel(hass, component_name, title=None, icon=None,
|
||||
url_name=None, config=None):
|
||||
def register_built_in_panel(hass, component_name, sidebar_title=None,
|
||||
sidebar_icon=None, url_path=None, config=None):
|
||||
"""Register a built-in panel."""
|
||||
# pylint: disable=too-many-arguments
|
||||
path = 'panels/ha-panel-{}.html'.format(component_name)
|
||||
|
@ -33,30 +33,31 @@ def register_built_in_panel(hass, component_name, title=None, icon=None,
|
|||
url = None # use default url generate mechanism
|
||||
|
||||
register_panel(hass, component_name, os.path.join(STATIC_PATH, path),
|
||||
FINGERPRINTS[path], title, icon, url_name, url, config)
|
||||
FINGERPRINTS[path], sidebar_title, sidebar_icon, url_path,
|
||||
url, config)
|
||||
|
||||
|
||||
def register_panel(hass, component_name, path, md5=None, title=None, icon=None,
|
||||
url_name=None, url=None, config=None):
|
||||
def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||
sidebar_icon=None, url_path=None, url=None, config=None):
|
||||
"""Register a panel for the frontend.
|
||||
|
||||
component_name: name of the web component
|
||||
path: path to the HTML of the web component
|
||||
md5: the md5 hash of the web component (for versioning, optional)
|
||||
title: title to show in the sidebar (optional)
|
||||
icon: icon to show next to title in sidebar (optional)
|
||||
url_name: name to use in the url (defaults to component_name)
|
||||
sidebar_title: title to show in the sidebar (optional)
|
||||
sidebar_icon: icon to show next to title in sidebar (optional)
|
||||
url_path: name to use in the url (defaults to component_name)
|
||||
url: for the web component (for dev environment, optional)
|
||||
config: config to be passed into the web component
|
||||
|
||||
Warning: this API will probably change. Use at own risk.
|
||||
"""
|
||||
# pylint: disable=too-many-arguments
|
||||
if url_name is None:
|
||||
url_name = component_name
|
||||
if url_path is None:
|
||||
url_path = component_name
|
||||
|
||||
if url_name in PANELS:
|
||||
_LOGGER.warning('Overwriting component %s', url_name)
|
||||
if url_path in PANELS:
|
||||
_LOGGER.warning('Overwriting component %s', url_path)
|
||||
if not os.path.isfile(path):
|
||||
_LOGGER.error('Panel %s component does not exist: %s',
|
||||
component_name, path)
|
||||
|
@ -67,14 +68,14 @@ def register_panel(hass, component_name, path, md5=None, title=None, icon=None,
|
|||
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
|
||||
|
||||
data = {
|
||||
'url_name': url_name,
|
||||
'url_path': url_path,
|
||||
'component_name': component_name,
|
||||
}
|
||||
|
||||
if title:
|
||||
data['title'] = title
|
||||
if icon:
|
||||
data['icon'] = icon
|
||||
if sidebar_title:
|
||||
data['title'] = sidebar_title
|
||||
if sidebar_icon:
|
||||
data['icon'] = sidebar_icon
|
||||
if config is not None:
|
||||
data['config'] = config
|
||||
|
||||
|
@ -90,7 +91,7 @@ def register_panel(hass, component_name, path, md5=None, title=None, icon=None,
|
|||
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
|
||||
data['url'] = fprinted_url
|
||||
|
||||
PANELS[url_name] = data
|
||||
PANELS[url_path] = data
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
@ -195,6 +196,6 @@ class IndexView(HomeAssistantView):
|
|||
resp = template.render(
|
||||
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
||||
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
||||
panel_url=panel_url)
|
||||
panel_url=panel_url, panels=PANELS)
|
||||
|
||||
return self.Response(resp, mimetype='text/html')
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
<link rel='icon' href='/static/icons/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
{% for panel in panels.values() -%}
|
||||
<link rel='prefetch' href='{{ panel.url }}'>
|
||||
{% endfor -%}
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
|
||||
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
|
||||
|
@ -86,9 +89,9 @@
|
|||
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
|
||||
<script src='{{ core_url }}'></script>
|
||||
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
|
||||
{% if panel_url %}
|
||||
{% if panel_url -%}
|
||||
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
|
||||
{% endif %}
|
||||
{% endif -%}
|
||||
<link rel='import' href='{{ icons_url }}' async>
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "bc78f21f5280217aa2c78dfc5848134f",
|
||||
"frontend.html": "6c52e8cb797bafa3124d936af5ce1fcc",
|
||||
"mdi.html": "f6c6cc64c2ec38a80e91f801b41119b3",
|
||||
"panels/ha-panel-dev-event.html": "20327fbd4fb0370aec9be4db26fd723f",
|
||||
"panels/ha-panel-dev-info.html": "28e0a19ceb95aa714fd53228d9983a49",
|
||||
"panels/ha-panel-dev-service.html": "85fd5b48600418bb5a6187539a623c38",
|
||||
"panels/ha-panel-dev-state.html": "25d84d7b7aea779bb3bb3cd6c155f8d9",
|
||||
"panels/ha-panel-dev-template.html": "d079abf61cff9690f828cafb0d29b7e7",
|
||||
"panels/ha-panel-history.html": "7e051b5babf5653b689e0107ea608acb",
|
||||
"panels/ha-panel-iframe.html": "7bdb564a8f37971d7b89b718935810a1",
|
||||
"panels/ha-panel-logbook.html": "9b285357b0b2d82ee282e634f4e1cab2",
|
||||
"panels/ha-panel-map.html": "dfe141a3fa5fd403be554def1dd039a9"
|
||||
"core.js": "457d5acd123e7dc38947c07984b3a5e8",
|
||||
"frontend.html": "829ee7cb591b8a63d7f22948a7aeb07a",
|
||||
"mdi.html": "b399b5d3798f5b68b0a4fbaae3432d48",
|
||||
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
|
||||
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
||||
"panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa",
|
||||
"panels/ha-panel-dev-state.html": "4608326978256644c42b13940c028e0a",
|
||||
"panels/ha-panel-dev-template.html": "0a099d4589636ed3038a3e9f020468a7",
|
||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
||||
"panels/ha-panel-map.html": "af7d04aff7dd5479c5a0016bc8d4dd7d"
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit 697f9397de357cec9662626575fc01d6f921ef22
|
||||
Subproject commit 474366c536ec3e471da12d5f15b07b79fe9b07e2
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1,2 +1,2 @@
|
|||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style is="custom-style" include="iron-positioning"></style><style>.content{margin-top:64px;padding:24px;background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
|
||||
clear: both;white-space:pre-wrap}</style><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">About</span><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/balloob/home-assistant" target="_blank">server</a> — <a href="https://github.com/balloob/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/balloob/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></partial-base></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:24px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
|
||||
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/balloob/home-assistant" target="_blank">server</a> — <a href="https://github.com/balloob/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/balloob/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-iframe"><template><style>iframe{border:0;width:100%;height:100%}</style><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">[[panel.title]]</span><iframe src="[[panel.config.url]]" sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"></iframe></partial-base></template></dom-module><script>Polymer({is:"ha-panel-iframe",properties:{panel:{type:Object},narrow:{type:Boolean},showMenu:{type:Boolean}}})</script></body></html>
|
||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-iframe"><template><style include="ha-style">iframe{border:0;width:100%;height:calc(100% - 64px)}</style><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">[[panel.title]]</div></app-toolbar><iframe src="[[panel.config.url]]" sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"></iframe></template></dom-module><script>Polymer({is:"ha-panel-iframe",properties:{panel:{type:Object},narrow:{type:Boolean},showMenu:{type:Boolean}}})</script></body></html>
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
@ -72,7 +72,7 @@ class RPiGPIOGarageDoor(GarageDoorDevice):
|
|||
|
||||
def update(self):
|
||||
"""Update the state of the garage door."""
|
||||
self._state = rpi_gpio.read_input(self._state_pin) is True
|
||||
self._state = rpi_gpio.read_input(self._state_pin)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
open:
|
||||
description: Open all or specified garage door
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of garage door(s) to open
|
||||
example: 'garage.main'
|
||||
|
||||
close:
|
||||
description: Close all or a specified garage door
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of garage door(s) to close
|
||||
example: 'garage.main'
|
|
@ -13,7 +13,7 @@ from homeassistant.components import zwave
|
|||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
|
||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
||||
|
||||
COMMAND_CLASS_BARRIER_OPERATOR = 0x66 # 102
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -25,7 +25,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY:
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY and \
|
||||
value.command_class != zwave.COMMAND_CLASS_BARRIER_OPERATOR:
|
||||
return
|
||||
if value.type != zwave.TYPE_BOOL:
|
||||
return
|
||||
|
@ -62,8 +63,8 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
|
|||
|
||||
def close_door(self):
|
||||
"""Close the garage door."""
|
||||
self._value.node.set_switch(self._value.value_id, False)
|
||||
self._value.data = False
|
||||
|
||||
def open_door(self):
|
||||
"""Open the garage door."""
|
||||
self._value.node.set_switch(self._value.value_id, True)
|
||||
self._value.data = True
|
||||
|
|
|
@ -14,8 +14,7 @@ from homeassistant.const import (
|
|||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
|
||||
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE)
|
||||
from homeassistant.helpers.entity import (
|
||||
Entity, generate_entity_id, split_entity_id)
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
@ -101,7 +100,7 @@ def expand_entity_ids(hass, entity_ids):
|
|||
|
||||
try:
|
||||
# If entity_id points at a group, expand it
|
||||
domain, _ = split_entity_id(entity_id)
|
||||
domain, _ = ha.split_entity_id(entity_id)
|
||||
|
||||
if domain == DOMAIN:
|
||||
found_ids.extend(
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
|
|||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ["pyhomematic==0.1.10"]
|
||||
REQUIREMENTS = ["pyhomematic==0.1.11"]
|
||||
|
||||
HOMEMATIC = None
|
||||
HOMEMATIC_LINK_DELAY = 0.5
|
||||
|
|
|
@ -20,12 +20,12 @@ from homeassistant.const import (
|
|||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
from homeassistant.core import split_entity_id
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = "http"
|
||||
REQUIREMENTS = ("cherrypy==6.1.1", "static3==0.7.0", "Werkzeug==0.11.10")
|
||||
REQUIREMENTS = ("cherrypy==7.1.0", "static3==0.7.0", "Werkzeug==0.11.10")
|
||||
|
||||
CONF_API_PASSWORD = "api_password"
|
||||
CONF_SERVER_HOST = "server_host"
|
||||
|
@ -453,6 +453,10 @@ class HomeAssistantView(object):
|
|||
"""Handle request to url."""
|
||||
from werkzeug.exceptions import MethodNotAllowed, Unauthorized
|
||||
|
||||
if request.method == "OPTIONS":
|
||||
# For CORS preflight requests.
|
||||
return self.options(request)
|
||||
|
||||
try:
|
||||
handler = getattr(self, request.method.lower())
|
||||
except AttributeError:
|
||||
|
@ -473,16 +477,16 @@ class HomeAssistantView(object):
|
|||
self.hass.wsgi.api_password):
|
||||
authenticated = True
|
||||
|
||||
if authenticated:
|
||||
_LOGGER.info('Successful login/request from %s',
|
||||
request.remote_addr)
|
||||
elif self.requires_auth and not authenticated:
|
||||
if self.requires_auth and not authenticated:
|
||||
_LOGGER.warning('Login attempt or request with an invalid'
|
||||
'password from %s', request.remote_addr)
|
||||
raise Unauthorized()
|
||||
|
||||
request.authenticated = authenticated
|
||||
|
||||
_LOGGER.info('Serving %s to %s (auth: %s)',
|
||||
request.path, request.remote_addr, authenticated)
|
||||
|
||||
result = handler(request, **values)
|
||||
|
||||
if isinstance(result, self.Response):
|
||||
|
|
|
@ -6,17 +6,18 @@ https://home-assistant.io/components/hvac/
|
|||
"""
|
||||
import logging
|
||||
import os
|
||||
from numbers import Number
|
||||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.temperature import convert
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
TEMP_CELCIUS)
|
||||
TEMP_CELSIUS)
|
||||
|
||||
DOMAIN = "hvac"
|
||||
|
||||
|
@ -204,8 +205,8 @@ def setup(hass, config):
|
|||
return
|
||||
|
||||
for hvac in target_hvacs:
|
||||
hvac.set_temperature(convert(
|
||||
temperature, hass.config.temperature_unit,
|
||||
hvac.set_temperature(convert_temperature(
|
||||
temperature, hass.config.units.temperature_unit,
|
||||
hvac.unit_of_measurement))
|
||||
|
||||
if hvac.should_poll:
|
||||
|
@ -462,12 +463,12 @@ class HvacDevice(Entity):
|
|||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return convert(19, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
return convert_temperature(19, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return convert(30, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
return convert_temperature(30, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
|
||||
@property
|
||||
def min_humidity(self):
|
||||
|
@ -481,13 +482,13 @@ class HvacDevice(Entity):
|
|||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if temp is None:
|
||||
return None
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
return temp
|
||||
|
||||
value = convert(temp, self.unit_of_measurement,
|
||||
self.hass.config.temperature_unit)
|
||||
value = convert_temperature(temp, self.unit_of_measurement,
|
||||
self.hass.config.units.temperature_unit)
|
||||
|
||||
if self.hass.config.temperature_unit is TEMP_CELCIUS:
|
||||
if self.hass.config.units.temperature_unit is TEMP_CELSIUS:
|
||||
decimal_count = 1
|
||||
else:
|
||||
# Users of fahrenheit generally expect integer units.
|
||||
|
|
|
@ -33,6 +33,7 @@ CONF_PASSWORD = 'password'
|
|||
CONF_SSL = 'ssl'
|
||||
CONF_VERIFY_SSL = 'verify_ssl'
|
||||
CONF_BLACKLIST = 'blacklist'
|
||||
CONF_WHITELIST = 'whitelist'
|
||||
CONF_TAGS = 'tags'
|
||||
|
||||
|
||||
|
@ -57,6 +58,7 @@ def setup(hass, config):
|
|||
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
|
||||
DEFAULT_VERIFY_SSL)
|
||||
blacklist = conf.get(CONF_BLACKLIST, [])
|
||||
whitelist = conf.get(CONF_WHITELIST, [])
|
||||
tags = conf.get(CONF_TAGS, {})
|
||||
|
||||
try:
|
||||
|
@ -79,6 +81,9 @@ def setup(hass, config):
|
|||
return
|
||||
|
||||
try:
|
||||
if len(whitelist) > 0 and state.entity_id not in whitelist:
|
||||
return
|
||||
|
||||
_state = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
_state = state.state
|
||||
|
|
|
@ -9,11 +9,12 @@ import logging
|
|||
import socket
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
|
||||
Light)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.3.zip'
|
||||
'#flux_led==0.3']
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.6.zip'
|
||||
'#flux_led==0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = "flux_led"
|
||||
|
@ -37,7 +38,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
light_ips = []
|
||||
for ipaddr, device_config in config["devices"].items():
|
||||
device = {}
|
||||
device['id'] = device_config[ATTR_NAME]
|
||||
device['name'] = device_config[ATTR_NAME]
|
||||
device['ipaddr'] = ipaddr
|
||||
light = FluxLight(device)
|
||||
if light.is_valid:
|
||||
|
@ -50,11 +51,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
|
||||
# Find the bulbs on the LAN
|
||||
scanner = flux_led.BulbScanner()
|
||||
scanner.scan(timeout=20)
|
||||
scanner.scan(timeout=10)
|
||||
for device in scanner.getBulbInfo():
|
||||
light = FluxLight(device)
|
||||
ipaddr = device['ipaddr']
|
||||
if light.is_valid and ipaddr not in light_ips:
|
||||
if ipaddr in light_ips:
|
||||
continue
|
||||
device['name'] = device['id'] + " " + ipaddr
|
||||
light = FluxLight(device)
|
||||
if light.is_valid:
|
||||
lights.append(light)
|
||||
light_ips.append(ipaddr)
|
||||
|
||||
|
@ -69,7 +73,7 @@ class FluxLight(Light):
|
|||
"""Initialize the light."""
|
||||
import flux_led
|
||||
|
||||
self._name = device['id']
|
||||
self._name = device['name']
|
||||
self._ipaddr = device['ipaddr']
|
||||
self.is_valid = True
|
||||
self._bulb = None
|
||||
|
@ -96,9 +100,27 @@ class FluxLight(Light):
|
|||
"""Return true if device is on."""
|
||||
return self._bulb.isOn()
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._bulb.getWarmWhite255()
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the color property."""
|
||||
return self._bulb.getRgb()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the specified or all lights on."""
|
||||
self._bulb.turnOn()
|
||||
if not self.is_on:
|
||||
self._bulb.turnOn()
|
||||
|
||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
if rgb:
|
||||
self._bulb.setRgb(*tuple(rgb))
|
||||
elif brightness:
|
||||
self._bulb.setWarmWhite255(brightness)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the specified or all lights off."""
|
||||
|
|
|
@ -72,6 +72,11 @@ class Hyperion(Light):
|
|||
"""Get the remote's active color."""
|
||||
response = self.json_request({"command": "serverinfo"})
|
||||
if response:
|
||||
# workaround for outdated Hyperion
|
||||
if "activeLedColor" not in response["info"]:
|
||||
self._rgb_color = self._default_color
|
||||
return
|
||||
|
||||
if response["info"]["activeLedColor"] == []:
|
||||
self._rgb_color = [0, 0, 0]
|
||||
else:
|
||||
|
|
|
@ -4,8 +4,6 @@ Support for the LIFX platform that implements lights.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.lifx/
|
||||
"""
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
import colorsys
|
||||
import logging
|
||||
|
||||
|
@ -16,7 +14,6 @@ from homeassistant.helpers.event import track_time_change
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['liffylights==0.9.4']
|
||||
DEPENDENCIES = []
|
||||
|
||||
CONF_SERVER = "server" # server address configuration item
|
||||
CONF_BROADCAST = "broadcast" # broadcast address configuration item
|
||||
|
@ -94,11 +91,11 @@ class LIFX():
|
|||
|
||||
# pylint: disable=unused-argument
|
||||
def poll(self, now):
|
||||
"""Initialize the light."""
|
||||
"""Polling for the light."""
|
||||
self.probe()
|
||||
|
||||
def probe(self, address=None):
|
||||
"""Initialize the light."""
|
||||
"""Probe the light."""
|
||||
self._liffylights.probe(address)
|
||||
|
||||
|
||||
|
|
233
homeassistant/components/light/mqtt_json.py
Executable file
233
homeassistant/components/light/mqtt_json.py
Executable file
|
@ -0,0 +1,233 @@
|
|||
"""
|
||||
Support for MQTT JSON lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.mqtt_json/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||
ATTR_FLASH, FLASH_LONG, FLASH_SHORT, Light)
|
||||
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_PLATFORM
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "mqtt_json"
|
||||
|
||||
DEPENDENCIES = ["mqtt"]
|
||||
|
||||
DEFAULT_NAME = "MQTT JSON Light"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
DEFAULT_BRIGHTNESS = False
|
||||
DEFAULT_RGB = False
|
||||
DEFAULT_FLASH_TIME_SHORT = 2
|
||||
DEFAULT_FLASH_TIME_LONG = 10
|
||||
|
||||
CONF_BRIGHTNESS = "brightness"
|
||||
CONF_RGB = "rgb"
|
||||
CONF_FLASH_TIME_SHORT = "flash_time_short"
|
||||
CONF_FLASH_TIME_LONG = "flash_time_long"
|
||||
|
||||
# Stealing some of these from the base MQTT configs.
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): DOMAIN,
|
||||
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
|
||||
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
|
||||
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean,
|
||||
vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean,
|
||||
vol.Optional(CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_FLASH_TIME_LONG, default=DEFAULT_FLASH_TIME_LONG):
|
||||
cv.positive_int
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup a MQTT JSON Light."""
|
||||
add_devices_callback([MqttJson(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC
|
||||
)
|
||||
},
|
||||
config[CONF_QOS],
|
||||
config[CONF_RETAIN],
|
||||
config[CONF_OPTIMISTIC],
|
||||
config[CONF_BRIGHTNESS],
|
||||
config[CONF_RGB],
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_FLASH_TIME_SHORT,
|
||||
CONF_FLASH_TIME_LONG
|
||||
)
|
||||
}
|
||||
)])
|
||||
|
||||
|
||||
class MqttJson(Light):
|
||||
"""Representation of a MQTT JSON light."""
|
||||
|
||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||
def __init__(self, hass, name, topic, qos, retain,
|
||||
optimistic, brightness, rgb, flash_times):
|
||||
"""Initialize MQTT JSON light."""
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._optimistic = optimistic or topic["state_topic"] is None
|
||||
self._state = False
|
||||
if brightness:
|
||||
self._brightness = 255
|
||||
else:
|
||||
self._brightness = None
|
||||
|
||||
if rgb:
|
||||
self._rgb = [0, 0, 0]
|
||||
else:
|
||||
self._rgb = None
|
||||
|
||||
self._flash_times = flash_times
|
||||
|
||||
def state_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
values = json.loads(payload)
|
||||
|
||||
if values["state"] == "ON":
|
||||
self._state = True
|
||||
elif values["state"] == "OFF":
|
||||
self._state = False
|
||||
|
||||
if self._rgb is not None:
|
||||
try:
|
||||
red = int(values["color"]["r"])
|
||||
green = int(values["color"]["g"])
|
||||
blue = int(values["color"]["b"])
|
||||
|
||||
self._rgb = [red, green, blue]
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid color value received.")
|
||||
|
||||
if self._brightness is not None:
|
||||
try:
|
||||
self._brightness = int(values["brightness"])
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid brightness value received.")
|
||||
|
||||
self.update_ha_state()
|
||||
|
||||
if self._topic["state_topic"] is not None:
|
||||
mqtt.subscribe(self._hass, self._topic["state_topic"],
|
||||
state_received, self._qos)
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the RGB color value."""
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for a MQTT light."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return true if we do optimistic updates."""
|
||||
return self._optimistic
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
should_update = False
|
||||
|
||||
message = {"state": "ON"}
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
message["color"] = {
|
||||
"r": kwargs[ATTR_RGB_COLOR][0],
|
||||
"g": kwargs[ATTR_RGB_COLOR][1],
|
||||
"b": kwargs[ATTR_RGB_COLOR][2]
|
||||
}
|
||||
|
||||
if self._optimistic:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
should_update = True
|
||||
|
||||
if ATTR_FLASH in kwargs:
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
|
||||
if flash == FLASH_LONG:
|
||||
message["flash"] = self._flash_times[CONF_FLASH_TIME_LONG]
|
||||
elif flash == FLASH_SHORT:
|
||||
message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT]
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
message["transition"] = kwargs[ATTR_TRANSITION]
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
message["brightness"] = int(kwargs[ATTR_BRIGHTNESS])
|
||||
|
||||
if self._optimistic:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
should_update = True
|
||||
|
||||
mqtt.publish(self._hass, self._topic["command_topic"],
|
||||
json.dumps(message), self._qos, self._retain)
|
||||
|
||||
if self._optimistic:
|
||||
# Optimistically assume that the light has changed state.
|
||||
self._state = True
|
||||
should_update = True
|
||||
|
||||
if should_update:
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
message = {"state": "OFF"}
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
message["transition"] = kwargs[ATTR_TRANSITION]
|
||||
|
||||
mqtt.publish(self._hass, self._topic["command_topic"],
|
||||
json.dumps(message), self._qos, self._retain)
|
||||
|
||||
if self._optimistic:
|
||||
# Optimistically assume that the light has changed state.
|
||||
self._state = False
|
||||
self.update_ha_state()
|
|
@ -22,6 +22,7 @@ from homeassistant.components import group
|
|||
|
||||
DOMAIN = 'lock'
|
||||
SCAN_INTERVAL = 30
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
|
||||
GROUP_NAME_ALL_LOCKS = 'all locks'
|
||||
ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks')
|
||||
|
@ -101,6 +102,11 @@ def setup(hass, config):
|
|||
class LockDevice(Entity):
|
||||
"""Representation of a lock."""
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Last change triggered by."""
|
||||
return None
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@property
|
||||
def code_format(self):
|
||||
|
@ -127,6 +133,7 @@ class LockDevice(Entity):
|
|||
return None
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
ATTR_CHANGED_BY: self.changed_by
|
||||
}
|
||||
return state_attr
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
lock:
|
||||
description: Lock all or specified locks
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of lock to lock
|
||||
example: 'lock.front_door'
|
||||
code:
|
||||
description: An optional code to lock the lock with
|
||||
example: 1234
|
||||
|
||||
unlock:
|
||||
description: Unlock all or specified locks
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of lock to unlock
|
||||
example: 'lock.front_door'
|
||||
code:
|
||||
description: An optional code to unlock the lock with
|
||||
example: 1234
|
|
@ -35,6 +35,7 @@ class VerisureDoorlock(LockDevice):
|
|||
self._id = device_id
|
||||
self._state = STATE_UNKNOWN
|
||||
self._digits = int(hub.config.get('code_digits', '4'))
|
||||
self._changed_by = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -51,6 +52,11 @@ class VerisureDoorlock(LockDevice):
|
|||
"""Return True if entity is available."""
|
||||
return hub.available
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Last change triggered by."""
|
||||
return self._changed_by
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return the required six digit code."""
|
||||
|
@ -68,6 +74,7 @@ class VerisureDoorlock(LockDevice):
|
|||
_LOGGER.error(
|
||||
'Unknown lock state %s',
|
||||
hub.lock_status[self._id].status)
|
||||
self._changed_by = hub.lock_status[self._id].name
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
|
|
|
@ -18,10 +18,8 @@ from homeassistant.components.http import HomeAssistantView
|
|||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON)
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.core import State
|
||||
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
|
||||
DOMAIN = "logbook"
|
||||
DEPENDENCIES = ['recorder', 'frontend']
|
||||
|
@ -196,6 +194,11 @@ def humanify(events):
|
|||
event != last_sensor_event[to_state.entity_id]:
|
||||
continue
|
||||
|
||||
# Don't show continuous sensor value changes in the logbook
|
||||
if domain == 'sensor' and \
|
||||
to_state.attributes.get('unit_of_measurement'):
|
||||
continue
|
||||
|
||||
yield Entry(
|
||||
event.time_fired,
|
||||
name=to_state.name,
|
||||
|
|
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/media_player/
|
|||
"""
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -13,6 +14,7 @@ from homeassistant.config import load_yaml_config_file
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
|
||||
|
@ -25,10 +27,13 @@ from homeassistant.const import (
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'media_player'
|
||||
DEPENDENCIES = ['http']
|
||||
SCAN_INTERVAL = 10
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
ENTITY_IMAGE_URL = '/api/media_player_proxy/{0}?token={1}'
|
||||
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
SERVICE_SELECT_SOURCE = 'select_source'
|
||||
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
|
||||
|
@ -286,6 +291,8 @@ def setup(hass, config):
|
|||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.wsgi.register_view(MediaPlayerImageView(hass, component.entities))
|
||||
|
||||
component.setup(config)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
|
@ -398,6 +405,11 @@ class MediaPlayerDevice(Entity):
|
|||
"""State of the player."""
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
"""Access token for this media player."""
|
||||
return str(id(self))
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
|
@ -633,7 +645,8 @@ class MediaPlayerDevice(Entity):
|
|||
@property
|
||||
def entity_picture(self):
|
||||
"""Return image of the media playing."""
|
||||
return None if self.state == STATE_OFF else self.media_image_url
|
||||
return None if self.state == STATE_OFF else \
|
||||
ENTITY_IMAGE_URL.format(self.entity_id, self.access_token)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
|
@ -649,3 +662,39 @@ class MediaPlayerDevice(Entity):
|
|||
}
|
||||
|
||||
return state_attr
|
||||
|
||||
|
||||
class MediaPlayerImageView(HomeAssistantView):
|
||||
"""Media player view to serve an image."""
|
||||
|
||||
url = "/api/media_player_proxy/<entity(domain=media_player):entity_id>"
|
||||
name = "api:media_player:image"
|
||||
|
||||
def __init__(self, hass, entities):
|
||||
"""Initialize a media player view."""
|
||||
super().__init__(hass)
|
||||
self.entities = entities
|
||||
|
||||
def get(self, request, entity_id):
|
||||
"""Start a get request."""
|
||||
player = self.entities.get(entity_id)
|
||||
|
||||
if player is None:
|
||||
return self.Response(status=404)
|
||||
|
||||
authenticated = (request.authenticated or
|
||||
request.args.get('token') == player.access_token)
|
||||
|
||||
if not authenticated:
|
||||
return self.Response(status=401)
|
||||
|
||||
image_url = player.media_image_url
|
||||
if image_url:
|
||||
response = requests.get(image_url)
|
||||
else:
|
||||
response = None
|
||||
|
||||
if response is None:
|
||||
return self.Response(status=500)
|
||||
|
||||
return self.Response(response)
|
||||
|
|
|
@ -41,9 +41,12 @@ def _get_mac_address(ip_address):
|
|||
|
||||
pid = Popen(["arp", "-n", ip_address], stdout=PIPE)
|
||||
pid_component = pid.communicate()[0]
|
||||
mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'),
|
||||
pid_component).groups()[0]
|
||||
return mac
|
||||
match = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'),
|
||||
pid_component)
|
||||
if match is not None:
|
||||
return match.groups()[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _config_from_file(filename, config=None):
|
||||
|
|
|
@ -163,6 +163,11 @@ class LgTVDevice(MediaPlayerDevice):
|
|||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_LGTV
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""URL for obtaining a screen capture."""
|
||||
return self._client.url + 'data?target=screen_image'
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self.send_command(1)
|
||||
|
|
|
@ -12,15 +12,16 @@ from urllib.parse import urlparse
|
|||
|
||||
import homeassistant.util as util
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
||||
SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice)
|
||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_SET,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers.event import (track_utc_time_change)
|
||||
|
||||
REQUIREMENTS = ['plexapi==1.1.0']
|
||||
REQUIREMENTS = ['plexapi==2.0.2']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
|
||||
|
@ -30,7 +31,8 @@ PLEX_CONFIG_FILE = 'plex.conf'
|
|||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_STOP | SUPPORT_VOLUME_SET
|
||||
|
||||
|
||||
def config_from_file(filename, config=None):
|
||||
|
@ -193,6 +195,9 @@ class PlexClient(MediaPlayerDevice):
|
|||
# pylint: disable=too-many-public-methods, attribute-defined-outside-init
|
||||
def __init__(self, device, plex_sessions, update_devices, update_sessions):
|
||||
"""Initialize the Plex device."""
|
||||
from plexapi.utils import NA
|
||||
|
||||
self.na_type = NA
|
||||
self.plex_sessions = plex_sessions
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
|
@ -211,20 +216,17 @@ class PlexClient(MediaPlayerDevice):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self.device.name or DEVICE_DEFAULT_NAME
|
||||
return self.device.title or DEVICE_DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"""Return the session, if any."""
|
||||
if self.device.machineIdentifier not in self.plex_sessions:
|
||||
return None
|
||||
|
||||
return self.plex_sessions[self.device.machineIdentifier]
|
||||
return self.plex_sessions.get(self.device.machineIdentifier, None)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self.session:
|
||||
if self.session and self.session.player:
|
||||
state = self.session.player.state
|
||||
if state == 'playing':
|
||||
return STATE_PLAYING
|
||||
|
@ -243,11 +245,30 @@ class PlexClient(MediaPlayerDevice):
|
|||
self.update_devices(no_throttle=True)
|
||||
self.update_sessions(no_throttle=True)
|
||||
|
||||
# pylint: disable=no-self-use, singleton-comparison
|
||||
def _convert_na_to_none(self, value):
|
||||
"""Convert PlexAPI _NA() instances to None."""
|
||||
# PlexAPI will return a "__NA__" object which can be compared to
|
||||
# None, but isn't actually None - this converts it to a real None
|
||||
# type so that lower layers don't think it's a URL and choke on it
|
||||
if value is self.na_type:
|
||||
return None
|
||||
else:
|
||||
return value
|
||||
|
||||
@property
|
||||
def _active_media_plexapi_type(self):
|
||||
"""Get the active media type required by PlexAPI commands."""
|
||||
if self.media_content_type is MEDIA_TYPE_MUSIC:
|
||||
return 'music'
|
||||
else:
|
||||
return 'video'
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
if self.session is not None:
|
||||
return self.session.ratingKey
|
||||
return self._convert_na_to_none(self.session.ratingKey)
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
|
@ -259,65 +280,82 @@ class PlexClient(MediaPlayerDevice):
|
|||
return MEDIA_TYPE_TVSHOW
|
||||
elif media_type == 'movie':
|
||||
return MEDIA_TYPE_VIDEO
|
||||
elif media_type == 'track':
|
||||
return MEDIA_TYPE_MUSIC
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
if self.session is not None:
|
||||
return self.session.duration
|
||||
return self._convert_na_to_none(self.session.duration)
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
if self.session is not None:
|
||||
return self.session.thumbUrl
|
||||
thumb_url = self._convert_na_to_none(self.session.thumbUrl)
|
||||
if str(self.na_type) in thumb_url:
|
||||
# Audio tracks build their thumb urls internally before passing
|
||||
# back a URL with the PlexAPI _NA type already converted to a
|
||||
# string and embedded into a malformed URL
|
||||
thumb_url = None
|
||||
return thumb_url
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
# find a string we can use as a title
|
||||
if self.session is not None:
|
||||
return self.session.title
|
||||
return self._convert_na_to_none(self.session.title)
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
"""Season of curent playing media (TV Show only)."""
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.seasons()[0].index
|
||||
return self._convert_na_to_none(self.session.seasons()[0].index)
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
"""The title of the series of current playing media (TV Show only)."""
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.grandparentTitle
|
||||
return self._convert_na_to_none(self.session.grandparentTitle)
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
"""Episode of current playing media (TV Show only)."""
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self.session.index
|
||||
return self._convert_na_to_none(self.session.index)
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_PLEX
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
self.device.setVolume(int(volume * 100),
|
||||
self._active_media_plexapi_type)
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
self.device.play()
|
||||
self.device.play(self._active_media_plexapi_type)
|
||||
|
||||
def media_pause(self):
|
||||
"""Send pause command."""
|
||||
self.device.pause()
|
||||
self.device.pause(self._active_media_plexapi_type)
|
||||
|
||||
def media_stop(self):
|
||||
"""Send stop command."""
|
||||
self.device.stop(self._active_media_plexapi_type)
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.device.skipNext()
|
||||
self.device.skipNext(self._active_media_plexapi_type)
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
self.device.skipPrevious()
|
||||
self.device.skipPrevious(self._active_media_plexapi_type)
|
||||
|
|
|
@ -88,7 +88,8 @@ class RokuDevice(MediaPlayerDevice):
|
|||
self.current_app = None
|
||||
except (requests.exceptions.ConnectionError,
|
||||
requests.exceptions.ReadTimeout):
|
||||
_LOGGER.error("Unable to connect to roku at %s", self.ip_address)
|
||||
|
||||
pass
|
||||
|
||||
def get_source_list(self):
|
||||
"""Get the list of applications to be used as sources."""
|
||||
|
|
|
@ -10,7 +10,7 @@ import socket
|
|||
from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_OPTIMISTIC,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_OFF, STATE_ON, TEMP_CELSIUS)
|
||||
STATE_OFF, STATE_ON)
|
||||
from homeassistant.helpers import validate_config, discovery
|
||||
|
||||
CONF_GATEWAYS = 'gateways'
|
||||
|
@ -53,7 +53,7 @@ def setup(hass, config): # pylint: disable=too-many-locals
|
|||
import mysensors.mysensors as mysensors
|
||||
|
||||
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
|
||||
is_metric = (hass.config.temperature_unit == TEMP_CELSIUS)
|
||||
is_metric = hass.config.units.is_metric
|
||||
persistence = config[DOMAIN].get(CONF_PERSISTENCE, True)
|
||||
|
||||
def setup_gateway(device, persistence_file, baud_rate, tcp_port):
|
||||
|
|
|
@ -44,16 +44,19 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_message(hass, message, title=None):
|
||||
def send_message(hass, message, title=None, data=None):
|
||||
"""Send a notification message."""
|
||||
data = {
|
||||
info = {
|
||||
ATTR_MESSAGE: message
|
||||
}
|
||||
|
||||
if title is not None:
|
||||
data[ATTR_TITLE] = title
|
||||
info[ATTR_TITLE] = title
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_NOTIFY, data)
|
||||
if data is not None:
|
||||
info[ATTR_DATA] = data
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_NOTIFY, info)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
|
@ -4,7 +4,7 @@ Demo notification service.
|
|||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
from homeassistant.components.notify import ATTR_TITLE, BaseNotificationService
|
||||
from homeassistant.components.notify import BaseNotificationService
|
||||
|
||||
EVENT_NOTIFY = "notify"
|
||||
|
||||
|
@ -24,5 +24,5 @@ class DemoNotificationService(BaseNotificationService):
|
|||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
self.hass.bus.fire(EVENT_NOTIFY, {"title": title, "message": message})
|
||||
kwargs['message'] = message
|
||||
self.hass.bus.fire(EVENT_NOTIFY, kwargs)
|
||||
|
|
|
@ -10,7 +10,7 @@ from homeassistant.components.notify import (
|
|||
ATTR_TITLE, DOMAIN, BaseNotificationService)
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['sendgrid==3.0.7']
|
||||
REQUIREMENTS = ['sendgrid==3.1.10']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
|
@ -6,14 +6,18 @@ https://home-assistant.io/components/notify.smtp/
|
|||
"""
|
||||
import logging
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.image import MIMEImage
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TITLE, DOMAIN, BaseNotificationService)
|
||||
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_IMAGES = 'images' # optional embedded image file attachments
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the mail notification service."""
|
||||
|
@ -22,52 +26,21 @@ def get_service(hass, config):
|
|||
_LOGGER):
|
||||
return None
|
||||
|
||||
smtp_server = config.get('server', 'localhost')
|
||||
port = int(config.get('port', '25'))
|
||||
username = config.get('username', None)
|
||||
password = config.get('password', None)
|
||||
starttls = int(config.get('starttls', 0))
|
||||
debug = config.get('debug', 0)
|
||||
|
||||
server = None
|
||||
try:
|
||||
server = smtplib.SMTP(smtp_server, port, timeout=5)
|
||||
server.set_debuglevel(debug)
|
||||
server.ehlo()
|
||||
if starttls == 1:
|
||||
server.starttls()
|
||||
server.ehlo()
|
||||
if username and password:
|
||||
try:
|
||||
server.login(username, password)
|
||||
|
||||
except (smtplib.SMTPException, smtplib.SMTPSenderRefused):
|
||||
_LOGGER.exception("Please check your settings.")
|
||||
return None
|
||||
|
||||
except smtplib.socket.gaierror:
|
||||
_LOGGER.exception(
|
||||
"SMTP server not found (%s:%s). "
|
||||
"Please check the IP address or hostname of your SMTP server.",
|
||||
smtp_server, port)
|
||||
mail_service = MailNotificationService(
|
||||
config.get('server', 'localhost'),
|
||||
int(config.get('port', '25')),
|
||||
config.get('sender', None),
|
||||
int(config.get('starttls', 0)),
|
||||
config.get('username', None),
|
||||
config.get('password', None),
|
||||
config.get('recipient', None),
|
||||
config.get('debug', 0))
|
||||
|
||||
if mail_service.connection_is_valid():
|
||||
return mail_service
|
||||
else:
|
||||
return None
|
||||
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
_LOGGER.exception(
|
||||
"Login not possible. "
|
||||
"Please check your setting and/or your credentials.")
|
||||
|
||||
return None
|
||||
|
||||
finally:
|
||||
if server:
|
||||
server.quit()
|
||||
|
||||
return MailNotificationService(
|
||||
smtp_server, port, config['sender'], starttls, username, password,
|
||||
config['recipient'], debug)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods, too-many-instance-attributes
|
||||
class MailNotificationService(BaseNotificationService):
|
||||
|
@ -99,17 +72,57 @@ class MailNotificationService(BaseNotificationService):
|
|||
mail.login(self.username, self.password)
|
||||
return mail
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
mail = self.connect()
|
||||
subject = kwargs.get(ATTR_TITLE)
|
||||
def connection_is_valid(self):
|
||||
"""Check for valid config, verify connectivity."""
|
||||
server = None
|
||||
try:
|
||||
server = self.connect()
|
||||
except smtplib.socket.gaierror:
|
||||
_LOGGER.exception(
|
||||
"SMTP server not found (%s:%s). "
|
||||
"Please check the IP address or hostname of your SMTP server.",
|
||||
self._server, self._port)
|
||||
|
||||
return False
|
||||
|
||||
except (smtplib.SMTPAuthenticationError, ConnectionRefusedError):
|
||||
_LOGGER.exception(
|
||||
"Login not possible. "
|
||||
"Please check your setting and/or your credentials.")
|
||||
|
||||
return False
|
||||
|
||||
finally:
|
||||
if server:
|
||||
server.quit()
|
||||
|
||||
return True
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""
|
||||
Build and send a message to a user.
|
||||
|
||||
Will send plain text normally, or will build a multipart HTML message
|
||||
with inline image attachments if images config is defined.
|
||||
"""
|
||||
subject = kwargs.get(ATTR_TITLE)
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
|
||||
if data:
|
||||
msg = _build_multipart_msg(message, images=data.get(ATTR_IMAGES))
|
||||
else:
|
||||
msg = _build_text_msg(message)
|
||||
|
||||
msg = MIMEText(message)
|
||||
msg['Subject'] = subject
|
||||
msg['To'] = self.recipient
|
||||
msg['From'] = self._sender
|
||||
msg['X-Mailer'] = 'HomeAssistant'
|
||||
|
||||
return self._send_email(msg)
|
||||
|
||||
def _send_email(self, msg):
|
||||
"""Send the message."""
|
||||
mail = self.connect()
|
||||
for _ in range(self.tries):
|
||||
try:
|
||||
mail.sendmail(self._sender, self.recipient,
|
||||
|
@ -122,3 +135,36 @@ class MailNotificationService(BaseNotificationService):
|
|||
mail = self.connect()
|
||||
|
||||
mail.quit()
|
||||
|
||||
|
||||
def _build_text_msg(message):
|
||||
"""Build plaintext email."""
|
||||
_LOGGER.debug('Building plain text email.')
|
||||
return MIMEText(message)
|
||||
|
||||
|
||||
def _build_multipart_msg(message, images):
|
||||
"""Build Multipart message with in-line images."""
|
||||
_LOGGER.debug('Building multipart email with embedded attachment(s).')
|
||||
msg = MIMEMultipart('related')
|
||||
msg_alt = MIMEMultipart('alternative')
|
||||
msg.attach(msg_alt)
|
||||
body_txt = MIMEText(message)
|
||||
msg_alt.attach(body_txt)
|
||||
body_text = ['<p>{}</p><br>'.format(message)]
|
||||
|
||||
for atch_num, atch_name in enumerate(images):
|
||||
cid = 'image{}'.format(atch_num)
|
||||
body_text.append('<img src="cid:{}"><br>'.format(cid))
|
||||
try:
|
||||
with open(atch_name, 'rb') as attachment_file:
|
||||
attachment = MIMEImage(attachment_file.read())
|
||||
msg.attach(attachment)
|
||||
attachment.add_header('Content-ID', '<{}>'.format(cid))
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning('Attachment %s not found. Skipping.',
|
||||
atch_name)
|
||||
|
||||
body_html = MIMEText(''.join(body_text), 'html')
|
||||
msg_alt.attach(body_html)
|
||||
return msg
|
||||
|
|
69
homeassistant/components/panel_custom.py
Normal file
69
homeassistant/components/panel_custom.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
"""
|
||||
Register a custom front end panel.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/panel_custom/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.frontend import register_panel
|
||||
|
||||
DOMAIN = 'panel_custom'
|
||||
DEPENDENCIES = ['frontend']
|
||||
|
||||
CONF_COMPONENT_NAME = 'name'
|
||||
CONF_SIDEBAR_TITLE = 'sidebar_title'
|
||||
CONF_SIDEBAR_ICON = 'sidebar_icon'
|
||||
CONF_URL_PATH = 'url_path'
|
||||
CONF_CONFIG = 'config'
|
||||
CONF_WEBCOMPONENT_PATH = 'webcomponent_path'
|
||||
|
||||
DEFAULT_ICON = 'mdi:bookmark'
|
||||
|
||||
PANEL_DIR = 'panels'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_COMPONENT_NAME): cv.slug,
|
||||
vol.Optional(CONF_SIDEBAR_TITLE): cv.string,
|
||||
vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon,
|
||||
vol.Optional(CONF_URL_PATH): cv.string,
|
||||
vol.Optional(CONF_CONFIG): cv.match_all,
|
||||
vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile,
|
||||
}])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Initialize custom panel."""
|
||||
success = False
|
||||
|
||||
for panel in config.get(DOMAIN):
|
||||
name = panel.get(CONF_COMPONENT_NAME)
|
||||
panel_path = panel.get(CONF_WEBCOMPONENT_PATH)
|
||||
|
||||
if panel_path is None:
|
||||
panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name))
|
||||
|
||||
if not os.path.isfile(panel_path):
|
||||
_LOGGER.error('Unable to find webcomponent for %s: %s',
|
||||
name, panel_path)
|
||||
continue
|
||||
|
||||
register_panel(
|
||||
hass, name, panel_path,
|
||||
sidebar_title=panel.get(CONF_SIDEBAR_TITLE),
|
||||
sidebar_icon=panel.get(CONF_SIDEBAR_ICON),
|
||||
url_path=panel.get(CONF_URL_PATH),
|
||||
config=panel.get(CONF_CONFIG),
|
||||
)
|
||||
|
||||
success = True
|
||||
|
||||
return success
|
|
@ -23,9 +23,9 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
|
||||
def setup(hass, config):
|
||||
"""Setup iframe frontend panels."""
|
||||
for url_name, info in config[DOMAIN].items():
|
||||
for url_path, info in config[DOMAIN].items():
|
||||
register_built_in_panel(
|
||||
hass, 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
|
||||
url_name, {'url': info[CONF_URL]})
|
||||
url_path, {'url': info[CONF_URL]})
|
||||
|
||||
return True
|
||||
|
|
109
homeassistant/components/pilight.py
Normal file
109
homeassistant/components/pilight.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
"""
|
||||
Component to create an interface to a Pilight daemon (https://pilight.org/).
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/pilight/
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ensure_list
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
|
||||
REQUIREMENTS = ['pilight==0.0.2']
|
||||
|
||||
DOMAIN = "pilight"
|
||||
EVENT = 'pilight_received'
|
||||
SERVICE_NAME = 'send'
|
||||
|
||||
CONF_WHITELIST = 'whitelist'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST, default='127.0.0.1'): cv.string,
|
||||
vol.Required(CONF_PORT, default=5000): vol.Coerce(int),
|
||||
vol.Optional(CONF_WHITELIST): {cv.string: [cv.string]}
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
# The pilight code schema depends on the protocol
|
||||
# Thus only require to have the protocol information
|
||||
ATTR_PROTOCOL = 'protocol'
|
||||
RF_CODE_SCHEMA = vol.Schema({vol.Required(ATTR_PROTOCOL): cv.string},
|
||||
extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the pilight component."""
|
||||
from pilight import pilight
|
||||
|
||||
try:
|
||||
pilight_client = pilight.Client(host=config[DOMAIN][CONF_HOST],
|
||||
port=config[DOMAIN][CONF_PORT])
|
||||
except (socket.error, socket.timeout) as err:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to %s on port %s: %s",
|
||||
config[CONF_HOST], config[CONF_PORT], err)
|
||||
return False
|
||||
|
||||
# Start / stop pilight-daemon connection with HA start/stop
|
||||
def start_pilight_client(_):
|
||||
"""Called once when home assistant starts."""
|
||||
pilight_client.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_pilight_client)
|
||||
|
||||
def stop_pilight_client(_):
|
||||
"""Called once when home assistant stops."""
|
||||
pilight_client.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_pilight_client)
|
||||
|
||||
def send_code(call):
|
||||
"""Send RF code to the pilight-daemon."""
|
||||
message_data = call.data
|
||||
|
||||
# Patch data because of bug:
|
||||
# https://github.com/pilight/pilight/issues/296
|
||||
# Protocol has to be in a list otherwise segfault in pilight-daemon
|
||||
message_data["protocol"] = ensure_list(message_data["protocol"])
|
||||
|
||||
try:
|
||||
pilight_client.send_code(message_data)
|
||||
except IOError:
|
||||
_LOGGER.error('Pilight send failed for %s', str(message_data))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_NAME,
|
||||
send_code, schema=RF_CODE_SCHEMA)
|
||||
|
||||
# Publish received codes on the HA event bus
|
||||
# A whitelist of codes to be published in the event bus
|
||||
whitelist = config[DOMAIN].get('whitelist', False)
|
||||
|
||||
def handle_received_code(data):
|
||||
"""Called when RF codes are received."""
|
||||
# Unravel dict of dicts to make event_data cut in automation rule
|
||||
# possible
|
||||
data = dict(
|
||||
{'protocol': data['protocol'],
|
||||
'uuid': data['uuid']},
|
||||
**data['message'])
|
||||
|
||||
# No whitelist defined, put data on event bus
|
||||
if not whitelist:
|
||||
hass.bus.fire(EVENT, data)
|
||||
# Check if data matches the defined whitelist
|
||||
elif all(data[key] in whitelist[key] for key in whitelist):
|
||||
hass.bus.fire(EVENT, data)
|
||||
|
||||
pilight_client.set_callback(handle_received_code)
|
||||
|
||||
return True
|
|
@ -12,17 +12,30 @@ import logging
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.util.location import distance
|
||||
from homeassistant.util.distance import convert
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
|
||||
|
||||
DEPENDENCIES = ['zone', 'device_tracker']
|
||||
|
||||
DOMAIN = 'proximity'
|
||||
|
||||
NOT_SET = 'not set'
|
||||
|
||||
# Default tolerance
|
||||
DEFAULT_TOLERANCE = 1
|
||||
|
||||
# Default zone
|
||||
DEFAULT_PROXIMITY_ZONE = 'home'
|
||||
|
||||
# Default distance to zone
|
||||
DEFAULT_DIST_TO_ZONE = NOT_SET
|
||||
|
||||
# Default direction of travel
|
||||
DEFAULT_DIR_OF_TRAVEL = NOT_SET
|
||||
|
||||
# Default nearest device
|
||||
DEFAULT_NEAREST = NOT_SET
|
||||
|
||||
# Entity attributes
|
||||
ATTR_DIST_FROM = 'dist_to_zone'
|
||||
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
|
||||
|
@ -31,43 +44,41 @@ ATTR_NEAREST = 'nearest'
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements
|
||||
"""Get the zones and offsets from configuration.yaml."""
|
||||
ignored_zones = []
|
||||
if 'ignored_zones' in config[DOMAIN]:
|
||||
for variable in config[DOMAIN]['ignored_zones']:
|
||||
ignored_zones.append(variable)
|
||||
|
||||
def setup_proximity_component(hass, config):
|
||||
"""Set up individual proximity component."""
|
||||
# Get the devices from configuration.yaml.
|
||||
if 'devices' not in config[DOMAIN]:
|
||||
if 'devices' not in config:
|
||||
_LOGGER.error('devices not found in config')
|
||||
return False
|
||||
|
||||
ignored_zones = []
|
||||
if 'ignored_zones' in config:
|
||||
for variable in config['ignored_zones']:
|
||||
ignored_zones.append(variable)
|
||||
|
||||
proximity_devices = []
|
||||
for variable in config[DOMAIN]['devices']:
|
||||
for variable in config['devices']:
|
||||
proximity_devices.append(variable)
|
||||
|
||||
# Get the direction of travel tolerance from configuration.yaml.
|
||||
tolerance = config[DOMAIN].get('tolerance', DEFAULT_TOLERANCE)
|
||||
tolerance = config.get('tolerance', DEFAULT_TOLERANCE)
|
||||
|
||||
# Get the zone to monitor proximity to from configuration.yaml.
|
||||
proximity_zone = config[DOMAIN].get('zone', DEFAULT_PROXIMITY_ZONE)
|
||||
proximity_zone = config.get('zone', DEFAULT_PROXIMITY_ZONE)
|
||||
|
||||
entity_id = DOMAIN + '.' + proximity_zone
|
||||
proximity_zone = 'zone.' + proximity_zone
|
||||
# Get the unit of measurement from configuration.yaml.
|
||||
unit_of_measure = config.get(ATTR_UNIT_OF_MEASUREMENT,
|
||||
hass.config.units.length_unit)
|
||||
|
||||
state = hass.states.get(proximity_zone)
|
||||
zone_id = 'zone.{}'.format(proximity_zone)
|
||||
state = hass.states.get(zone_id)
|
||||
zone_friendly_name = (state.name).lower()
|
||||
|
||||
# Set the default values.
|
||||
dist_to_zone = 'not set'
|
||||
dir_of_travel = 'not set'
|
||||
nearest = 'not set'
|
||||
|
||||
proximity = Proximity(hass, zone_friendly_name, dist_to_zone,
|
||||
dir_of_travel, nearest, ignored_zones,
|
||||
proximity_devices, tolerance, proximity_zone)
|
||||
proximity.entity_id = entity_id
|
||||
proximity = Proximity(hass, zone_friendly_name, DEFAULT_DIST_TO_ZONE,
|
||||
DEFAULT_DIR_OF_TRAVEL, DEFAULT_NEAREST,
|
||||
ignored_zones, proximity_devices, tolerance,
|
||||
zone_id, unit_of_measure)
|
||||
proximity.entity_id = '{}.{}'.format(DOMAIN, proximity_zone)
|
||||
|
||||
proximity.update_ha_state()
|
||||
|
||||
|
@ -78,13 +89,26 @@ def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements
|
|||
return True
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Get the zones and offsets from configuration.yaml."""
|
||||
result = True
|
||||
if isinstance(config[DOMAIN], list):
|
||||
for proximity_config in config[DOMAIN]:
|
||||
if not setup_proximity_component(hass, proximity_config):
|
||||
result = False
|
||||
elif not setup_proximity_component(hass, config[DOMAIN]):
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class Proximity(Entity): # pylint: disable=too-many-instance-attributes
|
||||
"""Representation of a Proximity."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel,
|
||||
nearest, ignored_zones, proximity_devices, tolerance,
|
||||
proximity_zone):
|
||||
proximity_zone, unit_of_measure):
|
||||
"""Initialize the proximity."""
|
||||
self.hass = hass
|
||||
self.friendly_name = zone_friendly_name
|
||||
|
@ -95,6 +119,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
|
|||
self.proximity_devices = proximity_devices
|
||||
self.tolerance = tolerance
|
||||
self.proximity_zone = proximity_zone
|
||||
self.unit_of_measure = unit_of_measure
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -109,7 +134,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
|
|||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return "km"
|
||||
return self.unit_of_measure
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
|
@ -183,15 +208,16 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
|
|||
device_state.attributes['longitude'])
|
||||
|
||||
# Add the device and distance to a dictionary.
|
||||
distances_to_zone[device] = round(dist_to_zone / 1000, 1)
|
||||
distances_to_zone[device] = round(
|
||||
convert(dist_to_zone, 'm', self.unit_of_measure), 1)
|
||||
|
||||
# Loop through each of the distances collected and work out the
|
||||
# closest.
|
||||
closest_device = ''
|
||||
dist_to_zone = 1000000
|
||||
closest_device = None # type: str
|
||||
dist_to_zone = None # type: float
|
||||
|
||||
for device in distances_to_zone:
|
||||
if distances_to_zone[device] < dist_to_zone:
|
||||
if not dist_to_zone or distances_to_zone[device] < dist_to_zone:
|
||||
closest_device = device
|
||||
dist_to_zone = distances_to_zone[device]
|
||||
|
||||
|
|
|
@ -11,15 +11,18 @@ import logging
|
|||
import queue
|
||||
import threading
|
||||
import time
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
from typing import Any, Union, Optional, List
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
EVENT_TIME_CHANGED, MATCH_ALL)
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.helpers.typing import ConfigType, QueryType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
DOMAIN = "recorder"
|
||||
|
||||
|
@ -44,15 +47,16 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_INSTANCE = None
|
||||
_INSTANCE = None # type: Any
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# These classes will be populated during setup()
|
||||
# pylint: disable=invalid-name
|
||||
Session = None
|
||||
# pylint: disable=invalid-name,no-member
|
||||
Session = None # pylint: disable=no-member
|
||||
|
||||
|
||||
def execute(q):
|
||||
def execute(q: QueryType) \
|
||||
-> List[Any]: # pylint: disable=invalid-sequence-index
|
||||
"""Query the database and convert the objects to HA native form.
|
||||
|
||||
This method also retries a few times in the case of stale connections.
|
||||
|
@ -68,11 +72,11 @@ def execute(q):
|
|||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True)
|
||||
finally:
|
||||
Session().close()
|
||||
Session.close()
|
||||
return []
|
||||
|
||||
|
||||
def run_information(point_in_time=None):
|
||||
def run_information(point_in_time: Optional[datetime]=None):
|
||||
"""Return information about current run.
|
||||
|
||||
There is also the run that covers point_in_time.
|
||||
|
@ -91,7 +95,7 @@ def run_information(point_in_time=None):
|
|||
(recorder_runs.end > point_in_time)).first()
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Setup the recorder."""
|
||||
# pylint: disable=global-statement
|
||||
global _INSTANCE
|
||||
|
@ -112,30 +116,36 @@ def setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
def query(model_name, *args):
|
||||
def query(model_name: Union[str, Any], *args) -> QueryType:
|
||||
"""Helper to return a query handle."""
|
||||
_verify_instance()
|
||||
|
||||
if isinstance(model_name, str):
|
||||
return Session().query(get_model(model_name), *args)
|
||||
return Session().query(model_name, *args)
|
||||
return Session.query(get_model(model_name), *args)
|
||||
return Session.query(model_name, *args)
|
||||
|
||||
|
||||
def get_model(model_name):
|
||||
def get_model(model_name: str) -> Any:
|
||||
"""Get a model class."""
|
||||
from homeassistant.components.recorder import models
|
||||
|
||||
return getattr(models, model_name)
|
||||
try:
|
||||
return getattr(models, model_name)
|
||||
except AttributeError:
|
||||
_LOGGER.error("Invalid model name %s", model_name)
|
||||
return None
|
||||
|
||||
|
||||
def log_error(e, retry_wait=0, rollback=True,
|
||||
message="Error during query: %s"):
|
||||
def log_error(e: Exception, retry_wait: Optional[float]=0,
|
||||
rollback: Optional[bool]=True,
|
||||
message: Optional[str]="Error during query: %s") -> None:
|
||||
"""Log about SQLAlchemy errors in a sane manner."""
|
||||
import sqlalchemy.exc
|
||||
if not isinstance(e, sqlalchemy.exc.OperationalError):
|
||||
_LOGGER.exception(e)
|
||||
_LOGGER.exception(str(e))
|
||||
else:
|
||||
_LOGGER.error(message, str(e))
|
||||
if rollback:
|
||||
Session().rollback()
|
||||
Session.rollback()
|
||||
if retry_wait:
|
||||
_LOGGER.info("Retrying in %s seconds", retry_wait)
|
||||
time.sleep(retry_wait)
|
||||
|
@ -145,19 +155,20 @@ class Recorder(threading.Thread):
|
|||
"""A threaded recorder class."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, hass, purge_days, uri):
|
||||
def __init__(self, hass: HomeAssistant, purge_days: int, uri: str) \
|
||||
-> None:
|
||||
"""Initialize the recorder."""
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.hass = hass
|
||||
self.purge_days = purge_days
|
||||
self.queue = queue.Queue()
|
||||
self.queue = queue.Queue() # type: Any
|
||||
self.quit_object = object()
|
||||
self.recording_start = dt_util.utcnow()
|
||||
self.db_url = uri
|
||||
self.db_ready = threading.Event()
|
||||
self.engine = None
|
||||
self._run = None
|
||||
self.engine = None # type: Any
|
||||
self._run = None # type: Any
|
||||
|
||||
def start_recording(event):
|
||||
"""Start recording."""
|
||||
|
@ -276,7 +287,7 @@ class Recorder(threading.Thread):
|
|||
run.end = self.recording_start
|
||||
_LOGGER.warning("Ended unfinished session (id=%s from %s)",
|
||||
run.run_id, run.start)
|
||||
Session().add(run)
|
||||
Session.add(run)
|
||||
|
||||
_LOGGER.warning("Found unfinished sessions")
|
||||
|
||||
|
@ -321,7 +332,7 @@ class Recorder(threading.Thread):
|
|||
if self._commit(_purge_events):
|
||||
_LOGGER.info("Purged events created before %s", purge_before)
|
||||
|
||||
Session().expire_all()
|
||||
Session.expire_all()
|
||||
|
||||
# Execute sqlite vacuum command to free up space on disk
|
||||
if self.engine.driver == 'sqlite':
|
||||
|
@ -346,7 +357,8 @@ class Recorder(threading.Thread):
|
|||
return False
|
||||
|
||||
|
||||
def _verify_instance():
|
||||
def _verify_instance() -> None:
|
||||
"""Throw error if recorder not initialized."""
|
||||
if _INSTANCE is None:
|
||||
raise RuntimeError("Recorder not initialized.")
|
||||
_INSTANCE.block_till_db_ready()
|
||||
|
|
|
@ -9,9 +9,8 @@ from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer,
|
|||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.core import Event, EventOrigin, State
|
||||
from homeassistant.core import Event, EventOrigin, State, split_entity_id
|
||||
from homeassistant.remote import JSONEncoder
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
|
||||
# SQLAlchemy Schema
|
||||
# pylint: disable=invalid-name
|
||||
|
@ -62,7 +61,7 @@ class States(Base): # type: ignore
|
|||
__tablename__ = 'states'
|
||||
state_id = Column(Integer, primary_key=True)
|
||||
domain = Column(String(64))
|
||||
entity_id = Column(String(64))
|
||||
entity_id = Column(String(255))
|
||||
state = Column(String(255))
|
||||
attributes = Column(Text)
|
||||
event_id = Column(Integer, ForeignKey('events.event_id'))
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
|
||||
|
||||
REQUIREMENTS = ['pyRFXtrx==0.9.0']
|
||||
REQUIREMENTS = ['pyRFXtrx==0.10.1']
|
||||
|
||||
DOMAIN = "rfxtrx"
|
||||
|
||||
|
@ -66,6 +66,7 @@ def _valid_device(value, device_type):
|
|||
key = device.get('packetid')
|
||||
device.pop('packetid')
|
||||
|
||||
key = str(key)
|
||||
if not len(key) % 2 == 0:
|
||||
key = '0' + key
|
||||
|
||||
|
@ -130,7 +131,11 @@ def setup(hass, config):
|
|||
# Log RFXCOM event
|
||||
if not event.device.id_string:
|
||||
return
|
||||
_LOGGER.info("Receive RFXCOM event from %s", event.device)
|
||||
_LOGGER.info("Receive RFXCOM event from "
|
||||
"(Device_id: %s Class: %s Sub: %s)",
|
||||
slugify(event.device.id_string.lower()),
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype)
|
||||
|
||||
# Callback to HA registered components.
|
||||
for subscriber in RECEIVED_EVT_SUBSCRIBERS:
|
||||
|
@ -213,13 +218,14 @@ def get_new_device(event, config, device):
|
|||
if not config[ATTR_AUTOMATIC_ADD]:
|
||||
return
|
||||
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
_LOGGER.info(
|
||||
"Automatic add %s rfxtrx device (Class: %s Sub: %s)",
|
||||
"Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)",
|
||||
device_id,
|
||||
event.device.__class__.__name__,
|
||||
event.device.subtype
|
||||
event.device.subtype,
|
||||
pkt_id
|
||||
)
|
||||
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
|
||||
datas = {ATTR_STATE: False, ATTR_FIREEVENT: False}
|
||||
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
|
||||
new_device = device(pkt_id, event, datas,
|
||||
|
@ -236,7 +242,7 @@ def apply_received_command(event):
|
|||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"EntityID: %s device_update. Command: %s",
|
||||
"Device_id: %s device_update. Command: %s",
|
||||
device_id,
|
||||
event.values['Command']
|
||||
)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
move_up:
|
||||
description: Move up all or specified roller shutter
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of roller shutter(s) to move up
|
||||
example: 'rollershutter.living_room'
|
||||
|
||||
move_down:
|
||||
description: Move down all or specified roller shutter
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of roller shutter(s) to move down
|
||||
example: 'rollershutter.living_room'
|
||||
|
||||
stop:
|
||||
description: Stop all or specified roller shutter
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of roller shutter(s) to stop
|
||||
example: 'rollershutter.living_room'
|
|
@ -40,10 +40,14 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
|||
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
import libopenzwave
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._lozwmgr = libopenzwave.PyManager()
|
||||
self._lozwmgr.create()
|
||||
self._node = value.node
|
||||
self._current_position = None
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
|
@ -51,32 +55,53 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
|||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data change for the registered node/value pair."""
|
||||
# Position value
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Level':
|
||||
self._current_position = value.data
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""Return the current position of Zwave roller shutter."""
|
||||
if self._value.data <= 5:
|
||||
return 100
|
||||
elif self._value.data >= 95:
|
||||
return 0
|
||||
else:
|
||||
return 100 - self._value.data
|
||||
if self._current_position is not None:
|
||||
if self._current_position <= 5:
|
||||
return 100
|
||||
elif self._current_position >= 95:
|
||||
return 0
|
||||
else:
|
||||
return 100 - self._current_position
|
||||
|
||||
def move_up(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
self._node.set_dimmer(self._value.value_id, 100)
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Open':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
|
||||
def move_down(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
self._node.set_dimmer(self._value.value_id, 0)
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Close':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_BINARY).values():
|
||||
# Rollershutter will toggle between UP (True), DOWN (False).
|
||||
# It also stops the shutter if the same value is sent while moving.
|
||||
value.data = value.data
|
||||
break
|
||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
||||
and value.label == 'Open':
|
||||
self._lozwmgr.releaseButton(value.value_id)
|
||||
break
|
||||
|
|
|
@ -14,7 +14,8 @@ import voluptuous as vol
|
|||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_TOGGLE, STATE_ON, CONF_ALIAS)
|
||||
from homeassistant.helpers.entity import ToggleEntity, split_entity_id
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ from datetime import timedelta
|
|||
from homeassistant.const import TEMP_FAHRENHEIT
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.util.temperature import celsius_to_fahrenheit
|
||||
|
||||
# Update this requirement to upstream as soon as it supports Python 3.
|
||||
REQUIREMENTS = ['http://github.com/mala-zaba/Adafruit_Python_DHT/archive/'
|
||||
|
@ -32,8 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
# pylint: disable=import-error
|
||||
import Adafruit_DHT
|
||||
|
||||
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
|
||||
unit = hass.config.temperature_unit
|
||||
SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit
|
||||
available_sensors = {
|
||||
"DHT11": Adafruit_DHT.DHT11,
|
||||
"DHT22": Adafruit_DHT.DHT22,
|
||||
|
@ -58,7 +58,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
if variable not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||
else:
|
||||
dev.append(DHTSensor(data, variable, unit, name))
|
||||
dev.append(
|
||||
DHTSensor(data, variable, SENSOR_TYPES[variable][1], name))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
@ -103,7 +104,8 @@ class DHTSensor(Entity):
|
|||
if self.type == 'temperature':
|
||||
self._state = round(data['temperature'], 1)
|
||||
if self.temp_unit == TEMP_FAHRENHEIT:
|
||||
self._state = round(data['temperature'] * 1.8 + 32, 1)
|
||||
self._state = round(celsius_to_fahrenheit(data['temperature']),
|
||||
1)
|
||||
elif self.type == 'humidity':
|
||||
self._state = round(data['humidity'], 1)
|
||||
|
||||
|
|
104
homeassistant/components/sensor/fastdotcom.py
Normal file
104
homeassistant/components/sensor/fastdotcom.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
"""
|
||||
Support for Fast.com internet speed testing sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.fastdotcom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import recorder
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
|
||||
REQUIREMENTS = ['https://github.com/nkgilley/fast.com/archive/'
|
||||
'master.zip#fastdotcom==0.0.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SECOND = 'second'
|
||||
CONF_MINUTE = 'minute'
|
||||
CONF_HOUR = 'hour'
|
||||
CONF_DAY = 'day'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Fast.com sensor."""
|
||||
data = SpeedtestData(hass, config)
|
||||
sensor = SpeedtestSensor(data)
|
||||
add_devices([sensor])
|
||||
|
||||
def update(call=None):
|
||||
"""Update service for manual updates."""
|
||||
data.update(dt_util.now())
|
||||
sensor.update()
|
||||
|
||||
hass.services.register(DOMAIN, 'update_fastdotcom', update)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class SpeedtestSensor(Entity):
|
||||
"""Implementation of a FAst.com sensor."""
|
||||
|
||||
def __init__(self, speedtest_data):
|
||||
"""Initialize the sensor."""
|
||||
self._name = 'Fast.com Speedtest'
|
||||
self.speedtest_client = speedtest_data
|
||||
self._state = None
|
||||
self._unit_of_measurement = 'Mbps'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
data = self.speedtest_client.data
|
||||
if data is None:
|
||||
entity_id = 'sensor.fastcom_speedtest'
|
||||
states = recorder.get_model('States')
|
||||
try:
|
||||
last_state = recorder.execute(
|
||||
recorder.query('States').filter(
|
||||
(states.entity_id == entity_id) &
|
||||
(states.last_changed == states.last_updated) &
|
||||
(states.state != 'unknown')
|
||||
).order_by(states.state_id.desc()).limit(1))
|
||||
except TypeError:
|
||||
return
|
||||
if not last_state:
|
||||
return
|
||||
self._state = last_state[0].state
|
||||
else:
|
||||
self._state = data['download']
|
||||
|
||||
|
||||
class SpeedtestData(object):
|
||||
"""Get the latest data from fast.com."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the data object."""
|
||||
self.data = None
|
||||
track_time_change(hass, self.update,
|
||||
second=config.get(CONF_SECOND, 0),
|
||||
minute=config.get(CONF_MINUTE, 0),
|
||||
hour=config.get(CONF_HOUR, None),
|
||||
day=config.get(CONF_DAY, None))
|
||||
|
||||
def update(self, now):
|
||||
"""Get the latest data from fast.com."""
|
||||
from fastdotcom import fast_com
|
||||
_LOGGER.info('Executing fast.com speedtest')
|
||||
self.data = {'download': fast_com()}
|
|
@ -10,7 +10,6 @@ import logging
|
|||
import datetime
|
||||
import time
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
@ -233,13 +232,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
authd_client.client.refresh_token()
|
||||
|
||||
authd_client.system = authd_client.user_profile_get()["user"]["locale"]
|
||||
if authd_client.system != 'en_GB':
|
||||
if hass.config.units.is_metric:
|
||||
authd_client.system = "metric"
|
||||
else:
|
||||
authd_client.system = "en_US"
|
||||
|
||||
dev = []
|
||||
for resource in config.get("monitored_resources",
|
||||
FITBIT_DEFAULT_RESOURCE_LIST):
|
||||
dev.append(FitbitSensor(authd_client, config_path, resource,
|
||||
hass.config.temperature_unit ==
|
||||
TEMP_CELSIUS))
|
||||
hass.config.units.is_metric))
|
||||
add_devices(dev)
|
||||
|
||||
else:
|
||||
|
|
|
@ -10,7 +10,7 @@ from requests.exceptions import ConnectionError as ConnectError, \
|
|||
HTTPError, Timeout
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
@ -62,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
if 'units' in config:
|
||||
units = config['units']
|
||||
elif hass.config.temperature_unit == TEMP_CELSIUS:
|
||||
elif hass.config.units.is_metric:
|
||||
units = 'si'
|
||||
else:
|
||||
units = 'us'
|
||||
|
|
|
@ -11,8 +11,7 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE)
|
||||
CONF_API_KEY, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE)
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -92,10 +91,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
options = config.get(CONF_OPTIONS)
|
||||
|
||||
if options.get('units') is None:
|
||||
if hass.config.temperature_unit is TEMP_CELSIUS:
|
||||
options['units'] = 'metric'
|
||||
elif hass.config.temperature_unit is TEMP_FAHRENHEIT:
|
||||
options['units'] = 'imperial'
|
||||
options['units'] = hass.config.units.name
|
||||
|
||||
travel_mode = config.get(CONF_TRAVEL_MODE)
|
||||
mode = options.get(CONF_MODE)
|
||||
|
|
111
homeassistant/components/sensor/gpsd.py
Normal file
111
homeassistant/components/sensor/gpsd.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
"""
|
||||
Support for GPSD.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.gpsd/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE, STATE_UNKNOWN,
|
||||
CONF_HOST, CONF_PORT, CONF_PLATFORM,
|
||||
CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['gps3==0.33.2']
|
||||
|
||||
DEFAULT_NAME = 'GPS'
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 2947
|
||||
|
||||
ATTR_GPS_TIME = 'gps_time'
|
||||
ATTR_ELEVATION = 'elevation'
|
||||
ATTR_SPEED = 'speed'
|
||||
ATTR_CLIMB = 'climb'
|
||||
ATTR_MODE = 'mode'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'gpsd',
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the GPSD component."""
|
||||
name = config.get(CONF_NAME, DEFAULT_NAME)
|
||||
host = config.get(CONF_HOST, DEFAULT_HOST)
|
||||
port = config.get(CONF_PORT, DEFAULT_PORT)
|
||||
|
||||
# Will hopefully be possible with the next gps3 update
|
||||
# https://github.com/wadda/gps3/issues/11
|
||||
# from gps3 import gps3
|
||||
# try:
|
||||
# gpsd_socket = gps3.GPSDSocket()
|
||||
# gpsd_socket.connect(host=host, port=port)
|
||||
# except GPSError:
|
||||
# _LOGGER.warning('Not able to connect to GPSD')
|
||||
# return False
|
||||
import socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect((host, port))
|
||||
sock.shutdown(2)
|
||||
_LOGGER.debug('Connection to GPSD possible')
|
||||
except socket.error:
|
||||
_LOGGER.error('Not able to connect to GPSD')
|
||||
return False
|
||||
|
||||
add_devices([GpsdSensor(hass, name, host, port)])
|
||||
|
||||
|
||||
class GpsdSensor(Entity):
|
||||
"""Representation of a GPS receiver available via GPSD."""
|
||||
|
||||
def __init__(self, hass, name, host, port):
|
||||
"""Initialize the GPSD sensor."""
|
||||
from gps3.agps3threaded import AGPS3mechanism
|
||||
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._host = host
|
||||
self._port = port
|
||||
|
||||
self.agps_thread = AGPS3mechanism()
|
||||
self.agps_thread.stream_data(host=self._host, port=self._port)
|
||||
self.agps_thread.run_thread()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return self._name
|
||||
|
||||
# pylint: disable=no-member
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of GPSD."""
|
||||
if self.agps_thread.data_stream.mode == 3:
|
||||
return "3D Fix"
|
||||
elif self.agps_thread.data_stream.mode == 2:
|
||||
return "2D Fix"
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes of the GPS."""
|
||||
return {
|
||||
ATTR_LATITUDE: self.agps_thread.data_stream.lat,
|
||||
ATTR_LONGITUDE: self.agps_thread.data_stream.lon,
|
||||
ATTR_ELEVATION: self.agps_thread.data_stream.alt,
|
||||
ATTR_GPS_TIME: self.agps_thread.data_stream.time,
|
||||
ATTR_SPEED: self.agps_thread.data_stream.speed,
|
||||
ATTR_CLIMB: self.agps_thread.data_stream.climb,
|
||||
ATTR_MODE: self.agps_thread.data_stream.mode,
|
||||
}
|
|
@ -65,7 +65,7 @@ class MoldIndicator(Entity):
|
|||
self._indoor_humidity_sensor = indoor_humidity_sensor
|
||||
self._outdoor_temp_sensor = outdoor_temp_sensor
|
||||
self._calib_factor = calib_factor
|
||||
self._is_metric = (hass.config.temperature_unit == TEMP_CELSIUS)
|
||||
self._is_metric = hass.config.units.is_metric
|
||||
|
||||
self._dewpoint = None
|
||||
self._indoor_temp = None
|
||||
|
@ -109,7 +109,7 @@ class MoldIndicator(Entity):
|
|||
|
||||
# convert to celsius if necessary
|
||||
if unit == TEMP_FAHRENHEIT:
|
||||
return util.temperature.fahrenheit_to_celcius(temp)
|
||||
return util.temperature.fahrenheit_to_celsius(temp)
|
||||
elif unit == TEMP_CELSIUS:
|
||||
return temp
|
||||
else:
|
||||
|
@ -260,9 +260,9 @@ class MoldIndicator(Entity):
|
|||
else:
|
||||
return {
|
||||
ATTR_DEWPOINT:
|
||||
util.temperature.celcius_to_fahrenheit(
|
||||
util.temperature.celsius_to_fahrenheit(
|
||||
self._dewpoint),
|
||||
ATTR_CRITICAL_TEMP:
|
||||
util.temperature.celcius_to_fahrenheit(
|
||||
util.temperature.celsius_to_fahrenheit(
|
||||
self._crit_temp),
|
||||
}
|
||||
|
|
|
@ -95,6 +95,9 @@ class OctoPrintSensor(Entity):
|
|||
"""Return the state of the sensor."""
|
||||
sensor_unit = self.unit_of_measurement
|
||||
if sensor_unit == TEMP_CELSIUS or sensor_unit == "%":
|
||||
# API sometimes returns null and not 0
|
||||
if self._state is None:
|
||||
self._state = 0
|
||||
return round(self._state, 2)
|
||||
else:
|
||||
return self._state
|
||||
|
|
74
homeassistant/components/sensor/ohmconnect.py
Normal file
74
homeassistant/components/sensor/ohmconnect.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
"""
|
||||
Support for OhmConnect.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/sensor.ohmconnect/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import xml.etree.ElementTree as ET
|
||||
import requests
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the OhmConnect sensors."""
|
||||
ohmid = config.get("id")
|
||||
if ohmid is None:
|
||||
_LOGGER.error("You must provide your OhmConnect ID!")
|
||||
return False
|
||||
|
||||
add_devices([OhmconnectSensor(config.get("name", "OhmConnect Status"),
|
||||
ohmid)])
|
||||
|
||||
|
||||
class OhmconnectSensor(Entity):
|
||||
"""Representation of a OhmConnect sensor."""
|
||||
|
||||
def __init__(self, name, ohmid):
|
||||
"""Initialize the sensor."""
|
||||
self._name = name
|
||||
self._ohmid = ohmid
|
||||
self._data = {}
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._data.get("active") == "True":
|
||||
return "Active"
|
||||
else:
|
||||
return "Inactive"
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {"Address": self._data.get("address"), "ID": self._ohmid}
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from OhmConnect."""
|
||||
try:
|
||||
url = ("https://login.ohmconnect.com"
|
||||
"/verify-ohm-hour/{}").format(self._ohmid)
|
||||
response = requests.get(url, timeout=10)
|
||||
root = ET.fromstring(response.text)
|
||||
|
||||
for child in root:
|
||||
self._data[child.tag] = child.text
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to host/endpoint: %s", url)
|
||||
self.data = {}
|
|
@ -48,8 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
from pyowm import OWM
|
||||
|
||||
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
|
||||
unit = hass.config.temperature_unit
|
||||
SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit
|
||||
forecast = config.get('forecast')
|
||||
owm = OWM(config.get(CONF_API_KEY, None))
|
||||
|
||||
|
@ -67,13 +66,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
if variable not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||
else:
|
||||
dev.append(OpenWeatherMapSensor(data, variable, unit))
|
||||
dev.append(OpenWeatherMapSensor(data, variable,
|
||||
SENSOR_TYPES[variable][1]))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if forecast:
|
||||
SENSOR_TYPES['forecast'] = ['Forecast', None]
|
||||
dev.append(OpenWeatherMapSensor(data, 'forecast', unit))
|
||||
dev.append(OpenWeatherMapSensor(data, 'forecast',
|
||||
SENSOR_TYPES['temperature'][1]))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['plexapi==1.1.0']
|
||||
REQUIREMENTS = ['plexapi==2.0.2']
|
||||
|
||||
CONF_SERVER = 'server'
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||
|
@ -54,15 +54,18 @@ class PlexSensor(Entity):
|
|||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, plex_url, plex_user, plex_password, plex_server):
|
||||
"""Initialize the sensor."""
|
||||
from plexapi.utils import NA
|
||||
|
||||
self._na_type = NA
|
||||
self._name = name
|
||||
self._state = 0
|
||||
self._now_playing = []
|
||||
|
||||
if plex_user and plex_password:
|
||||
from plexapi.myplex import MyPlexUser
|
||||
user = MyPlexUser.signin(plex_user, plex_password)
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
user = MyPlexAccount.signin(plex_user, plex_password)
|
||||
server = plex_server if plex_server else user.resources()[0].name
|
||||
self._server = user.getResource(server).connect()
|
||||
self._server = user.resource(server).connect()
|
||||
else:
|
||||
from plexapi.server import PlexServer
|
||||
self._server = PlexServer(plex_url)
|
||||
|
@ -93,7 +96,11 @@ class PlexSensor(Entity):
|
|||
def update(self):
|
||||
"""Update method for plex sensor."""
|
||||
sessions = self._server.sessions()
|
||||
now_playing = [(s.user.title, "{0} ({1})".format(s.title, s.year))
|
||||
for s in sessions]
|
||||
now_playing = []
|
||||
for sess in sessions:
|
||||
user = sess.user.title if sess.user is not self._na_type else ""
|
||||
title = sess.title if sess.title is not self._na_type else ""
|
||||
year = sess.year if sess.year is not self._na_type else ""
|
||||
now_playing.append((user, "{0} ({1})".format(title, year)))
|
||||
self._state = len(sessions)
|
||||
self._now_playing = now_playing
|
||||
|
|
94
homeassistant/components/sensor/serial_pm.py
Normal file
94
homeassistant/components/sensor/serial_pm.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
"""
|
||||
Support for particulate matter sensors connected to a serial port.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.serial_pm/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
|
||||
REQUIREMENTS = ['pmsensor==0.2']
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SERIAL_DEVICE = "serial_device"
|
||||
CONF_BRAND = "brand"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PLATFORM): 'serial_pm',
|
||||
vol.Optional(CONF_NAME, default=""): cv.string,
|
||||
vol.Required(CONF_SERIAL_DEVICE): cv.string,
|
||||
vol.Required(CONF_BRAND): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the available PM sensors."""
|
||||
from pmsensor import serial_data_collector as pm
|
||||
|
||||
try:
|
||||
coll = pm.PMDataCollector(config.get(CONF_SERIAL_DEVICE),
|
||||
pm.SUPPORTED_SENSORS[config.get(CONF_BRAND)])
|
||||
except KeyError:
|
||||
_LOGGER.error("Brand %s not supported\n supported brands: %s",
|
||||
config.get(CONF_BRAND), pm.SUPPORTED_SENSORS.keys())
|
||||
return
|
||||
except OSError as err:
|
||||
_LOGGER.error("Could not open serial connection to %s (%s)",
|
||||
config.get(CONF_SERIAL_DEVICE), err)
|
||||
return
|
||||
|
||||
dev = []
|
||||
|
||||
for pmname in coll.supported_values():
|
||||
if config.get("name") != "":
|
||||
name = "{} PM{}".format(config.get("name"), pmname)
|
||||
else:
|
||||
name = "PM{}".format(pmname)
|
||||
dev.append(ParticulateMatterSensor(coll, name, pmname))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
class ParticulateMatterSensor(Entity):
|
||||
"""Representation of an Particulate matter sensor."""
|
||||
|
||||
def __init__(self, pmDataCollector, name, pmname):
|
||||
"""Initialize a new PM sensor."""
|
||||
self._name = name
|
||||
self._pmname = pmname
|
||||
self._state = None
|
||||
self._collector = pmDataCollector
|
||||
|
||||
@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 "µg/m³"
|
||||
|
||||
def update(self):
|
||||
"""Read from sensor and update the state."""
|
||||
_LOGGER.debug("Reading data from PM sensor")
|
||||
try:
|
||||
self._state = self._collector.read_data()[self._pmname]
|
||||
except KeyError:
|
||||
_LOGGER.error("Could not read PM%s value", self._pmname)
|
||||
|
||||
def should_poll(self):
|
||||
"""Sensor needs polling."""
|
||||
return True
|
|
@ -21,7 +21,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
"""Setup the Temper sensors."""
|
||||
from temperusb.temper import TemperHandler
|
||||
|
||||
temp_unit = hass.config.temperature_unit
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME)
|
||||
temper_devices = TemperHandler().get_devices()
|
||||
add_devices_callback([TemperSensor(dev, temp_unit, name + '_' + str(idx))
|
||||
|
|
|
@ -51,7 +51,8 @@ class VeraSensor(VeraDevice, Entity):
|
|||
def update(self):
|
||||
"""Update the state."""
|
||||
if self.vera_device.category == "Temperature Sensor":
|
||||
current_temp = self.vera_device.temperature
|
||||
self.current_value = self.vera_device.temperature
|
||||
|
||||
vera_temp_units = (
|
||||
self.vera_device.vera_controller.temperature_units)
|
||||
|
||||
|
@ -60,14 +61,6 @@ class VeraSensor(VeraDevice, Entity):
|
|||
else:
|
||||
self._temperature_units = TEMP_CELSIUS
|
||||
|
||||
if self.hass:
|
||||
temp = self.hass.config.temperature(
|
||||
current_temp,
|
||||
self._temperature_units)
|
||||
|
||||
current_temp, self._temperature_units = temp
|
||||
|
||||
self.current_value = current_temp
|
||||
elif self.vera_device.category == "Light Sensor":
|
||||
self.current_value = self.vera_device.light
|
||||
elif self.vera_device.category == "Humidity Sensor":
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue