Merge pull request #2654 from home-assistant/dev

0.25
This commit is contained in:
Paulus Schoutsen 2016-07-30 11:33:41 -07:00 committed by GitHub
commit 06a68d0c62
176 changed files with 3241 additions and 1340 deletions

View File

@ -91,6 +91,7 @@ omit =
homeassistant/components/knx.py
homeassistant/components/switch/knx.py
homeassistant/components/binary_sensor/knx.py
homeassistant/components/thermostat/knx.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py
@ -129,21 +130,25 @@ omit =
homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py
homeassistant/components/light/blinksticklight.py
homeassistant/components/light/flux_led.py
homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/osramlightify.py
homeassistant/components/light/x10.py
homeassistant/components/lirc.py
homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/directv.py
homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/mpchc.py
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
@ -151,6 +156,7 @@ omit =
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rnet.py
homeassistant/components/media_player/samsungtv.py
homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py
@ -161,7 +167,6 @@ omit =
homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py
homeassistant/components/notify/googlevoice.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/message_bird.py

5
.gitignore vendored
View File

@ -7,9 +7,10 @@ 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
tests/config/deps
tests/config/home-assistant.log
tests/testing_config/deps
tests/testing_config/home-assistant.log
# Hide sublime text stuff
*.sublime-project

View File

@ -8,8 +8,13 @@ matrix:
env: TOXENV=requirements
- python: "3.5"
env: TOXENV=lint
- python: "3.5"
env: TOXENV=typing
- python: "3.5"
env: TOXENV=py35
allow_failures:
- python: "3.5"
env: TOXENV=typing
cache:
directories:
- $HOME/.cache/pip

View File

@ -20,7 +20,8 @@ RUN script/build_python_openzwave && \
COPY requirements_all.txt requirements_all.txt
# certifi breaks Debian based installs
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
pip3 install mysqlclient psycopg2
# Copy source
COPY . .

View File

@ -67,7 +67,7 @@ Build home automation on top of your devices:
- Turn on the lights when people get home after sunset
- Turn on lights slowly during sunset to compensate for less light
- Turn off all lights and devices when everybody leaves the house
- Offers a `REST API <https://home-assistant.io/developers/api/>`__
- Offers a `REST API <https://home-assistant.io/developers/rest_api/>`__
and can interface with MQTT for easy integration with other projects
like `OwnTracks <http://owntracks.org/>`__
- Allow sending notifications using

View File

@ -0,0 +1,30 @@
"""
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

View File

@ -0,0 +1,415 @@
<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>
<!-- for development, replace with:
<script src="https://fb.me/react-15.2.1.js"></script>
<script src="https://fb.me/react-dom-15.2.1.js"></script>
-->
<!--
CSS taken from ReactJS TodoMVC example by Pete Hunt
http://todomvc.com/examples/react/
-->
<style>
.todoapp input[type="checkbox"] {
outline: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.todoapp .main {
position: relative;
border-top: 1px solid #e6e6e6;
}
.todoapp .todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todoapp .todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todoapp .todo-list li:last-child {
border-bottom: none;
}
.todoapp .todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
.todoapp .todo-list li .toggle:focus {
border-left: 3px solid rgba(175, 47, 47, 0.35);
}
.todoapp .todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todoapp .todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todoapp .todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todoapp .todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todoapp .footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.todoapp .footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todoapp .todo-count {
float: left;
text-align: left;
font-weight: 300;
}
.todoapp .toggle-menu {
position: absolute;
right: 15px;
font-weight: 300;
color: rgba(175, 47, 47, 0.75);
}
.todoapp .filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.todoapp .filters li {
display: inline;
}
.todoapp .filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.todoapp .filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.todoapp .filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.todoapp .toggle-all,
.todoapp .todo-list li .toggle {
background: none;
}
.todoapp .todo-list li .toggle {
height: 40px;
}
.todoapp .toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.todoapp .footer {
height: 50px;
}
.todoapp .filters {
bottom: 10px;
}
}
</style>
<dom-module id='ha-panel-react'>
<template>
<style>
:host {
background: #f5f5f5;
display: block;
height: 100%;
overflow: auto;
}
.mount {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
</style>
<div id='mount' class='mount'></div>
</template>
</dom-module>
<script>
// Example uses ES6. Will only work in modern browsers
class TodoMVC extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: 'all',
// load initial value of entities
entities: this.props.hass.reactor.evaluate(
this.props.hass.entityGetters.visibleEntityMap),
};
}
componentDidMount() {
// register to entity updates
this._unwatchHass = this.props.hass.reactor.observe(
this.props.hass.entityGetters.visibleEntityMap,
entities => this.setState({entities}))
}
componentWillUnmount() {
// unregister to entity updates
this._unwatchHass();
}
handlePickFilter(filter, ev) {
ev.preventDefault();
this.setState({filter});
}
handleEntityToggle(entity, ev) {
this.props.hass.serviceActions.callService(
entity.domain, 'toggle', { entity_id: entity.entityId });
}
handleToggleMenu(ev) {
ev.preventDefault();
Polymer.Base.fire('open-menu', null, {node: ev.target});
}
entityRow(entity) {
const completed = entity.state === 'on';
return React.createElement(
'li', {
className: completed && 'completed',
key: entity.entityId,
},
React.createElement(
"div", { className: "view" },
React.createElement(
"input", {
checked: completed,
className: "toggle",
type: "checkbox",
onChange: ev => this.handleEntityToggle(entity, ev),
}),
React.createElement("label", null, entity.entityDisplay)));
}
filterRow(filter) {
return React.createElement(
"li", { key: filter },
React.createElement(
"a", {
href: "#",
className: this.state.filter === filter && "selected",
onClick: ev => this.handlePickFilter(filter, ev),
},
filter.substring(0, 1).toUpperCase() + filter.substring(1)
)
);
}
render() {
const { entities, filter } = this.state;
if (!entities) return null;
const filters = ['all', 'light', 'switch'];
const showEntities = filter === 'all' ?
entities.filter(ent => filters.includes(ent.domain)) :
entities.filter(ent => ent.domain == filter);
return React.createElement(
'div', { className: 'todoapp-wrapper' },
React.createElement(
"section", { className: "todoapp" },
React.createElement(
"div", null,
React.createElement(
"header", { className: "header" },
React.createElement("h1", null, this.props.title || "todos")
),
React.createElement(
"section", { className: "main" },
React.createElement(
"ul", { className: "todo-list" },
showEntities.valueSeq().map(ent => this.entityRow(ent)))
)
),
React.createElement(
"footer", { className: "footer" },
React.createElement(
"span", { className: "todo-count" },
showEntities.filter(ent => ent.state === 'off').size + " items left"
),
React.createElement(
"ul", { className: "filters" },
filters.map(filter => this.filterRow(filter))
),
!this.props.showMenu && React.createElement(
"a", {
className: "toggle-menu",
href: '#',
onClick: ev => this.handleToggleMenu(ev),
},
"Show menu"
)
)
));
}
}
Polymer({
is: 'ha-panel-react',
properties: {
// Home Assistant object
hass: {
type: Object,
},
// If should render in narrow mode
narrow: {
type: Boolean,
value: false,
},
// If sidebar is currently shown
showMenu: {
type: Boolean,
value: false,
},
// Home Assistant panel info
// panel.config contains config passed to register_panel serverside
panel: {
type: Object,
}
},
// This will make sure we forward changed properties to React
observers: [
'propsChanged(hass, narrow, showMenu, panel)',
],
// Mount React when element attached
attached: function () {
this.mount(this.hass, this.narrow, this.showMenu, this.panel);
},
// Called when properties change
propsChanged: function (hass, narrow, showMenu, panel) {
this.mount(hass, narrow, showMenu, panel);
},
// Render React. Debounce in case multiple properties change.
mount: function (hass, narrow, showMenu, panel) {
this.debounce('mount', function () {
ReactDOM.render(React.createElement(TodoMVC, {
hass: hass,
narrow: narrow,
showMenu: showMenu,
title: panel.config ? panel.config.title : null
}), this.$.mount);
}.bind(this));
},
// Unmount React node when panel no longer in use.
detached: function () {
ReactDOM.unmountComponentAtNode(this.$.mount);
},
});
</script>

View File

@ -8,6 +8,8 @@ import subprocess
import sys
import threading
from typing import Optional, List
from homeassistant.const import (
__version__,
EVENT_HOMEASSISTANT_START,
@ -16,7 +18,7 @@ from homeassistant.const import (
)
def validate_python():
def validate_python() -> None:
"""Validate we're running the right Python version."""
major, minor = sys.version_info[:2]
req_major, req_minor = REQUIRED_PYTHON_VER
@ -27,7 +29,7 @@ def validate_python():
sys.exit(1)
def ensure_config_path(config_dir):
def ensure_config_path(config_dir: str) -> None:
"""Validate the configuration directory."""
import homeassistant.config as config_util
lib_dir = os.path.join(config_dir, 'deps')
@ -56,7 +58,7 @@ def ensure_config_path(config_dir):
sys.exit(1)
def ensure_config_file(config_dir):
def ensure_config_file(config_dir: str) -> str:
"""Ensure configuration file exists."""
import homeassistant.config as config_util
config_path = config_util.ensure_config_exists(config_dir)
@ -68,7 +70,7 @@ def ensure_config_file(config_dir):
return config_path
def get_arguments():
def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
import homeassistant.config as config_util
parser = argparse.ArgumentParser(
@ -125,12 +127,12 @@ def get_arguments():
arguments = parser.parse_args()
if os.name != "posix" or arguments.debug or arguments.runner:
arguments.daemon = False
setattr(arguments, 'daemon', False)
return arguments
def daemonize():
def daemonize() -> None:
"""Move current process to daemon process."""
# Create first fork
pid = os.fork()
@ -155,7 +157,7 @@ def daemonize():
os.dup2(outfd.fileno(), sys.stderr.fileno())
def check_pid(pid_file):
def check_pid(pid_file: str) -> None:
"""Check that HA is not already running."""
# Check pid file
try:
@ -177,7 +179,7 @@ def check_pid(pid_file):
sys.exit(1)
def write_pid(pid_file):
def write_pid(pid_file: str) -> None:
"""Create a PID File."""
pid = os.getpid()
try:
@ -187,7 +189,7 @@ def write_pid(pid_file):
sys.exit(1)
def closefds_osx(min_fd, max_fd):
def closefds_osx(min_fd: int, max_fd: int) -> None:
"""Make sure file descriptors get closed when we restart.
We cannot call close on guarded fds, and we cannot easily test which fds
@ -205,7 +207,7 @@ def closefds_osx(min_fd, max_fd):
pass
def cmdline():
def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance."""
if sys.argv[0].endswith('/__main__.py'):
modulepath = os.path.dirname(sys.argv[0])
@ -213,16 +215,17 @@ def cmdline():
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
def setup_and_run_hass(config_dir, args):
def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> Optional[int]:
"""Setup HASS and run."""
from homeassistant import bootstrap
# Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv:
args = cmdline() + ['--runner']
nt_args = cmdline() + ['--runner']
while True:
try:
subprocess.check_call(args)
subprocess.check_call(nt_args)
sys.exit(0)
except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE:
@ -244,7 +247,7 @@ def setup_and_run_hass(config_dir, args):
log_rotate_days=args.log_rotate_days)
if hass is None:
return
return None
if args.open_ui:
def open_browser(event):
@ -261,7 +264,7 @@ def setup_and_run_hass(config_dir, args):
return exit_code
def try_to_restart():
def try_to_restart() -> None:
"""Attempt to clean up state and start a new homeassistant instance."""
# Things should be mostly shut down already at this point, now just try
# to clean up things that may have been left behind.
@ -303,7 +306,7 @@ def try_to_restart():
os.execv(args[0], args)
def main():
def main() -> int:
"""Start Home Assistant."""
validate_python()

View File

@ -7,6 +7,9 @@ import sys
from collections import defaultdict
from threading import RLock
from types import ModuleType
from typing import Any, Optional, Dict
import voluptuous as vol
import homeassistant.components as core_components
@ -30,7 +33,8 @@ ATTR_COMPONENT = 'component'
ERROR_LOG_FILENAME = 'home-assistant.log'
def setup_component(hass, domain, config=None):
def setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies."""
if domain in hass.config.components:
return True
@ -53,7 +57,8 @@ def setup_component(hass, domain, config=None):
return True
def _handle_requirements(hass, component, name):
def _handle_requirements(hass: core.HomeAssistant, component,
name: str) -> bool:
"""Install the requirements for a component."""
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
return True
@ -67,9 +72,10 @@ def _handle_requirements(hass, component, name):
return True
def _setup_component(hass, domain, config):
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
"""Setup a component for Home Assistant."""
# pylint: disable=too-many-return-statements,too-many-branches
# pylint: disable=too-many-statements
if domain in hass.config.components:
return True
@ -147,9 +153,15 @@ def _setup_component(hass, domain, config):
_CURRENT_SETUP.append(domain)
try:
if not component.setup(hass, config):
result = component.setup(hass, config)
if result is False:
_LOGGER.error('component %s failed to initialize', domain)
return False
elif result is not True:
_LOGGER.error('component %s did not return boolean if setup '
'was successful. Disabling component.', domain)
loader.set_component(domain, None)
return False
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error during setup of component %s', domain)
return False
@ -169,7 +181,8 @@ def _setup_component(hass, domain, config):
return True
def prepare_setup_platform(hass, config, domain, platform_name):
def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) -> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup."""
_ensure_loader_prepared(hass)
@ -202,9 +215,14 @@ def prepare_setup_platform(hass, config, domain, platform_name):
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
verbose=False, skip_pip=False,
log_rotate_days=None):
def from_config_dict(config: Dict[str, Any],
hass: Optional[core.HomeAssistant]=None,
config_dir: Optional[str]=None,
enable_log: bool=True,
verbose: bool=False,
skip_pip: bool=False,
log_rotate_days: Any=None) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a config dict.
Dynamically loads required components and its dependencies.
@ -266,8 +284,11 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
return hass
def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
log_rotate_days=None):
def from_config_file(config_path: str,
hass: Optional[core.HomeAssistant]=None,
verbose: bool=False,
skip_pip: bool=True,
log_rotate_days: Any=None):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@ -292,7 +313,8 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
skip_pip=skip_pip)
def enable_logging(hass, verbose=False, log_rotate_days=None):
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
log_rotate_days=None) -> None:
"""Setup the logging."""
logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
@ -343,12 +365,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None):
'Unable to setup error log %s (access denied)', err_log_path)
def _ensure_loader_prepared(hass):
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
"""Ensure Home Assistant loader is prepared."""
if not loader.PREPARED:
loader.prepare(hass)
def _mount_local_lib_path(config_dir):
def _mount_local_lib_path(config_dir: str) -> None:
"""Add local library to Python Path."""
sys.path.insert(0, os.path.join(config_dir, 'deps'))

View File

@ -6,9 +6,6 @@ https://home-assistant.io/components/binary_sensor.vera/
"""
import logging
import homeassistant.util.dt as dt_util
from homeassistant.const import (
ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED)
from homeassistant.components.binary_sensor import (
BinarySensorDevice)
from homeassistant.components.vera import (
@ -34,30 +31,6 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
self._state = False
VeraDevice.__init__(self, vera_device, controller)
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable:
armed = self.vera_device.is_armed
attr[ATTR_ARMED] = 'True' if armed else 'False'
if self.vera_device.is_trippable:
last_tripped = self.vera_device.last_trip
if last_tripped is not None:
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat()
else:
attr[ATTR_LAST_TRIP_TIME] = None
tripped = self.vera_device.is_tripped
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
@property
def is_on(self):
"""Return true if sensor is on."""

View File

@ -13,14 +13,15 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {
"opened": "opening",
"brightness": "light",
"vibration": "vibration",
"loudness": "sound"
"loudness": "sound",
"liquid_detected": "moisture"
}
@ -74,6 +75,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
return self.wink.vibration_boolean()
elif self.capability == "brightness":
return self.wink.brightness_boolean()
elif self.capability == "liquid_detected":
return self.wink.liquid_boolean()
else:
return self.wink.state()

View File

@ -94,7 +94,8 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_ha_state()

View File

@ -13,7 +13,8 @@ ATTR_URL = 'url'
ATTR_URL_DEFAULT = 'https://www.google.com'
SERVICE_BROWSE_URL_SCHEMA = vol.Schema({
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url,
# pylint: disable=no-value-for-parameter
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(),
})

View File

@ -5,17 +5,28 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.icloud/
"""
import logging
import re
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_START)
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.util import slugify
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT,
PLATFORM_SCHEMA)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyicloud==0.8.3']
REQUIREMENTS = ['pyicloud==0.9.1']
CONF_INTERVAL = 'interval'
DEFAULT_INTERVAL = 8
KEEPALIVE_INTERVAL = 4
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): vol.Coerce(str),
vol.Required(CONF_PASSWORD): vol.Coerce(str),
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int),
vol.Range(min=1))
})
def setup_scanner(hass, config, see):
@ -23,63 +34,67 @@ def setup_scanner(hass, config, see):
from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloudFailedLoginException
from pyicloud.exceptions import PyiCloudNoDevicesException
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
# Get the username and password from the configuration.
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if username is None or password is None:
_LOGGER.error('Must specify a username and password')
return False
username = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
try:
_LOGGER.info('Logging into iCloud Account')
# Attempt the login to iCloud
api = PyiCloudService(username,
password,
verify=True)
api = PyiCloudService(username, password, verify=True)
except PyiCloudFailedLoginException as error:
_LOGGER.exception('Error logging into iCloud Service: %s', error)
return False
def keep_alive(now):
"""Keep authenticating iCloud connection."""
"""Keep authenticating iCloud connection.
The session timeouts if we are not using it so we
have to re-authenticate & this will send an email.
"""
api.authenticate()
_LOGGER.info("Authenticate against iCloud")
track_utc_time_change(hass, keep_alive, second=0)
seen_devices = {}
def update_icloud(now):
"""Authenticate against iCloud and scan for devices."""
try:
# The session timeouts if we are not using it so we
# have to re-authenticate. This will send an email.
api.authenticate()
keep_alive(None)
# Loop through every device registered with the iCloud account
for device in api.devices:
status = device.status()
dev_id = slugify(status['name'].replace(' ', '', 99))
# An entity will not be created by see() when track=false in
# 'known_devices.yaml', but we need to see() it at least once
entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id))
if entity is None and dev_id in seen_devices:
continue
seen_devices[dev_id] = True
location = device.location()
# If the device has a location add it. If not do nothing
if location:
see(
dev_id=re.sub(r"(\s|\W|')",
'',
status['name']),
dev_id=dev_id,
host_name=status['name'],
gps=(location['latitude'], location['longitude']),
battery=status['batteryLevel']*100,
gps_accuracy=location['horizontalAccuracy']
)
else:
# No location found for the device so continue
continue
except PyiCloudNoDevicesException:
_LOGGER.info('No iCloud Devices found!')
track_utc_time_change(
hass, update_icloud,
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)),
second=0
)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
# Schedule keepalives between the updates
keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL)
if x not in update_minutes)
track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes)
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes)
return True

View File

@ -13,7 +13,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover
DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.6.7']
REQUIREMENTS = ['netdisco==0.7.0']
SCAN_INTERVAL = 300 # seconds
@ -30,6 +30,7 @@ SERVICE_HANDLERS = {
'roku': ('media_player', 'roku'),
'sonos': ('media_player', 'sonos'),
'logitech_mediaserver': ('media_player', 'squeezebox'),
'directv': ('media_player', 'directv'),
}

View File

@ -24,7 +24,8 @@ ATTR_URL = "url"
ATTR_SUBDIR = "subdir"
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
vol.Required(ATTR_URL): vol.Url,
# pylint: disable=no-value-for-parameter
vol.Required(ATTR_URL): vol.Url(),
vol.Optional(ATTR_SUBDIR): cv.string,
})

View File

@ -1,37 +1,130 @@
"""Handle the frontend for Home Assistant."""
import hashlib
import logging
import os
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView
from . import version, mdi_version
from .version import FINGERPRINTS
DOMAIN = 'frontend'
DEPENDENCIES = ['api']
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
PANELS = {}
# To keep track we don't register a component twice (gives a warning)
_REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__)
def register_built_in_panel(hass, component_name, title=None, icon=None,
url_name=None, config=None):
"""Register a built-in panel."""
# pylint: disable=too-many-arguments
path = 'panels/ha-panel-{}.html'.format(component_name)
if hass.wsgi.development:
url = ('/static/home-assistant-polymer/panels/'
'{0}/ha-panel-{0}.html'.format(component_name))
else:
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)
def register_panel(hass, component_name, path, md5=None, title=None, icon=None,
url_name=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)
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_name in PANELS:
_LOGGER.warning('Overwriting component %s', url_name)
if not os.path.isfile(path):
_LOGGER.error('Panel %s component does not exist: %s',
component_name, path)
return
if md5 is None:
with open(path) as fil:
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
data = {
'url_name': url_name,
'component_name': component_name,
}
if title:
data['title'] = title
if icon:
data['icon'] = icon
if config is not None:
data['config'] = config
if url is not None:
data['url'] = url
else:
url = URL_PANEL_COMPONENT.format(component_name)
if url not in _REGISTERED_COMPONENTS:
hass.wsgi.register_static_path(url, path)
_REGISTERED_COMPONENTS.add(url)
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
data['url'] = fprinted_url
PANELS[url_name] = data
def setup(hass, config):
"""Setup serving the frontend."""
hass.wsgi.register_view(IndexView)
hass.wsgi.register_view(BootstrapView)
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
if hass.wsgi.development:
sw_path = "home-assistant-polymer/build/service_worker.js"
else:
sw_path = "service_worker.js"
hass.wsgi.register_static_path(
"/service_worker.js",
os.path.join(www_static_path, sw_path),
0
)
hass.wsgi.register_static_path(
"/robots.txt",
os.path.join(www_static_path, "robots.txt")
)
hass.wsgi.register_static_path("/static", www_static_path)
hass.wsgi.register_static_path("/service_worker.js",
os.path.join(STATIC_PATH, sw_path), 0)
hass.wsgi.register_static_path("/robots.txt",
os.path.join(STATIC_PATH, "robots.txt"))
hass.wsgi.register_static_path("/static", STATIC_PATH)
hass.wsgi.register_static_path("/local", hass.config.path('www'))
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template'):
register_built_in_panel(hass, panel)
def register_frontend_index(event):
"""Register the frontend index urls.
Done when Home Assistant is started so that all panels are known.
"""
hass.wsgi.register_view(IndexView(
hass, ['/{}'.format(name) for name in PANELS]))
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
return True
@ -48,6 +141,7 @@ class BootstrapView(HomeAssistantView):
'states': self.hass.states.all(),
'events': api.events_json(self.hass),
'services': api.services_json(self.hass),
'panels': PANELS,
})
@ -57,16 +151,15 @@ class IndexView(HomeAssistantView):
url = '/'
name = "frontend:index"
requires_auth = False
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent', '/devInfo', '/devTemplate',
'/states', '/states/<entity:entity_id>']
extra_urls = ['/states', '/states/<entity:entity_id>']
def __init__(self, hass):
def __init__(self, hass, extra_urls):
"""Initialize the frontend view."""
super().__init__(hass)
from jinja2 import FileSystemLoader, Environment
self.extra_urls = self.extra_urls + extra_urls
self.templates = Environment(
loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/')
@ -76,32 +169,32 @@ class IndexView(HomeAssistantView):
def get(self, request, entity_id=None):
"""Serve the index view."""
if self.hass.wsgi.development:
core_url = '/static/home-assistant-polymer/build/_core_compiled.js'
core_url = '/static/home-assistant-polymer/build/core.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
map_url = ('/static/home-assistant-polymer/src/layouts/'
'partial-map.html')
dev_url = ('/static/home-assistant-polymer/src/entry-points/'
'dev-tools.html')
else:
core_url = '/static/core-{}.js'.format(version.CORE)
ui_url = '/static/frontend-{}.html'.format(version.UI)
map_url = '/static/partial-map-{}.html'.format(version.MAP)
dev_url = '/static/dev-tools-{}.html'.format(version.DEV)
core_url = '/static/core-{}.js'.format(
FINGERPRINTS['core.js'])
ui_url = '/static/frontend-{}.html'.format(
FINGERPRINTS['frontend.html'])
if request.path == '/':
panel = 'states'
else:
panel = request.path.split('/')[1]
panel_url = PANELS[panel]['url'] if panel != 'states' else ''
# auto login if no password was set
if self.hass.config.api.api_password is None:
auth = 'true'
else:
auth = 'false'
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
no_auth = 'false' if self.hass.config.api.api_password else 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = self.templates.get_template('index.html')
# pylint is wrong
# pylint: disable=no-member
resp = template.render(
core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth,
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION)
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url)
return self.Response(resp, mimetype='text/html')

View File

@ -1,2 +0,0 @@
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
VERSION = "758957b7ea989d6beca60e218ea7f7dd"

View File

@ -5,19 +5,26 @@
<title>Home Assistant</title>
<link rel='manifest' href='/static/manifest.json'>
<link rel='icon' href='/static/favicon.ico'>
<link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/favicon-apple-180x180.png'>
href='/static/icons/favicon-apple-180x180.png'>
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/tile-win-310x310.png"/>
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='theme-color' content='#03a9f4'>
<style>
body {
font-family: 'Roboto', 'Noto', sans-serif;
font-weight: 300;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
#ha-init-skeleton {
display: -webkit-flex;
display: flex;
@ -65,23 +72,23 @@
.getElementById('ha-init-skeleton')
.classList.add('error');
};
window.noAuth = {{ auth }};
window.deferredLoading = {
map: '{{ map_url }}',
dev: '{{ dev_url }}',
};
window.noAuth = {{ no_auth }};
window.Polymer = {lazyRegister: true, useNativeCSSProperties: true, dom: 'shady'};
</script>
</head>
<body fullbleed>
<body>
<div id='ha-init-skeleton'>
<img src='/static/favicon-192x192.png' height='192'>
<img src='/static/icons/favicon-192x192.png' height='192'>
<paper-spinner active></paper-spinner>
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
</div>
<home-assistant icons='{{ icons }}'></home-assistant>
{# <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()' async>
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
{% if panel_url %}
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
{% endif %}
<link rel='import' href='{{ icons_url }}' async>
<script>
var webComponentsSupported = (
@ -89,11 +96,11 @@
'import' in document.createElement('link') &&
'content' in document.createElement('template'));
if (!webComponentsSupported) {
var script = document.createElement('script')
script.async = true
script.onerror = initError;
script.src = '/static/webcomponents-lite.min.js'
document.head.appendChild(script)
var e = document.createElement('script');
e.async = true;
e.onerror = initError;
e.src = '/static/webcomponents-lite.min.js';
document.head.appendChild(e);
}
</script>
</body>

View File

@ -1,5 +1,16 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
UI = "805f8dda70419b26daabc8e8f625127f"
MAP = "c922306de24140afd14f857f927bf8f0"
DEV = "b7079ac3121b95b9856e5603a6d8a263"
"""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"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 718384f22aa0a689190a4d3f41b5e9ed091c80a3
Subproject commit 697f9397de357cec9662626575fc01d6f921ef22

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -7,22 +7,22 @@
"background_color": "#FFFFFF",
"icons": [
{
"src": "/static/favicon-192x192.png",
"src": "/static/icons/favicon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/favicon-384x384.png",
"src": "/static/icons/favicon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/static/favicon-512x512.png",
"src": "/static/icons/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/static/favicon-1024x1024.png",
"src": "/static/icons/favicon-1024x1024.png",
"sizes": "1024x1024",
"type": "image/png"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@ -44,7 +44,6 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
self._state = value.data
dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
@ -53,7 +52,7 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
self._state = value.data
self.update_ha_state(True)
self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
@property

View File

@ -12,8 +12,8 @@ import voluptuous as vol
import homeassistant.core as ha
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_UNKNOWN,
ATTR_ASSUMED_STATE, )
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.event import track_state_change
@ -64,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema({
# List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
(STATE_OPEN, STATE_CLOSED)]
(STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED)]
def _get_group_on_off(state):
@ -304,8 +304,9 @@ class Group(Entity):
if gr_on is None:
return
if tr_state is None or (gr_state == gr_on and
tr_state.state == gr_off):
if tr_state is None or ((gr_state == gr_on and
tr_state.state == gr_off) or
tr_state.state not in (gr_on, gr_off)):
if states is None:
states = self._tracking_states

View File

@ -4,13 +4,13 @@ Provide pre-made queries on top of the recorder component.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/history/
"""
import re
from collections import defaultdict
from datetime import timedelta
from itertools import groupby
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, script
from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'history'
@ -19,9 +19,6 @@ DEPENDENCIES = ['recorder', 'http']
SIGNIFICANT_DOMAINS = ('thermostat',)
IGNORE_DOMAINS = ('zone', 'scene',)
URL_HISTORY_PERIOD = re.compile(
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
def last_5_states(entity_id):
"""Return the last 5 states for entity_id."""
@ -153,6 +150,7 @@ def setup(hass, config):
"""Setup the history hooks."""
hass.wsgi.register_view(Last5StatesView)
hass.wsgi.register_view(HistoryPeriodView)
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
return True
@ -173,14 +171,14 @@ class HistoryPeriodView(HomeAssistantView):
url = '/api/history/period'
name = 'api:history:view-period'
extra_urls = ['/api/history/period/<date:date>']
extra_urls = ['/api/history/period/<datetime:datetime>']
def get(self, request, date=None):
def get(self, request, datetime=None):
"""Return history over a period of time."""
one_day = timedelta(days=1)
if date:
start_time = dt_util.as_utc(dt_util.start_of_local_day(date))
if datetime:
start_time = dt_util.as_utc(datetime)
else:
start_time = dt_util.utcnow() - one_day

View File

@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
from homeassistant.config import load_yaml_config_file
DOMAIN = 'homematic'
REQUIREMENTS = ["pyhomematic==0.1.9"]
REQUIREMENTS = ["pyhomematic==0.1.10"]
HOMEMATIC = None
HOMEMATIC_LINK_DELAY = 0.5

View File

@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv
DOMAIN = "http"
REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10")
REQUIREMENTS = ("cherrypy==6.1.1", "static3==0.7.0", "Werkzeug==0.11.10")
CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host"
@ -216,9 +216,29 @@ def routing_map(hass):
"""Convert date to url value."""
return value.isoformat()
class DateTimeValidator(BaseConverter):
"""Validate datetimes in urls formatted per ISO 8601."""
regex = r'\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d' \
r'\.\d+([+-][0-2]\d:[0-5]\d|Z)'
def to_python(self, value):
"""Validate and convert date."""
parsed = dt_util.parse_datetime(value)
if parsed is None:
raise ValidationError()
return parsed
def to_url(self, value):
"""Convert date to url value."""
return value.isoformat()
return Map(converters={
'entity': EntityValidator,
'date': DateValidator,
'datetime': DateTimeValidator,
})

View File

@ -98,9 +98,10 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties()
self.update_ha_state(True)
self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
def update_properties(self):
@ -135,7 +136,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data
self._swing_list = [0, 1]
self._swing_list = list(value.data_items)
_LOGGER.debug("self._swing_list=%s", self._swing_list)
@property
@ -235,5 +236,5 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33:
value.data = int(swing_mode)
value.data = bytes(swing_mode, 'utf-8')
break

View File

@ -33,6 +33,7 @@ CONF_PASSWORD = 'password'
CONF_SSL = 'ssl'
CONF_VERIFY_SSL = 'verify_ssl'
CONF_BLACKLIST = 'blacklist'
CONF_TAGS = 'tags'
# pylint: disable=too-many-locals
@ -56,6 +57,7 @@ def setup(hass, config):
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
DEFAULT_VERIFY_SSL)
blacklist = conf.get(CONF_BLACKLIST, [])
tags = conf.get(CONF_TAGS, {})
try:
influx = InfluxDBClient(host=host, port=port, username=username,
@ -99,6 +101,9 @@ def setup(hass, config):
}
]
for tag in tags:
json_body[0]['tags'][tag] = tags[tag]
try:
influx.write_points(json_body)
except exceptions.InfluxDBClientError:

View File

@ -34,7 +34,7 @@ SERVICE_SELECT_VALUE = 'select_value'
SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_VALUE): vol.Coerce(int),
vol.Required(ATTR_VALUE): vol.Coerce(float),
})
@ -152,7 +152,7 @@ class InputSlider(Entity):
def select_value(self, value):
"""Select new value."""
num_value = int(value)
num_value = float(value)
if num_value < self._minimum or num_value > self._maximum:
_LOGGER.warning('Invalid value: %s (range %s - %s)',
num_value, self._minimum, self._maximum)

View File

@ -19,26 +19,19 @@ DOMAIN = 'joaoapps_join'
CONF_DEVICE_ID = 'device_id'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [{
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_API_KEY): cv.string
})
}])
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=too-many-locals
def setup(hass, config):
"""Setup Join services."""
from pyjoin import (get_devices, ring_device, set_wallpaper, send_sms,
def register_device(hass, device_id, api_key, name):
"""Method to register services for each join device listed."""
from pyjoin import (ring_device, set_wallpaper, send_sms,
send_file, send_url, send_notification)
device_id = config[DOMAIN].get(CONF_DEVICE_ID)
api_key = config[DOMAIN].get(CONF_API_KEY)
name = config[DOMAIN].get(CONF_NAME)
if api_key:
if not get_devices(api_key):
_LOGGER.error("Error connecting to Join, check API key")
return False
def ring_service(service):
"""Service to ring devices."""
@ -69,7 +62,6 @@ def setup(hass, config):
sms_text=service.data.get('message'),
api_key=api_key)
name = name.lower().replace(" ", "_") + "_" if name else ""
hass.services.register(DOMAIN, name + 'ring', ring_service)
hass.services.register(DOMAIN, name + 'set_wallpaper',
set_wallpaper_service)
@ -77,4 +69,19 @@ def setup(hass, config):
hass.services.register(DOMAIN, name + 'send_file', send_file_service)
hass.services.register(DOMAIN, name + 'send_url', send_url_service)
hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service)
def setup(hass, config):
"""Setup Join services."""
from pyjoin import get_devices
for device in config[DOMAIN]:
device_id = device.get(CONF_DEVICE_ID)
api_key = device.get(CONF_API_KEY)
name = device.get(CONF_NAME)
name = name.lower().replace(" ", "_") + "_" if name else ""
if api_key:
if not get_devices(api_key):
_LOGGER.error("Error connecting to Join, check API key")
return False
register_device(hass, device_id, api_key, name)
return True

View File

@ -10,7 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
DOMAIN = "knx"
REQUIREMENTS = ['knxip==0.3.0']
REQUIREMENTS = ['knxip==0.3.2']
EVENT_KNX_FRAME_RECEIVED = "knx_frame_received"
@ -45,7 +45,12 @@ def setup(hass, config):
KNXTUNNEL = KNXIPTunnel(host, port)
try:
KNXTUNNEL.connect()
res = KNXTUNNEL.connect()
_LOGGER.debug("Res = %s", res)
if not res:
_LOGGER.exception("Could not connect to KNX/IP interface %s", host)
return False
except KNXException as ex:
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
KNXTUNNEL = None
@ -74,7 +79,10 @@ class KNXConfig(object):
self.config = config
self.should_poll = config.get("poll", True)
self._address = parse_group_address(config.get("address"))
if config.get("address"):
self._address = parse_group_address(config.get("address"))
else:
self._address = None
if self.config.get("state_address"):
self._state_address = parse_group_address(
self.config.get("state_address"))
@ -198,7 +206,7 @@ class KNXGroupAddress(Entity):
return False
class KNXMultiAddressDevice(KNXGroupAddress):
class KNXMultiAddressDevice(Entity):
"""Representation of devices connected to a multiple KNX group address.
This is needed for devices like dimmers or shutter actuators as they have
@ -218,18 +226,21 @@ class KNXMultiAddressDevice(KNXGroupAddress):
"""
from knxip.core import parse_group_address, KNXException
super().__init__(self, hass, config)
self.config = config
self._config = config
self._state = False
self._data = None
_LOGGER.debug("Initalizing KNX multi address device")
# parse required addresses
for name in required:
_LOGGER.info(name)
paramname = name + "_address"
addr = self._config.config.get(paramname)
if addr is None:
_LOGGER.exception("Required KNX group address %s missing",
paramname)
raise KNXException("Group address missing in configuration")
raise KNXException("Group address for %s missing "
"in configuration", paramname)
addr = parse_group_address(addr)
self.names[addr] = name
@ -244,23 +255,25 @@ class KNXMultiAddressDevice(KNXGroupAddress):
_LOGGER.exception("Cannot parse group address %s", addr)
self.names[addr] = name
def handle_frame(frame):
"""Handle an incoming KNX frame.
@property
def name(self):
"""The entity's display name."""
return self._config.name
Handle an incoming frame and update our status if it contains
information relating to this device.
"""
addr = frame.data[0]
@property
def config(self):
"""The entity's configuration."""
return self._config
if addr in self.names:
self.values[addr] = frame.data[1]
self.update_ha_state()
@property
def should_poll(self):
"""Return the state of the polling, if needed."""
return self._config.should_poll
hass.bus.listen(EVENT_KNX_FRAME_RECEIVED, handle_frame)
def group_write_address(self, name, value):
"""Write to the group address with the given name."""
KNXTUNNEL.group_write(self.address, [value])
@property
def cache(self):
"""The name given to the entity."""
return self._config.config.get("cache", True)
def has_attribute(self, name):
"""Check if the attribute with the given name is defined.
@ -277,7 +290,7 @@ class KNXMultiAddressDevice(KNXGroupAddress):
from knxip.core import KNXException
addr = None
for attributename, attributeaddress in self.names.items():
for attributeaddress, attributename in self.names.items():
if attributename == name:
addr = attributeaddress
@ -293,3 +306,25 @@ class KNXMultiAddressDevice(KNXGroupAddress):
return False
return res
def set_value(self, name, value):
"""Set the value of a given named attribute."""
from knxip.core import KNXException
addr = None
for attributeaddress, attributename in self.names.items():
if attributename == name:
addr = attributeaddress
if addr is None:
_LOGGER.exception("Attribute %s undefined", name)
return False
try:
KNXTUNNEL.group_write(addr, value)
except KNXException:
_LOGGER.exception("Unable to write to KNX address: %s",
addr)
return False
return True

View File

@ -0,0 +1,109 @@
"""
Support for Flux lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.flux_led/
"""
import logging
import socket
import voluptuous as vol
from homeassistant.components.light import Light
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.3.zip'
'#flux_led==0.3']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "flux_led"
ATTR_NAME = 'name'
DEVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_NAME): cv.string,
})
PLATFORM_SCHEMA = vol.Schema({
vol.Required('platform'): DOMAIN,
vol.Optional('devices', default={}): {cv.string: DEVICE_SCHEMA},
vol.Optional('automatic_add', default=False): cv.boolean,
}, extra=vol.ALLOW_EXTRA)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the Flux lights."""
import flux_led
lights = []
light_ips = []
for ipaddr, device_config in config["devices"].items():
device = {}
device['id'] = device_config[ATTR_NAME]
device['ipaddr'] = ipaddr
light = FluxLight(device)
if light.is_valid:
lights.append(light)
light_ips.append(ipaddr)
if not config['automatic_add']:
add_devices_callback(lights)
return
# Find the bulbs on the LAN
scanner = flux_led.BulbScanner()
scanner.scan(timeout=20)
for device in scanner.getBulbInfo():
light = FluxLight(device)
ipaddr = device['ipaddr']
if light.is_valid and ipaddr not in light_ips:
lights.append(light)
light_ips.append(ipaddr)
add_devices_callback(lights)
class FluxLight(Light):
"""Representation of a Flux light."""
# pylint: disable=too-many-arguments
def __init__(self, device):
"""Initialize the light."""
import flux_led
self._name = device['id']
self._ipaddr = device['ipaddr']
self.is_valid = True
self._bulb = None
try:
self._bulb = flux_led.WifiLedBulb(self._ipaddr)
except socket.error:
self.is_valid = False
_LOGGER.error("Failed to connect to bulb %s, %s",
self._ipaddr, self._name)
@property
def unique_id(self):
"""Return the ID of this light."""
return "{}.{}".format(
self.__class__, self._ipaddr)
@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._bulb.isOn()
def turn_on(self, **kwargs):
"""Turn the specified or all lights on."""
self._bulb.turnOn()
def turn_off(self, **kwargs):
"""Turn the specified or all lights off."""
self._bulb.turnOff()
def update(self):
"""Synchronize state with bulb."""
self._bulb.refreshState()

View File

@ -19,24 +19,24 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup a Hyperion server remote."""
host = config.get(CONF_HOST, None)
port = config.get("port", 19444)
device = Hyperion(config.get('name', host), host, port)
default_color = config.get("default_color", [255, 255, 255])
device = Hyperion(config.get('name', host), host, port, default_color)
if device.setup():
add_devices_callback([device])
return True
else:
return False
return False
class Hyperion(Light):
"""Representation of a Hyperion remote."""
def __init__(self, name, host, port):
def __init__(self, name, host, port, default_color):
"""Initialize the light."""
self._host = host
self._port = port
self._name = name
self._is_available = True
self._rgb_color = [255, 255, 255]
self._default_color = default_color
self._rgb_color = [0, 0, 0]
@property
def name(self):
@ -50,38 +50,43 @@ class Hyperion(Light):
@property
def is_on(self):
"""Return true if the device is online."""
return self._is_available
"""Return true if not black."""
return self._rgb_color != [0, 0, 0]
def turn_on(self, **kwargs):
"""Turn the lights on."""
if self._is_available:
if ATTR_RGB_COLOR in kwargs:
self._rgb_color = kwargs[ATTR_RGB_COLOR]
if ATTR_RGB_COLOR in kwargs:
self._rgb_color = kwargs[ATTR_RGB_COLOR]
else:
self._rgb_color = self._default_color
self.json_request({"command": "color", "priority": 128,
"color": self._rgb_color})
self.json_request({"command": "color", "priority": 128,
"color": self._rgb_color})
def turn_off(self, **kwargs):
"""Disconnect the remote."""
"""Disconnect all remotes."""
self.json_request({"command": "clearall"})
self._rgb_color = [0, 0, 0]
def update(self):
"""Ping the remote."""
# just see if the remote port is open
self._is_available = self.json_request()
"""Get the remote's active color."""
response = self.json_request({"command": "serverinfo"})
if response:
if response["info"]["activeLedColor"] == []:
self._rgb_color = [0, 0, 0]
else:
self._rgb_color =\
response["info"]["activeLedColor"][0]["RGB Value"]
def setup(self):
"""Get the hostname of the remote."""
response = self.json_request({"command": "serverinfo"})
if response:
if self._name == self._host:
self._name = response["info"]["hostname"]
self._name = response["info"]["hostname"]
return True
return False
def json_request(self, request=None, wait_for_response=False):
def json_request(self, request, wait_for_response=False):
"""Communicate with the JSON server."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
@ -92,11 +97,6 @@ class Hyperion(Light):
sock.close()
return False
if not request:
# No communication needed, simple presence detection returns True
sock.close()
return True
sock.send(bytearray(json.dumps(request) + "\n", "utf-8"))
try:
buf = sock.recv(4096)

View File

@ -1,35 +1,21 @@
"""
Support for Qwikswitch Relays and Dimmers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.qwikswitch/
"""
import logging
import homeassistant.components.qwikswitch as qwikswitch
from homeassistant.components.light import Light
DEPENDENCIES = ['qwikswitch']
class QSLight(qwikswitch.QSToggleEntity, Light):
"""Light based on a Qwikswitch relay/dimmer module."""
pass
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Store add_devices for the light components."""
if discovery_info is None or 'qsusb_id' not in discovery_info:
logging.getLogger(__name__).error(
'Configure main Qwikswitch component')
return False
qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']]
for item in qsusb.ha_devices:
if item['type'] not in ['dim', 'rel']:
continue
dev = QSLight(item, qsusb)
add_devices([dev])
qsusb.ha_objects[item['id']] = dev
"""
Support for Qwikswitch Relays and Dimmers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.qwikswitch/
"""
import logging
import homeassistant.components.qwikswitch as qwikswitch
DEPENDENCIES = ['qwikswitch']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Add lights from the main Qwikswitch component."""
if discovery_info is None:
logging.getLogger(__name__).error('Configure Qwikswitch Component.')
return False
add_devices(qwikswitch.QSUSB['light'])
return True

View File

@ -6,10 +6,8 @@ https://home-assistant.io/components/light.vera/
"""
import logging
import homeassistant.util.dt as dt_util
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
from homeassistant.const import (
ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED,
STATE_OFF, STATE_ON)
from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
@ -56,31 +54,6 @@ class VeraLight(VeraDevice, Light):
self._state = STATE_OFF
self.update_ha_state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable:
armed = self.vera_device.is_armed
attr[ATTR_ARMED] = 'True' if armed else 'False'
if self.vera_device.is_trippable:
last_tripped = self.vera_device.last_trip
if last_tripped is not None:
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat()
else:
attr[ATTR_LAST_TRIP_TIME] = None
tripped = self.vera_device.is_tripped
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
@property
def is_on(self):
"""Return true if device is on."""

View File

@ -14,7 +14,7 @@ from homeassistant.util import color as color_util
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):

View File

@ -0,0 +1,80 @@
"""
Support for X10 lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.x10/
"""
import logging
from subprocess import check_output, CalledProcessError, STDOUT
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
_LOGGER = logging.getLogger(__name__)
def x10_command(command):
"""Execute X10 command and check output."""
return check_output(["heyu"] + command.split(' '), stderr=STDOUT)
def get_status():
"""Get on/off status for all x10 units in default housecode."""
output = check_output("heyu info | grep monitored", shell=True)
return output.decode('utf-8').split(' ')[-1].strip('\n()')
def get_unit_status(code):
"""Get on/off status for given unit."""
unit = int(code[1])
return get_status()[16 - int(unit)] == '1'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the x10 Light platform."""
try:
x10_command("info")
except CalledProcessError as err:
_LOGGER.error(err.output)
return False
add_devices(X10Light(light) for light in config['lights'])
class X10Light(Light):
"""Representation of an X10 Light."""
def __init__(self, light):
"""Initialize an X10 Light."""
self._name = light['name']
self._id = light['id']
self._is_on = False
self._brightness = 0
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def brightness(self):
"""Brightness of the light (an integer in the range 1-255)."""
return self._brightness
@property
def is_on(self):
"""Return true if light is on."""
return self._is_on
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
x10_command("on " + self._id)
self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
self._is_on = True
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
x10_command("off " + self._id)
self._is_on = False
def update(self):
"""Fetch new state data for this light."""
self._is_on = get_unit_status(self._id)

View File

@ -8,7 +8,6 @@ import logging
# Because we do not compile openzwave on CI
# pylint: disable=import-error
from threading import Timer
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
ATTR_RGB_COLOR, DOMAIN, Light
from homeassistant.components import zwave
@ -107,25 +106,10 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
def _value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id != value.value_id:
return
if self._refreshing:
self._refreshing = False
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties()
else:
def _refresh_value():
"""Used timer callback for delayed value refresh."""
self._refreshing = True
self._value.refresh()
if self._timer is not None and self._timer.isAlive():
self._timer.cancel()
self._timer = Timer(2, _refresh_value)
self._timer.start()
self.update_ha_state()
self.update_ha_state()
@property
def brightness(self):

View File

@ -8,7 +8,7 @@ import logging
from homeassistant.components.lock import LockDevice
from homeassistant.const import (
ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED)
STATE_LOCKED, STATE_UNLOCKED)
from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
@ -32,16 +32,6 @@ class VeraLock(VeraDevice, LockDevice):
self._state = None
VeraDevice.__init__(self, vera_device, controller)
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
def lock(self, **kwargs):
"""Lock the device."""
self.vera_device.lock()

View File

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

View File

@ -46,7 +46,8 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
def _value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self._state = value.data
self.update_ha_state()

View File

@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/logbook/
"""
import logging
import re
from datetime import timedelta
from itertools import groupby
@ -14,6 +13,7 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun
from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
@ -24,9 +24,7 @@ from homeassistant.helpers import template
from homeassistant.helpers.entity import split_entity_id
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
DEPENDENCIES = ['recorder', 'frontend']
_LOGGER = logging.getLogger(__name__)
@ -75,6 +73,9 @@ def setup(hass, config):
hass.wsgi.register_view(LogbookView)
register_built_in_panel(hass, 'logbook', 'Logbook',
'mdi:format-list-bulleted-type')
hass.services.register(DOMAIN, 'log', log_message,
schema=LOG_MESSAGE_SCHEMA)
return True
@ -85,16 +86,11 @@ class LogbookView(HomeAssistantView):
url = '/api/logbook'
name = 'api:logbook'
extra_urls = ['/api/logbook/<date:date>']
extra_urls = ['/api/logbook/<datetime:datetime>']
def get(self, request, date=None):
def get(self, request, datetime=None):
"""Retrieve logbook entries."""
if date:
start_day = dt_util.start_of_local_day(date)
else:
start_day = dt_util.start_of_local_day()
start_day = dt_util.as_utc(start_day)
start_day = dt_util.as_utc(datetime or dt_util.start_of_local_day())
end_day = start_day + timedelta(days=1)
events = recorder.get_model('Events')

View File

@ -0,0 +1,172 @@
"""Support for the DirecTV recievers."""
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP,
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING)
REQUIREMENTS = ['directpy==0.1']
DEFAULT_PORT = 8080
SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \
SUPPORT_PREVIOUS_TRACK
KNOWN_HOSTS = []
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the DirecTV platform."""
hosts = []
if discovery_info and discovery_info in KNOWN_HOSTS:
return
if discovery_info is not None:
hosts.append([
'DirecTV_' + discovery_info[1],
discovery_info[0],
DEFAULT_PORT
])
elif CONF_HOST in config:
hosts.append([
config.get(CONF_NAME, 'DirecTV Receiver'),
config[CONF_HOST], DEFAULT_PORT
])
dtvs = []
for host in hosts:
dtvs.append(DirecTvDevice(*host))
KNOWN_HOSTS.append(host)
add_devices(dtvs)
return True
class DirecTvDevice(MediaPlayerDevice):
"""Representation of a DirecTV reciever on the network."""
# pylint: disable=abstract-method
# pylint: disable=too-many-public-methods
def __init__(self, name, host, port):
"""Initialize the device."""
from DirectPy import DIRECTV
self.dtv = DIRECTV(host, port)
self._name = name
self._is_standby = True
self._current = None
def update(self):
"""Retrieve latest state."""
self._is_standby = self.dtv.get_standby()
if self._is_standby:
self._current = None
else:
self._current = self.dtv.get_tuned()
@property
def name(self):
"""Return the name of the device."""
return self._name
# MediaPlayerDevice properties and methods
@property
def state(self):
"""Return the state of the device."""
if self._is_standby:
return STATE_OFF
# haven't determined a way to see if the content is paused
else:
return STATE_PLAYING
@property
def media_content_id(self):
"""Content ID of current playing media."""
if self._is_standby:
return None
else:
return self._current['programId']
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
if self._is_standby:
return None
else:
return self._current['duration']
@property
def media_title(self):
"""Title of current playing media."""
if self._is_standby:
return None
else:
return self._current['title']
@property
def media_series_title(self):
"""Title of current episode of TV show."""
if self._is_standby:
return None
else:
if 'episodeTitle' in self._current:
return self._current['episodeTitle']
else:
return None
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_DTV
@property
def media_content_type(self):
"""Content type of current playing media."""
if 'episodeTitle' in self._current:
return MEDIA_TYPE_TVSHOW
else:
return MEDIA_TYPE_VIDEO
@property
def media_channel(self):
"""Channel current playing media."""
if self._is_standby:
return None
else:
chan = "{} ({})".format(self._current['callsign'],
self._current['major'])
return chan
def turn_on(self):
"""Turn on the reciever."""
self.dtv.key_press('poweron')
def turn_off(self):
"""Turn off the reciever."""
self.dtv.key_press('poweroff')
def media_play(self):
"""Send play commmand."""
self.dtv.key_press('play')
def media_pause(self):
"""Send pause commmand."""
self.dtv.key_press('pause')
def media_stop(self):
"""Send stop commmand."""
self.dtv.key_press('stop')
def media_previous_track(self):
"""Send rewind commmand."""
self.dtv.key_press('rew')
def media_next_track(self):
"""Send fast forward commmand."""
self.dtv.key_press('ffwd')

View File

@ -0,0 +1,153 @@
"""
Support to interface with the MPC-HC Web API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.mpchc/
"""
import logging
import re
import requests
from homeassistant.components.media_player import (
SUPPORT_VOLUME_MUTE, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_IDLE, STATE_PAUSED, STATE_PLAYING)
_LOGGER = logging.getLogger(__name__)
SUPPORT_MPCHC = SUPPORT_VOLUME_MUTE | SUPPORT_PAUSE | SUPPORT_STOP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_STEP
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the MPC-HC platform."""
name = config.get("name", "MPC-HC")
url = '{}:{}'.format(config.get('host'), config.get('port', '13579'))
if config.get('host') is None:
_LOGGER.error("Missing NPC-HC host address in config")
return False
add_devices([MpcHcDevice(name, url)])
# pylint: disable=abstract-method
class MpcHcDevice(MediaPlayerDevice):
"""Representation of a MPC-HC server."""
def __init__(self, name, url):
"""Initialize the MPC-HC device."""
self._name = name
self._url = url
self.update()
def update(self):
"""Get the latest details."""
self._player_variables = dict()
try:
response = requests.get("{}/variables.html".format(self._url),
data=None, timeout=3)
mpchc_variables = re.findall(r'<p id="(.+?)">(.+?)</p>',
response.text)
self._player_variables = dict()
for var in mpchc_variables:
self._player_variables[var[0]] = var[1].lower()
except requests.exceptions.RequestException:
_LOGGER.error("Could not connect to MPC-HC at: %s", self._url)
def _send_command(self, command_id):
"""Send a command to MPC-HC via its window message ID."""
try:
params = {"wm_command": command_id}
requests.get("{}/command.html".format(self._url),
params=params, timeout=3)
except requests.exceptions.RequestException:
_LOGGER.error("Could not send command %d to MPC-HC at: %s",
command_id, self._url)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
state = self._player_variables.get('statestring', None)
if state is None:
return STATE_OFF
if state == 'playing':
return STATE_PLAYING
elif state == 'paused':
return STATE_PAUSED
else:
return STATE_IDLE
@property
def media_title(self):
"""Title of current playing media."""
return self._player_variables.get('file', None)
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return int(self._player_variables.get('volumelevel', 0)) / 100.0
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._player_variables.get('muted', '0') == '1'
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
duration = self._player_variables.get('durationstring',
"00:00:00").split(':')
return \
int(duration[0]) * 3600 + \
int(duration[1]) * 60 + \
int(duration[2])
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_MPCHC
def volume_up(self):
"""Volume up the media player."""
self._send_command(907)
def volume_down(self):
"""Volume down media player."""
self._send_command(908)
def mute_volume(self, mute):
"""Mute the volume."""
self._send_command(909)
def media_play(self):
"""Send play command."""
self._send_command(887)
def media_pause(self):
"""Send pause command."""
self._send_command(888)
def media_stop(self):
"""Send stop command."""
self._send_command(890)
def media_next_track(self):
"""Send next track command."""
self._send_command(921)
def media_previous_track(self):
"""Send previous track command."""
self._send_command(920)

View File

@ -0,0 +1,118 @@
"""
Support for interfacing with Russound via RNET Protocol.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.russound_rnet/
"""
import logging
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON)
REQUIREMENTS = [
'https://github.com/laf/russound/archive/0.1.6.zip'
'#russound==0.1.6']
ZONES = 'zones'
SOURCES = 'sources'
SUPPORT_RUSSOUND = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Russound RNET platform."""
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
keypad = config.get('keypad', '70')
if host is None or port is None:
_LOGGER.error('Invalid config. Expected %s and %s',
CONF_HOST, CONF_PORT)
return False
from russound import russound
russ = russound.Russound(host, port)
russ.connect(keypad)
sources = []
for source in config[SOURCES]:
sources.append(source['name'])
if russ.is_connected():
for zone_id, extra in config[ZONES].items():
add_devices([RussoundRNETDevice(hass, russ, sources, zone_id,
extra)])
else:
_LOGGER.error('Not connected to %s:%s', host, port)
# pylint: disable=abstract-method, too-many-public-methods,
# pylint: disable=too-many-instance-attributes, too-many-arguments
class RussoundRNETDevice(MediaPlayerDevice):
"""Representation of a Russound RNET device."""
def __init__(self, hass, russ, sources, zone_id, extra):
"""Initialise the Russound RNET device."""
self._name = extra['name']
self._russ = russ
self._state = STATE_OFF
self._sources = sources
self._zone_id = zone_id
self._volume = 0
@property
def name(self):
"""Return the name of the zone."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_RUSSOUND
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._volume = volume * 100
self._russ.set_volume('1', self._zone_id, self._volume)
def turn_on(self):
"""Turn the media player on."""
self._russ.set_power('1', self._zone_id, '1')
self._state = STATE_ON
def turn_off(self):
"""Turn off media player."""
self._russ.set_power('1', self._zone_id, '0')
self._state = STATE_OFF
def mute_volume(self, mute):
"""Send mute command."""
self._russ.toggle_mute('1', self._zone_id)
def select_source(self, source):
"""Set the input source."""
if source in self._sources:
index = self._sources.index(source)+1
self._russ.set_source('1', self._zone_id, index)
@property
def source_list(self):
"""List of available input sources."""
return self._sources

View File

@ -6,8 +6,9 @@ https://home-assistant.io/components/media_player.sonos/
"""
import datetime
import logging
import socket
from os import path
import socket
import voluptuous as vol
from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
@ -15,8 +16,10 @@ from homeassistant.components.media_player import (
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF,
ATTR_ENTITY_ID)
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['SoCo==0.11.1']
@ -43,16 +46,28 @@ SUPPORT_SOURCE_LINEIN = 'Line-in'
SUPPORT_SOURCE_TV = 'TV'
SUPPORT_SOURCE_RADIO = 'Radio'
SONOS_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
})
# List of devices that have been registered
DEVICES = []
# pylint: disable=unused-argument, too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Sonos platform."""
import soco
global DEVICES
if discovery_info:
player = soco.SoCo(discovery_info)
if player.is_visible:
add_devices([SonosDevice(hass, player)])
device = SonosDevice(hass, player)
add_devices([device])
if not DEVICES:
register_services(hass)
DEVICES.append(device)
return True
return False
@ -74,60 +89,72 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.warning('No Sonos speakers found.')
return False
devices = [SonosDevice(hass, p) for p in players]
add_devices(devices)
DEVICES = [SonosDevice(hass, p) for p in players]
add_devices(DEVICES)
register_services(hass)
_LOGGER.info('Added %s Sonos speakers', len(players))
return True
def _apply_service(service, service_func, *service_func_args):
"""Internal func for applying a service."""
entity_id = service.data.get('entity_id')
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
service_func(device, *service_func_args)
device.update_ha_state(True)
def group_players_service(service):
"""Group media players, use player as coordinator."""
_apply_service(service, SonosDevice.group_players)
def unjoin_service(service):
"""Unjoin the player from a group."""
_apply_service(service, SonosDevice.unjoin)
def snapshot_service(service):
"""Take a snapshot."""
_apply_service(service, SonosDevice.snapshot)
def restore_service(service):
"""Restore a snapshot."""
_apply_service(service, SonosDevice.restore)
def register_services(hass):
"""Register all services for sonos devices."""
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS,
group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS))
_group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS),
schema=SONOS_SCHEMA)
hass.services.register(DOMAIN, SERVICE_UNJOIN,
unjoin_service,
descriptions.get(SERVICE_UNJOIN))
_unjoin_service,
descriptions.get(SERVICE_UNJOIN),
schema=SONOS_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
snapshot_service,
descriptions.get(SERVICE_SNAPSHOT))
_snapshot_service,
descriptions.get(SERVICE_SNAPSHOT),
schema=SONOS_SCHEMA)
hass.services.register(DOMAIN, SERVICE_RESTORE,
restore_service,
descriptions.get(SERVICE_RESTORE))
_restore_service,
descriptions.get(SERVICE_RESTORE),
schema=SONOS_SCHEMA)
return True
def _apply_service(service, service_func, *service_func_args):
"""Internal func for applying a service."""
entity_ids = service.data.get('entity_id')
if entity_ids:
_devices = [device for device in DEVICES
if device.entity_id in entity_ids]
else:
_devices = DEVICES
for device in _devices:
service_func(device, *service_func_args)
device.update_ha_state(True)
def _group_players_service(service):
"""Group media players, use player as coordinator."""
_apply_service(service, SonosDevice.group_players)
def _unjoin_service(service):
"""Unjoin the player from a group."""
_apply_service(service, SonosDevice.unjoin)
def _snapshot_service(service):
"""Take a snapshot."""
_apply_service(service, SonosDevice.snapshot)
def _restore_service(service):
"""Restore a snapshot."""
_apply_service(service, SonosDevice.restore)
def only_if_coordinator(func):

View File

@ -22,7 +22,7 @@ def get_service(hass, config):
"""Get the GNTP notification service."""
if config.get('app_icon') is None:
icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend",
"www_static", "favicon-192x192.png")
"www_static", "icons", "favicon-192x192.png")
app_icon = open(icon_file, 'rb').read()
else:
app_icon = config.get('app_icon')

View File

@ -1,59 +0,0 @@
"""
Google Voice SMS platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.google_voice/
"""
import logging
from homeassistant.components.notify import (
ATTR_TARGET, DOMAIN, BaseNotificationService)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/w1ll1am23/pygooglevoice-sms/archive/'
'7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#'
'pygooglevoice-sms==0.0.1']
def get_service(hass, config):
"""Get the Google Voice SMS notification service."""
if not validate_config({DOMAIN: config},
{DOMAIN: [CONF_USERNAME,
CONF_PASSWORD]},
_LOGGER):
return None
return GoogleVoiceSMSNotificationService(config[CONF_USERNAME],
config[CONF_PASSWORD])
# pylint: disable=too-few-public-methods
class GoogleVoiceSMSNotificationService(BaseNotificationService):
"""Implement the notification service for the Google Voice SMS service."""
def __init__(self, username, password):
"""Initialize the service."""
from googlevoicesms import Voice
self.voice = Voice()
self.username = username
self.password = password
def send_message(self, message="", **kwargs):
"""Send SMS to specified target user cell."""
targets = kwargs.get(ATTR_TARGET)
if not targets:
_LOGGER.info('At least 1 target is required')
return
if not isinstance(targets, list):
targets = [targets]
self.voice.login(self.username, self.password)
for target in targets:
self.voice.send_sms(target, message)
self.voice.logout()

View File

@ -10,7 +10,7 @@ from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService)
from homeassistant.helpers import validate_config
REQUIREMENTS = ['sendgrid>=1.6.0,<1.7.0']
REQUIREMENTS = ['sendgrid==3.0.7']
_LOGGER = logging.getLogger(__name__)
@ -24,27 +24,50 @@ def get_service(hass, config):
api_key = config['api_key']
sender = config['sender']
recipient = config['recipient']
return SendgridNotificationService(api_key, sender, recipient)
# pylint: disable=too-few-public-methods
class SendgridNotificationService(BaseNotificationService):
"""Implement the notification service for email via Sendgrid."""
"""Implementation the notification service for email via Sendgrid."""
def __init__(self, api_key, sender, recipient):
"""Initialize the service."""
from sendgrid import SendGridAPIClient
self.api_key = api_key
self.sender = sender
self.recipient = recipient
from sendgrid import SendGridClient
self._sg = SendGridClient(self.api_key)
self._sg = SendGridAPIClient(apikey=self.api_key)
def send_message(self, message='', **kwargs):
"""Send an email to a user via SendGrid."""
subject = kwargs.get(ATTR_TITLE)
from sendgrid import Mail
mail = Mail(from_email=self.sender, to=self.recipient,
html=message, text=message, subject=subject)
self._sg.send(mail)
data = {
"personalizations": [
{
"to": [
{
"email": self.recipient
}
],
"subject": subject
}
],
"from": {
"email": self.sender
},
"content": [
{
"type": "text/plain",
"value": message
}
]
}
response = self._sg.client.mail.send.post(request_body=data)
if response.status_code is not 202:
_LOGGER.error('Unable to send notification with SendGrid')

View File

@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
REQUIREMENTS = ['slacker==0.9.21']
REQUIREMENTS = ['slacker==0.9.24']
_LOGGER = logging.getLogger(__name__)

View File

@ -8,43 +8,69 @@ import io
import logging
import urllib
import requests
from requests.auth import HTTPBasicAuth
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
ATTR_TITLE, ATTR_DATA, BaseNotificationService)
from homeassistant.const import (CONF_API_KEY, CONF_NAME, ATTR_LOCATION,
ATTR_LATITUDE, ATTR_LONGITUDE, CONF_PLATFORM)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-telegram-bot==4.3.3']
REQUIREMENTS = ['python-telegram-bot==5.0.0']
ATTR_PHOTO = "photo"
ATTR_FILE = "file"
ATTR_URL = "url"
ATTR_CAPTION = "caption"
ATTR_USERNAME = "username"
ATTR_PASSWORD = "password"
CONF_CHAT_ID = 'chat_id'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "telegram",
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_CHAT_ID): cv.string,
})
def get_service(hass, config):
"""Get the Telegram notification service."""
import telegram
if not validate_config({DOMAIN: config},
{DOMAIN: [CONF_API_KEY, 'chat_id']},
_LOGGER):
return None
try:
bot = telegram.Bot(token=config[CONF_API_KEY])
chat_id = config.get(CONF_CHAT_ID)
api_key = config.get(CONF_API_KEY)
bot = telegram.Bot(token=api_key)
username = bot.getMe()['username']
_LOGGER.info("Telegram bot is '%s'.", username)
except urllib.error.HTTPError:
_LOGGER.error("Please check your access token.")
return None
return TelegramNotificationService(config[CONF_API_KEY], config['chat_id'])
return TelegramNotificationService(api_key, chat_id)
def load_data(url=None, file=None, username=None, password=None):
"""Load photo/document into ByteIO/File container from a source."""
try:
if url is not None:
# load photo from url
if username is not None and password is not None:
req = requests.get(url, auth=(username, password), timeout=15)
else:
req = requests.get(url, timeout=15)
return io.BytesIO(req.content)
elif file is not None:
# load photo from file
return open(file, "rb")
else:
_LOGGER.warning("Can't load photo no photo found in params!")
except (OSError, IOError, requests.exceptions.RequestException):
_LOGGER.error("Can't load photo into ByteIO")
return None
# pylint: disable=too-few-public-methods
@ -64,7 +90,18 @@ class TelegramNotificationService(BaseNotificationService):
import telegram
title = kwargs.get(ATTR_TITLE)
data = kwargs.get(ATTR_DATA, {})
data = kwargs.get(ATTR_DATA)
# exists data for send a photo/location
if data is not None and ATTR_PHOTO in data:
photos = data.get(ATTR_PHOTO, None)
photos = photos if isinstance(photos, list) else [photos]
for photo_data in photos:
self.send_photo(photo_data)
return
elif data is not None and ATTR_LOCATION in data:
return self.send_location(data.get(ATTR_LOCATION))
# send message
try:
@ -74,41 +111,30 @@ class TelegramNotificationService(BaseNotificationService):
_LOGGER.exception("Error sending message.")
return
def send_photo(self, data):
"""Send a photo."""
import telegram
caption = data.pop(ATTR_CAPTION, None)
# send photo
if ATTR_PHOTO in data:
# if not a list
if not isinstance(data[ATTR_PHOTO], list):
photos = [data[ATTR_PHOTO]]
else:
photos = data[ATTR_PHOTO]
try:
photo = load_data(**data)
self.bot.sendPhoto(chat_id=self._chat_id,
photo=photo, caption=caption)
except telegram.error.TelegramError:
_LOGGER.exception("Error sending photo.")
return
try:
for photo_data in photos:
caption = photo_data.get(ATTR_CAPTION, None)
def send_location(self, gps):
"""Send a location."""
import telegram
latitude = float(gps.get(ATTR_LATITUDE, 0.0))
longitude = float(gps.get(ATTR_LONGITUDE, 0.0))
# file is a url
if ATTR_URL in photo_data:
# use http authenticate
if ATTR_USERNAME in photo_data and\
ATTR_PASSWORD in photo_data:
req = requests.get(
photo_data[ATTR_URL],
auth=HTTPBasicAuth(photo_data[ATTR_USERNAME],
photo_data[ATTR_PASSWORD])
)
else:
req = requests.get(photo_data[ATTR_URL])
file_id = io.BytesIO(req.content)
elif ATTR_FILE in photo_data:
file_id = open(photo_data[ATTR_FILE], "rb")
else:
_LOGGER.error("No url or path is set for photo!")
continue
self.bot.sendPhoto(chat_id=self._chat_id,
photo=file_id, caption=caption)
except (OSError, IOError, telegram.error.TelegramError,
urllib.error.HTTPError):
_LOGGER.exception("Error sending photo.")
return
# send location
try:
self.bot.sendLocation(chat_id=self._chat_id,
latitude=latitude, longitude=longitude)
except telegram.error.TelegramError:
_LOGGER.exception("Error sending location.")
return

View File

@ -0,0 +1,31 @@
"""Add an iframe panel to Home Assistant."""
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.frontend import register_built_in_panel
DOMAIN = 'panel_iframe'
DEPENDENCIES = ['frontend']
CONF_TITLE = 'title'
CONF_ICON = 'icon'
CONF_URL = 'url'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: {
vol.Optional(CONF_TITLE): cv.string,
vol.Optional(CONF_ICON): cv.icon,
# pylint: disable=no-value-for-parameter
vol.Required(CONF_URL): vol.Url(),
}})}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup iframe frontend panels."""
for url_name, 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]})
return True

View File

@ -5,18 +5,29 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/qwikswitch/
"""
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.discovery import load_platform
import voluptuous as vol
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers.discovery import load_platform
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
from homeassistant.components.switch import SwitchDevice
DOMAIN = 'qwikswitch'
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip'
'#pyqwikswitch==0.4']
DEPENDENCIES = []
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'qwikswitch'
QSUSB = None
CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3))
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required('url', default='http://127.0.0.1:2020'): vol.Coerce(str),
vol.Optional('dimmer_adjust', default=1): CV_DIM_VALUE,
vol.Optional('button_events'): vol.Coerce(str)
})}, extra=vol.ALLOW_EXTRA)
QSUSB = {}
class QSToggleEntity(object):
@ -42,6 +53,7 @@ class QSToggleEntity(object):
self._value = qsitem[PQS_VALUE]
self._qsusb = qsusb
self._dim = qsitem[PQS_TYPE] == QSType.dimmer
QSUSB[self._id] = self
@property
def brightness(self):
@ -87,51 +99,70 @@ class QSToggleEntity(object):
self.update_value(0)
class QSSwitch(QSToggleEntity, SwitchDevice):
"""Switch based on a Qwikswitch relay module."""
pass
class QSLight(QSToggleEntity, Light):
"""Light based on a Qwikswitch relay/dimmer module."""
pass
# pylint: disable=too-many-locals
def setup(hass, config):
"""Setup the QSUSB component."""
from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD,
QS_TYPE, PQS_VALUE, PQS_TYPE, QSType)
PQS_VALUE, PQS_TYPE, QSType)
# Override which cmd's in /&listen packets will fire events
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS))
cmd_buttons = cmd_buttons.split(',')
try:
url = config[DOMAIN].get('url', 'http://127.0.0.1:2020')
dimmer_adjust = float(config[DOMAIN].get('dimmer_adjust', '1'))
qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
url = config[DOMAIN]['url']
dimmer_adjust = config[DOMAIN]['dimmer_adjust']
# Ensure qsusb terminates threads correctly
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: qsusb.stop())
except ValueError as val_err:
_LOGGER.error(str(val_err))
return False
qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
qsusb.ha_devices = qsusb.devices()
qsusb.ha_objects = {}
# Identify switches & remove ' Switch' postfix in name
for item in qsusb.ha_devices:
if item[PQS_TYPE] == QSType.relay and \
item[QS_NAME].lower().endswith(' switch'):
item[QS_TYPE] = 'switch'
item[QS_NAME] = item[QS_NAME][:-7]
global QSUSB
if QSUSB is None:
def _stop(event):
"""Stop the listener queue and clean up."""
nonlocal qsusb
qsusb.stop()
qsusb = None
global QSUSB
QSUSB = {}
QSUSB[id(qsusb)] = qsusb
_LOGGER.info("Waiting for long poll to QSUSB to time out")
# Load sub-components for qwikswitch
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _stop)
# Discover all devices in QSUSB
devices = qsusb.devices()
QSUSB['switch'] = []
QSUSB['light'] = []
for item in devices:
if item[PQS_TYPE] == QSType.relay and (item[QS_NAME].lower()
.endswith(' switch')):
item[QS_NAME] = item[QS_NAME][:-7] # Remove ' switch' postfix
QSUSB['switch'].append(QSSwitch(item, qsusb))
elif item[PQS_TYPE] in [QSType.relay, QSType.dimmer]:
QSUSB['light'].append(QSLight(item, qsusb))
else:
_LOGGER.warning("Ignored unknown QSUSB device: %s", item)
# Load platforms
for comp_name in ('switch', 'light'):
load_platform(hass, comp_name, 'qwikswitch',
{'qsusb_id': id(qsusb)}, config)
if len(QSUSB[comp_name]) > 0:
load_platform(hass, comp_name, 'qwikswitch', {}, config)
def qs_callback(item):
"""Typically a button press or update signal."""
if qsusb is None: # Shutting down
_LOGGER.info("Done")
return
# If button pressed, fire a hass event
if item.get(QS_CMD, '') in cmd_buttons:
hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id'))
@ -142,9 +173,13 @@ def setup(hass, config):
if qsreply is False:
return
for item in qsreply:
if item[QS_ID] in qsusb.ha_objects:
qsusb.ha_objects[item[QS_ID]].update_value(
if item[QS_ID] in QSUSB:
QSUSB[item[QS_ID]].update_value(
round(min(item[PQS_VALUE], 100) * 2.55))
qsusb.listen(callback=qs_callback, timeout=30)
def _start(event):
"""Start listening."""
qsusb.listen(callback=qs_callback, timeout=30)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start)
return True

View File

@ -39,7 +39,8 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
vol.Range(min=1)),
vol.Optional(CONF_DB_URL): vol.Url(''),
# pylint: disable=no-value-for-parameter
vol.Optional(CONF_DB_URL): vol.Url(),
})
}, extra=vol.ALLOW_EXTRA)
@ -90,8 +91,12 @@ def run_information(point_in_time=None):
def setup(hass, config):
"""Setup the recorder."""
# pylint: disable=global-statement
# pylint: disable=too-many-locals
global _INSTANCE
if _INSTANCE is not None:
_LOGGER.error('Only a single instance allowed.')
return False
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None)
@ -129,7 +134,7 @@ def log_error(e, retry_wait=0, rollback=True,
if rollback:
Session().rollback()
if retry_wait:
_LOGGER.info("Retrying failed query in %s seconds", QUERY_RETRY_WAIT)
_LOGGER.info("Retrying in %s seconds", retry_wait)
time.sleep(retry_wait)
@ -164,8 +169,6 @@ class Recorder(threading.Thread):
from homeassistant.components.recorder.models import Events, States
import sqlalchemy.exc
global _INSTANCE
while True:
try:
self._setup_connection()
@ -176,8 +179,12 @@ class Recorder(threading.Thread):
message="Error during connection setup: %s")
if self.purge_days is not None:
track_point_in_utc_time(self.hass,
lambda now: self._purge_old_data(),
def purge_ticker(event):
"""Rerun purge every second day."""
self._purge_old_data()
track_point_in_utc_time(self.hass, purge_ticker,
dt_util.utcnow() + timedelta(days=2))
track_point_in_utc_time(self.hass, purge_ticker,
dt_util.utcnow() + timedelta(minutes=5))
while True:
@ -186,42 +193,26 @@ class Recorder(threading.Thread):
if event == self.quit_object:
self._close_run()
self._close_connection()
# pylint: disable=global-statement
global _INSTANCE
_INSTANCE = None
self.queue.task_done()
return
elif event.event_type == EVENT_TIME_CHANGED:
if event.event_type == EVENT_TIME_CHANGED:
self.queue.task_done()
continue
session = Session()
dbevent = Events.from_event(event)
session.add(dbevent)
for _ in range(0, RETRIES):
try:
session.commit()
break
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT,
rollback=True)
self._commit(dbevent)
if event.event_type != EVENT_STATE_CHANGED:
self.queue.task_done()
continue
session = Session()
dbstate = States.from_event(event)
for _ in range(0, RETRIES):
try:
dbstate.event_id = dbevent.event_id
session.add(dbstate)
session.commit()
break
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT,
rollback=True)
dbstate.event_id = dbevent.event_id
self._commit(dbstate)
self.queue.task_done()
@ -268,6 +259,7 @@ class Recorder(threading.Thread):
def _close_connection(self):
"""Close the connection."""
# pylint: disable=global-statement
global Session
self.engine.dispose()
self.engine = None
@ -289,16 +281,12 @@ class Recorder(threading.Thread):
start=self.recording_start,
created=dt_util.utcnow()
)
session = Session()
session.add(self._run)
session.commit()
self._commit(self._run)
def _close_run(self):
"""Save end time for current run."""
self._run.end = dt_util.utcnow()
session = Session()
session.add(self._run)
session.commit()
self._commit(self._run)
self._run = None
def _purge_old_data(self):
@ -312,17 +300,24 @@ class Recorder(threading.Thread):
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
_LOGGER.info("Purging events created before %s", purge_before)
deleted_rows = Session().query(Events).filter(
(Events.created < purge_before)).delete(synchronize_session=False)
_LOGGER.debug("Deleted %s events", deleted_rows)
def _purge_states(session):
deleted_rows = session.query(States) \
.filter((States.created < purge_before)) \
.delete(synchronize_session=False)
_LOGGER.debug("Deleted %s states", deleted_rows)
_LOGGER.info("Purging states created before %s", purge_before)
deleted_rows = Session().query(States).filter(
(States.created < purge_before)).delete(synchronize_session=False)
_LOGGER.debug("Deleted %s states", deleted_rows)
if self._commit(_purge_states):
_LOGGER.info("Purged states created before %s", purge_before)
def _purge_events(session):
deleted_rows = session.query(Events) \
.filter((Events.created < purge_before)) \
.delete(synchronize_session=False)
_LOGGER.debug("Deleted %s events", deleted_rows)
if self._commit(_purge_events):
_LOGGER.info("Purged events created before %s", purge_before)
Session().commit()
Session().expire_all()
# Execute sqlite vacuum command to free up space on disk
@ -330,6 +325,23 @@ class Recorder(threading.Thread):
_LOGGER.info("Vacuuming SQLite to free space")
self.engine.execute("VACUUM")
@staticmethod
def _commit(work):
"""Commit & retry work: Either a model or in a function."""
import sqlalchemy.exc
session = Session()
for _ in range(0, RETRIES):
try:
if callable(work):
work(session)
else:
session.add(work)
session.commit()
return True
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True)
return False
def _verify_instance():
"""Throw error if recorder not initialized."""

View File

@ -20,7 +20,7 @@ Base = declarative_base()
_LOGGER = logging.getLogger(__name__)
class Events(Base):
class Events(Base): # type: ignore
# pylint: disable=too-few-public-methods
"""Event history data."""
@ -55,7 +55,7 @@ class Events(Base):
return None
class States(Base):
class States(Base): # type: ignore
# pylint: disable=too-few-public-methods
"""State change history."""
@ -114,7 +114,7 @@ class States(Base):
return None
class RecorderRuns(Base):
class RecorderRuns(Base): # type: ignore
# pylint: disable=too-few-public-methods
"""Representation of recorder run."""

View File

@ -100,6 +100,7 @@ DEVICE_SCHEMA = vol.Schema({
DEVICE_SCHEMA_SENSOR = vol.Schema({
vol.Optional(ATTR_NAME, default=None): cv.string,
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
vol.Optional(ATTR_DATA_TYPE, default=[]):
vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]),
})

View File

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

View File

@ -49,14 +49,20 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
def value_changed(self, value):
"""Called when a value has changed on the network."""
if self._value.value_id == value.value_id:
self.update_ha_state(True)
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value)
@property
def current_position(self):
"""Return the current position of Zwave roller shutter."""
return self._value.data
if self._value.data <= 5:
return 100
elif self._value.data >= 95:
return 0
else:
return 100 - self._value.data
def move_up(self, **kwargs):
"""Move the roller shutter up."""

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