Merge pull request #202 from balloob/dev

Update master with latest changes
This commit is contained in:
Paulus Schoutsen 2015-07-11 02:00:18 -07:00
commit 33b7585e0c
114 changed files with 9125 additions and 7588 deletions

View File

@ -7,6 +7,9 @@ omit =
homeassistant/external/*
# omit pieces of code that rely on external devices being present
homeassistant/components/arduino.py
homeassistant/components/*/arduino.py
homeassistant/components/wink.py
homeassistant/components/*/wink.py
@ -32,6 +35,7 @@ omit =
homeassistant/components/light/hue.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/mpd.py
homeassistant/components/notify/file.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
@ -40,6 +44,8 @@ omit =
homeassistant/components/notify/syslog.py
homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/sabnzbd.py
@ -47,6 +53,7 @@ omit =
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/transmission.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/nest.py

4
.gitignore vendored
View File

@ -65,3 +65,7 @@ nosetests.xml
.pydevproject
.python-version
# venv stuff
pyvenv.cfg
pip-selfcheck.json

View File

@ -1,3 +1,4 @@
sudo: false
language: python
python:
- "3.4"

View File

@ -1,50 +1,45 @@
# Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) [![Join the chat at https://gitter.im/balloob/home-assistant](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
This is the source code for Home Assistant. For installation instructions, tutorials and the docs, please see [the website](https://home-assistant.io). For a functioning demo frontend of Home Assistant, [click here](https://home-assistant.io/demo/).
Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control. [Open a demo.](https://home-assistant.io/demo/)
Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control.
It offers the following functionality through built-in components:
* Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index))
* Track and control [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
* Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/)
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), and [Modbus](http://www.modbus.org/)
* Track running system services and monitoring your system stats (Memory, disk usage, and more)
* Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands
* Turn on the lights when people get home after sun set
* Turn on lights slowly during sun set to compensate for light loss
* Turn off all lights and devices when everybody leaves the house
* Offers web interface to monitor and control Home Assistant
* Offers a [REST API](https://home-assistant.io/developers/api.html) for easy integration with other projects
* [Ability to have multiple instances of Home Assistant work together](https://home-assistant.io/developers/architecture.html)
* Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), and [Jabber (XMPP)](http://xmpp.org)
* Allow to display details about a running [Transmission](http://www.transmissionbt.com/) client, the [Bitcoin](https://bitcoin.org) network, local meteorological data from [OpenWeatherMap](http://openweathermap.org/), the time, the date, and the downloads from [SABnzbd](http://sabnzbd.org)
Home Assistant also includes functionality for controlling HTPCs:
* Simulate key presses for Play/Pause, Next track, Prev track, Volume up, Volume Down
* Download files
* Open URLs in the default browser
Check out [the website](https://home-assistant.io) for installation instructions, tutorials and documentation.
[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)](https://home-assistant.io/demo/)
Examples of devices it can interface it:
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/)
* [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/) and [Kodi (XBMC)](http://kodi.tv/)
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), and [Modbus](http://www.modbus.org/)
* Integrate data from the [Bitcoin](https://bitcoin.org) network, local meteorological data from [OpenWeatherMap](http://openweathermap.org/), [Transmission](http://www.transmissionbt.com/) or [SABnzbd](http://sabnzbd.org).
* [See full list of supported devices](https://home-assistant.io/components/)
Built home automation on top of your devices:
* Keep a precise history of every change to the state of your house
* Turn on the lights when people get home after sun set
* Turn on lights slowly during sun set 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.html) for easy integration with other projects
* Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), and [Jabber (XMPP)](http://xmpp.org)
The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html).
If you run into issues while using Home Assistant or during development of a component, reach out to the [Home Assistant developer community](https://groups.google.com/forum/#!forum/home-assistant-dev).
If you run into issues while using Home Assistant or during development of a component, reach out to the [Home Assistant help section](https://home-assistant.io/help/) how to reach us.
## Installation instructions / Quick-start guide
## Quick-start guide
Running Home Assistant requires that [Python](https://www.python.org/) 3.4 and the package [requests](http://docs.python-requests.org/en/latest/) are installed. Run the following code to install and start Home Assistant:
Running Home Assistant requires [Python 3.4](https://www.python.org/). Run the following code to get up and running:
```python
```
git clone --recursive https://github.com/balloob/home-assistant.git
python3 -m venv home-assistant
cd home-assistant
python3 -m pip install --user -r requirements.txt
python3 -m homeassistant --open-ui
```
The last command will start the Home Assistant server and launch its web interface. By default Home Assistant looks for the configuration file `config/home-assistant.conf`. A standard configuration file will be written if none exists.
The last command will start the Home Assistant server and launch its web interface. By default Home Assistant looks for the configuration file `config/configuration.yaml`. A standard configuration file will be written if none exists.
If you are still exploring if you want to use Home Assistant in the first place, you can enable the demo mode by adding the `--demo-mode` argument to the last command.

View File

@ -159,5 +159,5 @@ scene:
light.tv_back_light: on
light.ceiling:
state: on
color: [0.33, 0.66]
xy_color: [0.33, 0.66]
brightness: 200

View File

@ -4,15 +4,9 @@ from __future__ import print_function
import sys
import os
import argparse
import importlib
import subprocess
# Home Assistant dependencies, mapped module -> package name
DEPENDENCIES = {
'requests': 'requests',
'yaml': 'pyyaml',
'pytz': 'pytz',
}
DEPENDENCIES = ['requests>=2.0', 'pyyaml>=3.11', 'pytz>=2015.2']
def validate_python():
@ -24,21 +18,29 @@ def validate_python():
sys.exit()
# Copy of homeassistant.util.package because we can't import yet
def install_package(package):
"""Install a package on PyPi. Accepts pip compatible package strings.
Return boolean if install successfull."""
args = ['python3', '-m', 'pip', 'install', '--quiet', package]
if sys.base_prefix == sys.prefix:
args.append('--user')
return not subprocess.call(args)
def validate_dependencies():
""" Validate all dependencies that HA uses. """
print("Validating dependencies...")
import_fail = False
for module, name in DEPENDENCIES.items():
try:
importlib.import_module(module)
except ImportError:
for requirement in DEPENDENCIES:
if not install_package(requirement):
import_fail = True
print(
'Fatal Error: Unable to find dependency {}'.format(name))
print('Fatal Error: Unable to install dependency', requirement)
if import_fail:
print(("Install dependencies by running: "
"pip3 install -r requirements.txt"))
"python3 -m pip install -r requirements.txt"))
sys.exit()

View File

@ -14,8 +14,9 @@ import logging
from collections import defaultdict
import homeassistant
import homeassistant.util as util
import homeassistant.util.dt as date_util
import homeassistant.util.package as pkg_util
import homeassistant.util.location as loc_util
import homeassistant.config as config_util
import homeassistant.loader as loader
import homeassistant.components as core_components
@ -60,6 +61,17 @@ def setup_component(hass, domain, config=None):
return True
def _handle_requirements(component, name):
""" Installs requirements for component. """
if hasattr(component, 'REQUIREMENTS'):
for req in component.REQUIREMENTS:
if not pkg_util.install_package(req):
_LOGGER.error('Not initializing %s because could not install '
'dependency %s', name, req)
return False
return True
def _setup_component(hass, domain, config):
""" Setup a component for Home Assistant. """
component = loader.get_component(domain)
@ -74,6 +86,9 @@ def _setup_component(hass, domain, config):
return False
if not _handle_requirements(component, domain):
return False
try:
if component.setup(hass, config):
hass.config.components.append(component.DOMAIN)
@ -109,18 +124,22 @@ def prepare_setup_platform(hass, config, domain, platform_name):
if platform is None:
return None
# Already loaded or no dependencies
elif (platform_path in hass.config.components or
not hasattr(platform, 'DEPENDENCIES')):
# Already loaded
elif platform_path in hass.config.components:
return platform
# Load dependencies
for component in platform.DEPENDENCIES:
if not setup_component(hass, component, config):
_LOGGER.error(
'Unable to prepare setup for platform %s because dependency '
'%s could not be initialized', platform_path, component)
return None
if hasattr(platform, 'DEPENDENCIES'):
for component in platform.DEPENDENCIES:
if not setup_component(hass, component, config):
_LOGGER.error(
'Unable to prepare setup for platform %s because '
'dependency %s could not be initialized', platform_path,
component)
return None
if not _handle_requirements(platform, platform_path):
return None
return platform
@ -276,7 +295,7 @@ def process_ha_core_config(hass, config):
_LOGGER.info('Auto detecting location and temperature unit')
info = util.detect_location_info()
info = loc_util.detect_location_info()
if info is None:
_LOGGER.error('Could not detect location information')

View File

@ -0,0 +1,143 @@
"""
components.arduino
~~~~~~~~~~~~~~~~~~
Arduino component that connects to a directly attached Arduino board which
runs with the Firmata firmware.
Configuration:
To use the Arduino board you will need to add something like the following
to your config/configuration.yaml
arduino:
port: /dev/ttyACM0
Variables:
port
*Required
The port where is your board connected to your Home Assistant system.
If you are using an original Arduino the port will be named ttyACM*. The exact
number can be determined with 'ls /dev/ttyACM*' or check your 'dmesg'/
'journalctl -f' output. Keep in mind that Arduino clones are often using a
different name for the port (e.g. '/dev/ttyUSB*').
A word of caution: The Arduino is not storing states. This means that with
every initialization the pins are set to off/low.
"""
import logging
try:
from PyMata.pymata import PyMata
except ImportError:
PyMata = None
from homeassistant.helpers import validate_config
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
DOMAIN = "arduino"
DEPENDENCIES = []
REQUIREMENTS = ['PyMata==2.07a']
BOARD = None
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Setup the Arduino component. """
global PyMata # pylint: disable=invalid-name
if PyMata is None:
from PyMata.pymata import PyMata as PyMata_
PyMata = PyMata_
import serial
if not validate_config(config,
{DOMAIN: ['port']},
_LOGGER):
return False
global BOARD
try:
BOARD = ArduinoBoard(config[DOMAIN]['port'])
except (serial.serialutil.SerialException, FileNotFoundError):
_LOGGER.exception("Your port is not accessible.")
return False
if BOARD.get_firmata()[1] <= 2:
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer.")
return False
def stop_arduino(event):
""" Stop the Arduino service. """
BOARD.disconnect()
def start_arduino(event):
""" Start the Arduino service. """
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_arduino)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_arduino)
return True
class ArduinoBoard(object):
""" Represents an Arduino board. """
def __init__(self, port):
self._port = port
self._board = PyMata(self._port, verbose=False)
def set_mode(self, pin, direction, mode):
""" Sets the mode and the direction of a given pin. """
if mode == 'analog' and direction == 'in':
self._board.set_pin_mode(pin,
self._board.INPUT,
self._board.ANALOG)
elif mode == 'analog' and direction == 'out':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.ANALOG)
elif mode == 'digital' and direction == 'in':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.DIGITAL)
elif mode == 'digital' and direction == 'out':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.DIGITAL)
elif mode == 'pwm':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.PWM)
def get_analog_inputs(self):
""" Get the values from the pins. """
self._board.capability_query()
return self._board.get_analog_response_table()
def set_digital_out_high(self, pin):
""" Sets a given digital pin to high. """
self._board.digital_write(pin, 1)
def set_digital_out_low(self, pin):
""" Sets a given digital pin to low. """
self._board.digital_write(pin, 0)
def get_digital_in(self, pin):
""" Gets the value from a given digital pin. """
self._board.digital_read(pin)
def get_analog_in(self, pin):
""" Gets the value from a given analog pin. """
self._board.analog_read(pin)
def get_firmata(self):
""" Return the version of the Firmata firmware. """
return self._board.get_firmata_version()
def disconnect(self):
""" Disconnects the board and closes the serial connection. """
self._board.reset()
self._board.close()

View File

@ -0,0 +1,227 @@
# pylint: disable=too-many-lines
"""
homeassistant.components.camera
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with various cameras.
The following features are supported:
- Returning recorded camera images and streams
- Proxying image requests via HA for external access
- Converting a still image url into a live video stream
Upcoming features
- Recording
- Snapshot
- Motion Detection Recording(for supported cameras)
- Automatic Configuration(for supported cameras)
- Creation of child entities for supported functions
- Collating motion event images passed via FTP into time based events
- A service for calling camera functions
- Camera movement(panning)
- Zoom
- Light/Nightvision toggling
- Support for more devices
- Expanded documentation
"""
import requests
import logging
import time
import re
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
HTTP_NOT_FOUND,
ATTR_ENTITY_ID,
)
from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'camera'
DEPENDENCIES = ['http']
GROUP_NAME_ALL_CAMERAS = 'all_cameras'
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SWITCH_ACTION_RECORD = 'record'
SWITCH_ACTION_SNAPSHOT = 'snapshot'
SERVICE_CAMERA = 'camera_service'
STATE_RECORDING = 'recording'
DEFAULT_RECORDING_SECONDS = 30
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {}
FILE_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S-%f'
DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
REC_DIR_PREFIX = 'recording-'
REC_IMG_PREFIX = 'recording_image-'
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'
CAMERA_PROXY_URL = '/api/camera_proxy_stream/{0}'
CAMERA_STILL_URL = '/api/camera_proxy/{0}'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?time={1}'
MULTIPART_BOUNDARY = '--jpegboundary'
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
# pylint: disable=too-many-branches
def setup(hass, config):
""" Track states and offer events for sensors. """
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
component.setup(config)
# -------------------------------------------------------------------------
# CAMERA COMPONENT ENDPOINTS
# -------------------------------------------------------------------------
# The following defines the endpoints for serving images from the camera
# via the HA http server. This is means that you can access images from
# your camera outside of your LAN without the need for port forwards etc.
# Because the authentication header can't be added in image requests these
# endpoints are secured with session based security.
# pylint: disable=unused-argument
def _proxy_camera_image(handler, path_match, data):
""" Proxies the camera image via the HA server. """
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = None
if entity_id in component.entities.keys():
camera = component.entities[entity_id]
if camera:
response = camera.camera_image()
handler.wfile.write(response)
else:
handler.send_response(HTTP_NOT_FOUND)
hass.http.register_path(
'GET',
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_image,
require_auth=True)
# pylint: disable=unused-argument
def _proxy_camera_mjpeg_stream(handler, path_match, data):
""" Proxies the camera image as an mjpeg stream via the HA server.
This function takes still images from the IP camera and turns them
into an MJPEG stream. This means that HA can return a live video
stream even with only a still image URL available.
"""
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = None
if entity_id in component.entities.keys():
camera = component.entities[entity_id]
if not camera:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
try:
camera.is_streaming = True
camera.update_ha_state()
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
handler.request.sendall(bytes(
'Content-type: multipart/x-mixed-replace; \
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
# MJPEG_START_HEADER.format()
while True:
img_bytes = camera.camera_image()
headers_str = '\r\n'.join((
'Content-length: {}'.format(len(img_bytes)),
'Content-type: image/jpeg',
)) + '\r\n\r\n'
handler.request.sendall(
bytes(headers_str, 'utf-8') +
img_bytes +
bytes('\r\n', 'utf-8'))
handler.request.sendall(
bytes('--jpgboundary\r\n', 'utf-8'))
except (requests.RequestException, IOError):
camera.is_streaming = False
camera.update_ha_state()
camera.is_streaming = False
hass.http.register_path(
'GET',
re.compile(
r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_mjpeg_stream,
require_auth=True)
return True
class Camera(Entity):
""" The base class for camera components """
def __init__(self):
self.is_streaming = False
@property
# pylint: disable=no-self-use
def is_recording(self):
""" Returns true if the device is recording """
return False
@property
# pylint: disable=no-self-use
def brand(self):
""" Should return a string of the camera brand """
return None
@property
# pylint: disable=no-self-use
def model(self):
""" Returns string of camera model """
return None
def camera_image(self):
""" Return bytes of camera image """
raise NotImplementedError()
@property
def state(self):
""" Returns the state of the entity. """
if self.is_recording:
return STATE_RECORDING
elif self.is_streaming:
return STATE_STREAMING
else:
return STATE_IDLE
@property
def state_attributes(self):
""" Returns optional state attributes. """
return {
'model_name': self.model,
'brand': self.brand,
'still_image_url': CAMERA_STILL_URL.format(self.entity_id),
ATTR_ENTITY_PICTURE: ENTITY_IMAGE_URL.format(
self.entity_id, str(time.time())),
'stream_url': CAMERA_PROXY_URL.format(self.entity_id)
}

View File

@ -0,0 +1,94 @@
"""
Support for IP Cameras.
This component provides basic support for IP cameras. For the basic support to
work you camera must support accessing a JPEG snapshot via a URL and you will
need to specify the "still_image_url" parameter which should be the location of
the JPEG image.
As part of the basic support the following features will be provided:
-MJPEG video streaming
-Saving a snapshot
-Recording(JPEG frame capture)
To use this component, add the following to your config/configuration.yaml:
camera:
platform: generic
name: Door Camera
username: YOUR_USERNAME
password: YOUR_PASSWORD
still_image_url: http://YOUR_CAMERA_IP_AND_PORT/image.jpg
VARIABLES:
These are the variables for the device_data array:
still_image_url
*Required
The URL your camera serves the image on.
Example: http://192.168.1.21:2112/
name
*Optional
This parameter allows you to override the name of your camera in homeassistant
username
*Optional
THe username for acessing your camera
password
*Optional
the password for accessing your camera
"""
import logging
from requests.auth import HTTPBasicAuth
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN
from homeassistant.components.camera import Camera
import requests
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Adds a generic IP Camera. """
if not validate_config({DOMAIN: config}, {DOMAIN: ['still_image_url']},
_LOGGER):
return None
add_devices_callback([GenericCamera(config)])
# pylint: disable=too-many-instance-attributes
class GenericCamera(Camera):
"""
A generic implementation of an IP camera that is reachable over a URL.
"""
def __init__(self, device_info):
super().__init__()
self._name = device_info.get('name', 'Generic Camera')
self._username = device_info.get('username')
self._password = device_info.get('password')
self._still_image_url = device_info['still_image_url']
def camera_image(self):
""" Return a still image reponse from the camera """
if self._username and self._password:
response = requests.get(
self._still_image_url,
auth=HTTPBasicAuth(self._username, self._password))
else:
response = requests.get(self._still_image_url)
return response.content
@property
def name(self):
""" Return the name of this device """
return self._name

View File

@ -51,6 +51,15 @@ def setup(hass, config):
group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0]])
group.setup_group(hass, 'bedroom', [lights[2], switches[1]])
# Setup IP Camera
bootstrap.setup_component(
hass, 'camera',
{'camera': {
'platform': 'generic',
'name': 'IP Camera',
'still_image_url': 'http://194.218.96.92/jpg/image.jpg',
}})
# Setup scripts
bootstrap.setup_component(
hass, 'script',
@ -93,17 +102,17 @@ def setup(hass, config):
# Setup fake device tracker
hass.states.set("device_tracker.paulus", "home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/schoutsen/picture"})
"http://graph.facebook.com/297400035/picture"})
hass.states.set("device_tracker.anne_therese", "not_home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/anne.t.frederiksen/picture"})
"http://graph.facebook.com/621994601/picture"})
hass.states.set("group.all_devices", "home",
{
"auto": True,
ATTR_ENTITY_ID: [
"device_tracker.Paulus",
"device_tracker.Anne_Therese"
"device_tracker.paulus",
"device_tracker.anne_therese"
]
})

View File

@ -0,0 +1,117 @@
"""
homeassistant.components.device_tracker.tplink
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a TP-Link router for device
presence.
Configuration:
To use the TP-Link tracker you will need to add something like the following
to your config/configuration.yaml
device_tracker:
platform: tplink
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
import threading
import requests
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
def get_scanner(hass, config):
""" Validates config and returns a TP-Link scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = TplinkDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class TplinkDeviceScanner(object):
""" This class queries a wireless router running TP-Link firmware
for connected devices.
"""
def __init__(self, config):
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.parse_macs = re.compile('[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}-' +
'[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}')
self.host = host
self.username = username
self.password = password
self.last_results = {}
self.lock = threading.Lock()
self.success_init = self._update_info()
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return self.last_results
# pylint: disable=no-self-use
def get_device_name(self, device):
""" The TP-Link firmware doesn't save the name of the wireless
device. """
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the TP-Link router is up to date.
Returns boolean if scanning successful. """
with self.lock:
_LOGGER.info("Loading wireless clients...")
url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host)
referer = 'http://{}'.format(self.host)
page = requests.get(url, auth=(self.username, self.password),
headers={'referer': referer})
result = self.parse_macs.findall(page.text)
if result:
self.last_results = [mac.replace("-", ":") for mac in result]
return True
return False

View File

@ -22,6 +22,7 @@ from homeassistant.const import (
DOMAIN = "discovery"
DEPENDENCIES = []
REQUIREMENTS = ['zeroconf>=0.16.0']
SCAN_INTERVAL = 300 # seconds

View File

@ -20,13 +20,21 @@ INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
_LOGGER = logging.getLogger(__name__)
FRONTEND_URLS = [
URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent']
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
def setup(hass, config):
""" Setup serving the frontend. """
if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded')
return False
hass.http.register_path('GET', URL_ROOT, _handle_get_root, False)
for url in FRONTEND_URLS:
hass.http.register_path('GET', url, _handle_get_root, False)
hass.http.register_path('GET', STATES_URL, _handle_get_root, False)
# Static files
hass.http.register_path(

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "db1ec3e116565340804da0e590058d60"
VERSION = "301633b1e436a798afcbdb5776744588"

File diff suppressed because one or more lines are too long

View File

@ -31,12 +31,13 @@
"paper-slider": "PolymerElements/paper-slider#^1.0.0",
"paper-checkbox": "PolymerElements/paper-checkbox#^1.0.0",
"paper-drawer-panel": "PolymerElements/paper-drawer-panel#^1.0.0",
"paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#~1.0",
"paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#^1.0.0",
"google-apis": "GoogleWebComponents/google-apis#0.8-preview",
"moment": "^2.10.3",
"layout": "Polymer/layout",
"color-picker-element": "~0.0.3",
"paper-styles": "polymerelements/paper-styles#~1.0"
"paper-styles": "polymerelements/paper-styles#^1.0.0",
"lodash": "~3.9.3",
"pikaday": "~1.3.2"
},
"resolutions": {
"polymer": "^1.0.0",

View File

@ -16,6 +16,10 @@
cursor: pointer;
overflow: hidden;
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
</style>
@ -26,7 +30,7 @@
<script>
(function(){
var uiActions = window.hass.uiActions;
var moreInfoActions = window.hass.moreInfoActions;
Polymer({
is: 'state-card',
@ -41,8 +45,10 @@
'tap': 'cardTapped',
},
cardTapped: function() {
uiActions.showMoreInfoDialog(this.stateObj.entityId);
cardTapped: function(ev) {
ev.stopPropagation();
this.async(moreInfoActions.selectEntity.bind(
this, this.stateObj.entityId), 100);
},
});
})();

View File

@ -20,7 +20,7 @@
<template>
<ul>
<template is='dom-repeat' items='[[entities]]' as='entity'>
<li><a href='#' on-click='entitySelected'>[[entity]]</a></li>
<li><a href='#' on-click='entitySelected'>[[entity.entityId]]</a></li>
</template>
</ul>
</template>
@ -28,25 +28,30 @@
<script>
(function() {
var entityGetters = window.hass.entityGetters;
Polymer({
is: 'entity-list',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
entities: {
type: Array,
value: [],
bindNuclear: [
entityGetters.entityMap,
function(map) {
return map.valueSeq().
sortBy(function(entity) { return entity.entityId; })
.toArray();
},
],
},
},
stateStoreChanged: function(stateStore) {
this.entities = stateStore.entityIDs.toArray();
},
entitySelected: function(ev) {
ev.preventDefault();
this.fire('entity-selected', {entityId: ev.model.entity});
this.fire('entity-selected', {entityId: ev.model.entity.entityId});
},
});
})();

View File

@ -31,22 +31,27 @@
<script>
(function() {
var eventGetters = window.hass.eventGetters;
Polymer({
is: 'events-list',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
events: {
type: Array,
value: [],
bindNuclear: [
eventGetters.entityMap,
function(map) {
return map.valueSeq()
.sortBy(function(event) { return event.event; })
.toArray();
}
],
},
},
eventStoreChanged: function(eventStore) {
this.events = eventStore.all.toArray();
},
eventSelected: function(ev) {
ev.preventDefault();
this.fire('event-selected', {eventType: ev.model.event.event});

View File

@ -0,0 +1,169 @@
<link rel='import' href='../bower_components/polymer/polymer.html'>
<dom-module id='ha-color-picker'>
<style>
canvas {
cursor: crosshair;
}
</style>
<template>
<canvas width='[[width]]' height='[[height]]'></canvas>
</template>
</dom-module>
<script>
/**
* Color-picker custom element
* Originally created by bbrewer97202 (Ben Brewer). MIT Licensed.
* https://github.com/bbrewer97202/color-picker-element
*
* Adapted to work with Polymer.
*/
(function() {
/**
* given red, green, blue values, return the equivalent hexidecimal value
* base source: http://stackoverflow.com/a/5624139
*/
var componentToHex = function(c) {
var hex = c.toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
var rgbToHex = function(color) {
return "#" + componentToHex(color.r) + componentToHex(color.g) +
componentToHex(color.b);
};
Polymer({
is: 'ha-color-picker',
properties: {
width: {
type: Number,
value: 300,
},
height: {
type: Number,
value: 300,
},
color: {
type: Object,
},
},
listeners: {
'mousedown': 'onMouseDown',
'mouseup': 'onMouseUp',
'touchstart': 'onTouchStart',
'touchend': 'onTouchEnd',
'tap': 'onTap',
},
onMouseDown: function(e) {
this.onMouseMove(e);
this.addEventListener('mousemove', this.onMouseMove);
},
onMouseUp: function(e) {
this.removeEventListener('mousemove', this.onMouseMove);
},
onTouchStart: function(e) {
this.onTouchMove(e);
this.addEventListener('touchmove', this.onTouchMove);
},
onTouchEnd: function(e) {
this.removeEventListener('touchmove', this.onTouchMove);
},
onTap: function(e) {
e.stopPropagation();
},
onTouchMove: function(e) {
var touch = e.touches[0];
this.onColorSelect(e, {x: touch.clientX, y: touch.clientY});
},
onMouseMove: function(e) {
e.preventDefault();
if (this.mouseMoveIsThrottled) {
this.mouseMoveIsThrottled = false;
this.onColorSelect(e);
this.async(
function() { this.mouseMoveIsThrottled = true; }.bind(this), 100);
}
},
onColorSelect: function(e, coords) {
if (this.context) {
coords = coords || this.relativeMouseCoordinates(e);
var data = this.context.getImageData(coords.x, coords.y, 1, 1).data;
this.setColor({r: data[0], g: data[1], b: data[2]});
}
},
setColor: function(rgb) {
//save calculated color
this.color = {hex: rgbToHex(rgb), rgb: rgb};
this.fire('colorselected', {
rgb: this.color.rgb,
hex: this.color.hex
});
},
/**
* given a mouse click event, return x,y coordinates relative to the clicked target
* @returns object with x, y values
*/
relativeMouseCoordinates: function(e) {
var x = 0, y = 0;
if (this.canvas) {
var rect = this.canvas.getBoundingClientRect();
x = e.clientX - rect.left;
y = e.clientY - rect.top;
}
return {
x: x,
y: y
};
},
ready: function() {
this.setColor = this.setColor.bind(this);
this.mouseMoveIsThrottled = true;
this.canvas = this.children[0];
this.context = this.canvas.getContext('2d');
var colorGradient = this.context.createLinearGradient(0, 0, this.width, 0);
colorGradient.addColorStop(0, "rgb(255,0,0)");
colorGradient.addColorStop(0.16, "rgb(255,0,255)");
colorGradient.addColorStop(0.32, "rgb(0,0,255)");
colorGradient.addColorStop(0.48, "rgb(0,255,255)");
colorGradient.addColorStop(0.64, "rgb(0,255,0)");
colorGradient.addColorStop(0.80, "rgb(255,255,0)");
colorGradient.addColorStop(1, "rgb(255,0,0)");
this.context.fillStyle = colorGradient;
this.context.fillRect(0, 0, this.width, this.height);
var bwGradient = this.context.createLinearGradient(0, 0, 0, this.height);
bwGradient.addColorStop(0, "rgba(255,255,255,1)");
bwGradient.addColorStop(0.5, "rgba(255,255,255,0)");
bwGradient.addColorStop(0.5, "rgba(0,0,0,0)");
bwGradient.addColorStop(1, "rgba(0,0,0,1)");
this.context.fillStyle = bwGradient;
this.context.fillRect(0, 0, this.width, this.height);
},
});
})();
</script>

View File

@ -10,6 +10,9 @@
}
</style>
<template>
<template is='dom-if' if='[[noEntries(entries)]]'>
No logbook entries found.
</template>
<template is='dom-repeat' items="[[entries]]">
<logbook-entry entry-obj="[[item]]"></logbook-entry>
</template>
@ -27,6 +30,10 @@
value: [],
},
},
noEntries: function(entries) {
return !entries.length;
}
});
})();
</script>

View File

@ -0,0 +1,247 @@
<link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel='import' href='../bower_components/layout/layout.html'>
<link rel='import' href='../bower_components/paper-header-panel/paper-header-panel.html'>
<link rel='import' href='../bower_components/paper-toolbar/paper-toolbar.html'>
<!--
Too broken for now.
<link rel='import' href='../bower_components/paper-menu/paper-menu.html'> -->
<link rel='import' href='../bower_components/iron-icon/iron-icon.html'>
<link rel='import' href='../bower_components/paper-item/paper-item.html'>
<link rel='import' href='../bower_components/paper-item/paper-icon-item.html'>
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
<link rel='import' href='../components/stream-status.html'>
<dom-module id='ha-sidebar'>
<style>
.sidenav {
background: #fafafa;
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.1);
overflow: hidden;
white-space: nowrap;
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
/*.sidenav paper-menu {
--paper-menu-color: var(--secondary-text-color);
--paper-menu-background-color: #fafafa;
}*/
div.menu {
color: var(--secondary-text-color);
background-color: #fafafa;
}
paper-icon-item {
cursor: pointer;
}
paper-icon-item.selected {
font-weight: bold;
}
paper-icon-item.logout {
margin-top: 16px;
}
.divider {
border-top: 1px solid #e0e0e0;
}
.text {
padding: 16px;
font-size: 14px;
}
.dev-tools {
padding: 0 8px;
}
</style>
<template>
<paper-header-panel mode='scroll' class='sidenav fit'>
<paper-toolbar>
<!-- forces paper toolbar to style title appropriate -->
<paper-icon-button hidden></paper-icon-button>
<div class="title">Home Assistant</div>
</paper-toolbar>
<!-- <paper-menu id='menu' selected='{{menuSelected}}'
selectable='[data-panel]' attr-for-selected='data-panel'> -->
<div class='menu'>
<paper-icon-item on-click='menuClicked' data-panel='states'>
<iron-icon item-icon icon='apps'></iron-icon> States
</paper-icon-item>
<template is='dom-repeat' items='{{possibleFilters}}'>
<paper-icon-item on-click='menuClicked' data-panel$='[[filterType(item)]]'>
<iron-icon item-icon icon='[[filterIcon(item)]]'></iron-icon>
<span>[[filterName(item)]]</span>
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasHistoryComponent]]'>
<paper-icon-item on-click='menuClicked' data-panel='history'>
<iron-icon item-icon icon='assessment'></iron-icon>
History
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasLogbookComponent]]'>
<paper-icon-item on-click='menuClicked' data-panel='logbook'>
<iron-icon item-icon icon='list'></iron-icon>
Logbook
</paper-icon-item>
</template>
<paper-icon-item on-click='menuClicked' data-panel='logout' class='logout'>
<iron-icon item-icon icon='exit-to-app'></iron-icon>
Log Out
</paper-icon-item>
<paper-item class='divider horizontal layout justified'>
<div>Streaming updates</div>
<stream-status></stream-status>
</paper-item>
<div class='text label divider'>Developer Tools</div>
<div class='dev-tools layout horizontal justified'>
<paper-icon-button
icon='settings-remote' data-panel='devService'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-ethernet' data-panel='devState'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-input-antenna' data-panel='devEvent'
on-click='handleDevClick'></paper-icon-button>
</div>
<!-- </paper-menu> -->
</div>
</paper-header-panel>
</template>
</dom-module>
<script>
(function() {
var configGetters = window.hass.configGetters;
var navigationGetters = window.hass.navigationGetters;
var authActions = window.hass.authActions;
var navigationActions = window.hass.navigationActions;
var uiUtil = window.hass.uiUtil;
var entityDomainFilters = window.hass.util.entityDomainFilters;
Polymer({
is: 'ha-sidebar',
behaviors: [nuclearObserver],
properties: {
menuSelected: {
type: String,
// observer: 'menuSelectedChanged',
},
selected: {
type: String,
bindNuclear: navigationGetters.activePage,
observer: 'selectedChanged',
},
possibleFilters: {
type: Array,
value: [],
bindNuclear: [
navigationGetters.possibleEntityDomainFilters,
function(domains) { return domains.toArray(); }
],
},
hasHistoryComponent: {
type: Boolean,
bindNuclear: configGetters.isComponentLoaded('history'),
},
hasLogbookComponent: {
type: Boolean,
bindNuclear: configGetters.isComponentLoaded('logbook'),
},
},
// menuSelectedChanged: function(newVal) {
// if (this.selected !== newVal) {
// this.selectPanel(newVal);
// }
// },
selectedChanged: function(newVal) {
// if (this.menuSelected !== newVal) {
// this.menuSelected = newVal;
// }
var menuItems = this.querySelectorAll('.menu [data-panel]');
for (var i = 0; i < menuItems.length; i++) {
if(menuItems[i].dataset.panel === newVal) {
menuItems[i].classList.add('selected');
} else {
menuItems[i].classList.remove('selected');
}
}
},
menuClicked: function(ev) {
var target = ev.target;
var checks = 5;
// find panel to select
while(checks && !target.dataset.panel) {
target = target.parentElement;
checks--;
}
if (checks) {
this.selectPanel(target.dataset.panel);
}
},
handleDevClick: function(ev) {
// prevent it from highlighting first menu item
document.activeElement.blur();
this.menuClicked(ev);
},
selectPanel: function(newChoice) {
if(newChoice === this.selected) {
return;
} else if (newChoice == 'logout') {
this.handleLogOut();
return;
}
navigationActions.navigate.apply(null, newChoice.split('/'));
},
handleLogOut: function() {
authActions.logOut();
},
filterIcon: function(filter) {
return uiUtil.domainIcon(filter);
},
filterName: function(filter) {
return entityDomainFilters[filter];
},
filterType: function(filter) {
return 'states/' + filter;
},
});
})();
</script>

View File

@ -0,0 +1,61 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
<dom-module id="ha-voice-command-progress">
<style>
:host {
display: block;
}
iron-icon {
margin-right: 8px;
}
.interimTranscript {
color: darkgrey;
}
.listening paper-spinner {
float: right;
}
</style>
<template>
<iron-icon icon="av:hearing"></iron-icon>
<span>{{finalTranscript}}</span>
<span class='interimTranscript'>[[interimTranscript]]</span>
<paper-spinner active$="[[isTransmitting]]" alt="Sending voice command to Home Assistant"></paper-spinner>
</template>
</dom-module>
<script>
(function(){
var voiceGetters = window.hass.voiceGetters;
Polymer({
is: 'ha-voice-command-progress',
behaviors: [nuclearObserver],
properties: {
isTransmitting: {
type: Boolean,
bindNuclear: voiceGetters.isTransmitting,
},
interimTranscript: {
type: String,
bindNuclear: voiceGetters.extraInterimTranscript,
},
finalTranscript: {
type: String,
bindNuclear: voiceGetters.finalTranscript,
},
},
});
})();
</script>

View File

@ -54,14 +54,14 @@
<script>
(function() {
var uiActions = window.hass.uiActions;
var moreInfoActions = window.hass.moreInfoActions;
Polymer({
is: 'logbook-entry',
entityClicked: function(ev) {
ev.preventDefault();
uiActions.showMoreInfoDialog(this.entryObj.entityId);
moreInfoActions.selectEntity(this.entryObj.entityId);
}
});

View File

@ -23,10 +23,10 @@
<template>
<ul>
<template is='dom-repeat' items="[[domains]]" as="domain">
<template is='dom-repeat' items="[[computeServices(domain)]]" as="service">
<template is='dom-repeat' items="[[serviceDomains]]" as="domain">
<template is='dom-repeat' items="[[domain.services]]" as="service">
<li><a href='#' on-click='serviceClicked'>
<span>[[domain]]</span>/<span>[[service]]</span>
<span>[[domain.domain]]</span>/<span>[[service]]</span>
</a></li>
</template>
</template>
@ -36,19 +36,24 @@
<script>
(function() {
var serviceGetters = window.hass.serviceGetters;
Polymer({
is: 'services-list',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
domains: {
serviceDomains: {
type: Array,
value: [],
},
services: {
type: Object,
bindNuclear: [
serviceGetters.entityMap,
function(map) {
return map.valueSeq()
.sortBy(function(domain) { return domain.domain; })
.toJS();
},
],
},
},
@ -56,15 +61,10 @@
return this.services.get(domain).toArray();
},
serviceStoreChanged: function(serviceStore) {
this.services = serviceStore.all;
this.domains = this.services.keySeq().sort().toArray();
},
serviceClicked: function(ev) {
ev.preventDefault();
this.fire(
'service-selected', {domain: ev.model.domain, service: ev.model.service});
'service-selected', {domain: ev.model.domain.domain, service: ev.model.service});
},
});
})();

View File

@ -1,5 +1,7 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../resources/lodash.html">
<script>
(function() {
Polymer({

View File

@ -1,5 +1,14 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<dom-module is='state-history-chart-timeline'>
<style>
:host {
display: block;
}
</style>
<template></template>
</dom-module>
<script>
(function() {
Polymer({
@ -18,10 +27,6 @@
},
},
created: function() {
this.style.display = 'block';
},
attached: function() {
this.isAttached = true;
},
@ -34,18 +39,17 @@
if (!this.isAttached) {
return;
}
var root = Polymer.dom(this);
var stateHistory = this.data;
while (root.lastChild) {
root.removeChild(root.lastChild);
while (root.node.lastChild) {
root.node.removeChild(root.node.lastChild);
}
if (!stateHistory || stateHistory.length === 0) {
return;
}
// debugger;
var chart = new google.visualization.Timeline(this);
var dataTable = new google.visualization.DataTable();
@ -59,14 +63,19 @@
dataTable.addRow([entityDisplay, stateStr, start, end]);
};
// people can pass in history of 1 entityId or a collection.
// var stateHistory;
// if (_.isArray(data[0])) {
// stateHistory = data;
// } else {
// stateHistory = [data];
// isSingleDevice = true;
// }
var startTime = new Date(
stateHistory.reduce(function(minTime, stateInfo) {
return Math.min(
minTime, stateInfo[0].lastChangedAsDate);
}, new Date())
);
// end time is Math.min(curTime, start time + 1 day)
var endTime = new Date(startTime);
endTime.setDate(endTime.getDate()+1);
if (endTime > new Date()) {
endTime = new Date();
}
var numTimelines = 0;
// stateHistory is a list of lists of sorted state objects
@ -90,17 +99,13 @@
}
});
addRow(entityDisplay, prevState, prevLastChanged, new Date());
addRow(entityDisplay, prevState, prevLastChanged, endTime);
numTimelines++;
}.bind(this));
chart.draw(dataTable, {
height: 55 + numTimelines * 42,
// interactive properties require CSS, the JS api puts it on the document
// instead of inside our Shadow DOM.
enableInteractivity: false,
timeline: {
showRowLabels: stateHistory.length > 1
},

View File

@ -16,29 +16,35 @@
text-align: center;
padding: 8px;
}
.loading {
height: 0px;
overflow: hidden;
}
</style>
<template>
<google-legacy-loader on-api-load="googleApiLoaded"></google-legacy-loader>
<div hidden$="{{!isLoading}}" class='loading-container'>
<loading-box>Loading history data</loading-box>
<loading-box>Updating history data</loading-box>
</div>
<template is='dom-if' if='[[!isLoading]]'>
<template is='dom-if' if='[[groupedStateHistory.timeline]]'>
<state-history-chart-timeline data='[[groupedStateHistory.timeline]]'
is-single-device='[[isSingleDevice]]'>
</state-history-chart-timeline>
<div class$='[[computeContentClasses(isLoading)]]'>
<template is='dom-if' if='[[computeIsEmpty(stateHistory)]]'>
No state history found.
</template>
<template is='dom-if' if='[[groupedStateHistory.line]]'>
<template is='dom-repeat' items='[[groupedStateHistory.line]]'>
<state-history-chart-line unit='[[extractUnit(item)]]'
data='[[extractData(item)]]' is-single-device='[[isSingleDevice]]'>
</state-history-chart-line>
</template>
<state-history-chart-timeline
data='[[groupedStateHistory.timeline]]'
is-single-device='[[isSingleDevice]]'>
</state-history-chart-timeline>
<template is='dom-repeat' items='[[groupedStateHistory.line]]'>
<state-history-chart-line unit='[[extractUnit(item)]]'
data='[[extractData(item)]]' is-single-device='[[isSingleDevice]]'>
</state-history-chart-line>
</template>
</template>
</div>
</template>
</dom-module>
@ -69,7 +75,7 @@
groupedStateHistory: {
type: Object,
computed: 'computeGroupedStateHistory(stateHistory)',
computed: 'computeGroupedStateHistory(isLoading, stateHistory)',
},
isSingleDevice: {
@ -79,36 +85,35 @@
},
computeIsSingleDevice: function(stateHistory) {
return stateHistory && stateHistory.length == 1;
return stateHistory && stateHistory.size == 1;
},
computeGroupedStateHistory: function(stateHistory) {
computeGroupedStateHistory: function(isLoading, stateHistory) {
if (isLoading || !stateHistory) {
return {line: [], timeline: []};
}
var lineChartDevices = {};
var timelineDevices = [];
if (!stateHistory) {
return {line: unitStates, timeline: timelineDevices};
}
stateHistory.forEach(function(stateInfo) {
if (!stateInfo || stateInfo.length === 0) {
if (!stateInfo || stateInfo.size === 0) {
return;
}
var unit;
var stateWithUnit = stateInfo.find(function(state) {
return 'unit_of_measurement' in state.attributes;
});
for (var i = 0; i < stateInfo.length && !unit; i++) {
unit = stateInfo[i].attributes.unit_of_measurement;
}
var unit = stateWithUnit ?
stateWithUnit.attributes.unit_of_measurement : false;
if (unit) {
if (!(unit in lineChartDevices)) {
lineChartDevices[unit] = [stateInfo];
} else {
lineChartDevices[unit].push(stateInfo);
}
if (!unit) {
timelineDevices.push(stateInfo.toArray());
} else if(unit in lineChartDevices) {
lineChartDevices[unit].push(stateInfo.toArray());
} else {
timelineDevices.push(stateInfo);
lineChartDevices[unit] = [stateInfo.toArray()];
}
});
@ -129,10 +134,18 @@
});
},
computeContentClasses: function(isLoading) {
return isLoading ? 'loading' : '';
},
computeIsLoading: function(isLoadingData, apiLoaded) {
return isLoadingData || !apiLoaded;
},
computeIsEmpty: function(stateHistory) {
return stateHistory && stateHistory.size === 0;
},
extractUnit: function(arr) {
return arr[0];
},

View File

@ -17,42 +17,37 @@
}
</style>
<template>
<iron-icon icon="warning" hidden$="{{!hasError}}"></iron-icon>
<paper-toggle-button id="toggle" on-change='toggleChanged' hidden$="{{hasError}}"></paper-toggle-button>
<iron-icon icon="warning" hidden$="[[!hasError]]"></iron-icon>
<paper-toggle-button id="toggle" on-change='toggleChanged' checked$='[[isStreaming]]' hidden$="[[hasError]]"></paper-toggle-button>
</template>
</dom-module>
<script>
var streamGetters = window.hass.streamGetters;
var streamActions = window.hass.streamActions;
var authStore = window.hass.authStore;
Polymer({
is: 'stream-status',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
isStreaming: {
type: Boolean,
value: false,
bindNuclear: streamGetters.isStreamingEvents,
},
hasError: {
type: Boolean,
value: false,
bindNuclear: streamGetters.hasStreamingEventsError,
},
},
streamStoreChanged: function(streamStore) {
this.hasError = streamStore.hasError;
this.$.toggle.checked = this.isStreaming = streamStore.isStreaming;
},
toggleChanged: function(ev) {
toggleChanged: function() {
if (this.isStreaming) {
streamActions.stop();
} else {
streamActions.start(authStore.authToken);
streamActions.start();
}
},
});

View File

@ -31,10 +31,10 @@
</style>
<template>
<!-- entry-animation='slide-up-animation' exit-animation='slide-down-animation' -->
<paper-dialog id="dialog" with-backdrop>
<paper-dialog id="dialog" with-backdrop opened='{{dialogOpen}}'>
<h2><state-card-content state-obj="[[stateObj]]"></state-card-content></h2>
<div>
<template is='dom-if' if="[[hasHistoryComponent]]">
<template is='dom-if' if="[[showHistoryComponent]]">
<state-history-charts state-history="[[stateHistory]]"
is-loading-data="[[isLoadingHistoryData]]"></state-history-charts>
</template>
@ -49,34 +49,57 @@
<script>
(function() {
var stateStore = window.hass.stateStore;
var stateHistoryStore = window.hass.stateHistoryStore;
var stateHistoryActions = window.hass.stateHistoryActions;
var configGetters = window.hass.configGetters;
var entityHistoryGetters = window.hass.entityHistoryGetters;
var entityHistoryActions = window.hass.entityHistoryActions;
var moreInfoGetters = window.hass.moreInfoGetters;
var moreInfoActions = window.hass.moreInfoActions;
// if you don't want the history component to show add the domain to this array
var DOMAINS_WITH_NO_HISTORY = ['camera'];
Polymer({
is: 'more-info-dialog',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
entityId: {
type: String,
},
stateObj: {
type: Object,
bindNuclear: moreInfoGetters.currentEntity,
observer: 'stateObjChanged',
},
stateHistory: {
type: Object,
bindNuclear: [
moreInfoGetters.currentEntityHistory,
function(history) {
return history ? [history] : false;
},
],
},
isLoadingHistoryData: {
type: Boolean,
value: false,
bindNuclear: entityHistoryGetters.isLoadingEntityHistory,
},
hasHistoryComponent: {
type: Boolean,
bindNuclear: configGetters.isComponentLoaded('history'),
observer: 'fetchHistoryData',
},
shouldFetchHistory: {
type: Boolean,
bindNuclear: moreInfoGetters.isCurrentEntityHistoryStale,
observer: 'fetchHistoryData',
},
showHistoryComponent: {
type: Boolean,
value: false,
},
@ -84,69 +107,45 @@
dialogOpen: {
type: Boolean,
value: false,
observer: 'dialogOpenChanged',
},
},
listeners: {
'iron-overlay-opened': 'onIronOverlayOpened',
'iron-overlay-closed': 'onIronOverlayClosed'
},
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
},
stateStoreChanged: function() {
var newState = this.entityId ? stateStore.get(this.entityId) : null;
if (newState !== this.stateObj) {
this.stateObj = newState;
fetchHistoryData: function() {
if (this.stateObj && this.hasHistoryComponent &&
this.shouldFetchHistory) {
entityHistoryActions.fetchRecent(this.stateObj.entityId);
}
if(this.stateObj) {
if(DOMAINS_WITH_NO_HISTORY.indexOf(this.stateObj.domain) !== -1) {
this.showHistoryComponent = false;
}
else {
this.showHistoryComponent = this.hasHistoryComponent;
}
}
},
stateHistoryStoreChanged: function() {
var newHistory;
if (this.hasHistoryComponent && this.entityId) {
newHistory = [stateHistoryStore.get(this.entityId)];
} else {
newHistory = null;
stateObjChanged: function(newVal) {
if (!newVal) {
this.dialogOpen = false;
return;
}
this.isLoadingHistoryData = false;
this.fetchHistoryData();
if (newHistory !== this.stateHistory) {
this.stateHistory = newHistory;
// allow dialog to render content before showing it so it is
// positioned correctly.
this.async(function() {
this.dialogOpen = true;
}.bind(this), 10);
},
dialogOpenChanged: function(newVal) {
if (!newVal) {
moreInfoActions.deselectEntity();
}
},
onIronOverlayOpened: function() {
this.dialogOpen = true;
},
onIronOverlayClosed: function() {
this.dialogOpen = false;
},
changeEntityId: function(entityId) {
this.entityId = entityId;
this.stateStoreChanged();
this.stateHistoryStoreChanged();
if (this.hasHistoryComponent && stateHistoryStore.isStale(entityId)) {
this.isLoadingHistoryData = true;
stateHistoryActions.fetch(entityId);
}
},
show: function(entityId) {
this.changeEntityId(entityId);
this.debounce('showDialogAfterRender', function() {
this.$.dialog.toggle();
}.bind(this));
},
});
})();
</script>

@ -1 +1 @@
Subproject commit 015edf9c28a63122aa8f6bc153f0c0ddfaad1caa
Subproject commit 32444771075d13f2ad3aaa66c0a73e84bc0320ba

View File

@ -21,9 +21,8 @@
}
</style>
<home-assistant-icons></home-assistant-icons>
<template>
<home-assistant-icons></home-assistant-icons>
<template is='dom-if' if='[[!loaded]]'>
<login-form></login-form>
</template>
@ -37,10 +36,9 @@
<script>
(function() {
var storeListenerMixIn = window.hass.storeListenerMixIn,
uiActions = window.hass.uiActions,
preferenceStore = window.hass.preferenceStore;
var uiActions = window.hass.uiActions;
var syncGetters = window.hass.syncGetters;
var preferences = window.hass.localStoragePreferences;
Polymer({
is: 'home-assistant',
@ -49,12 +47,15 @@
auth: null,
},
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
auth: {
type: String,
},
loaded: {
type: Boolean,
value: false,
bindNuclear: syncGetters.isDataLoaded,
},
},
@ -65,13 +66,11 @@
// if auth was given, tell the backend
if(this.auth) {
uiActions.validateAuth(this.auth, false);
} else if (preferenceStore.hasAuthToken) {
uiActions.validateAuth(preferenceStore.authToken, false);
} else if (preferences.authToken) {
uiActions.validateAuth(preferences.authToken, true);
}
},
syncStoreChanged: function(syncStore) {
this.loaded = syncStore.initialLoadDone;
preferences.startSync();
},
});

View File

@ -2,13 +2,6 @@
<link rel='import' href='../bower_components/layout/layout.html'>
<link rel='import' href='../bower_components/paper-drawer-panel/paper-drawer-panel.html'>
<link rel='import' href='../bower_components/paper-header-panel/paper-header-panel.html'>
<link rel='import' href='../bower_components/paper-toolbar/paper-toolbar.html'>
<link rel='import' href='../bower_components/paper-menu/paper-menu.html'>
<link rel='import' href='../bower_components/iron-icon/iron-icon.html'>
<link rel='import' href='../bower_components/paper-item/paper-item.html'>
<link rel='import' href='../bower_components/paper-item/paper-icon-item.html'>
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
<link rel='import' href='../layouts/partial-states.html'>
<link rel='import' href='../layouts/partial-logbook.html'>
@ -18,117 +11,22 @@
<link rel='import' href='../layouts/partial-dev-set-state.html'>
<link rel='import' href='../managers/notification-manager.html'>
<link rel='import' href='../managers/modal-manager.html'>
<link rel="import" href="../dialogs/more-info-dialog.html">
<link rel='import' href='../components/stream-status.html'>
<link rel='import' href='../components/ha-sidebar.html'>
<dom-module id='home-assistant-main'>
<style>
.sidenav {
background: #fafafa;
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.sidenav paper-menu {
--paper-menu-color: var(--secondary-text-color);
--paper-menu-background-color: #fafafa;
}
paper-icon-item {
cursor: pointer;
}
paper-icon-item.logout {
margin-top: 16px;
}
.divider {
border-top: 1px solid #e0e0e0;
}
.text {
padding: 16px;
font-size: 14px;
}
.dev-tools {
padding: 0 8px;
}
</style>
<template>
<notification-manager></notification-manager>
<modal-manager></modal-manager>
<more-info-dialog></more-info-dialog>
<paper-drawer-panel id='drawer' narrow='{{narrow}}'>
<paper-header-panel mode='scroll' drawer class='sidenav fit'>
<paper-toolbar>
<!-- forces paper toolbar to style title appropriate -->
<paper-icon-button hidden></paper-icon-button>
<div class="title">Home Assistant</div>
</paper-toolbar>
<ha-sidebar drawer></ha-sidebar>
<paper-menu id='menu'
selectable='[data-panel]' attr-for-selected='data-panel'
on-iron-select='menuSelect' selected='[[selected]]'>
<paper-icon-item data-panel='states'>
<iron-icon item-icon icon='apps'></iron-icon> States
</paper-icon-item>
<template is='dom-repeat' items='{{activeFilters}}'>
<paper-icon-item data-panel$='[[filterType(item)]]'>
<iron-icon item-icon icon='[[filterIcon(item)]]'></iron-icon>
<span>[[filterName(item)]]</span>
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasHistoryComponent]]'>
<paper-icon-item data-panel='history'>
<iron-icon item-icon icon='assessment'></iron-icon>
History
</paper-icon-item>
</template>
<template is='dom-if' if='[[hasLogbookComponent]]'>
<paper-icon-item data-panel='logbook'>
<iron-icon item-icon icon='list'></iron-icon>
Logbook
</paper-icon-item>
</template>
<paper-icon-item data-panel='logout' class='logout'>
<iron-icon item-icon icon='exit-to-app'></iron-icon>
Log Out
</paper-icon-item>
<paper-item class='divider horizontal layout justified'>
<div>Streaming updates</div>
<stream-status></stream-status>
</paper-item>
<div class='text label divider'>Developer Tools</div>
<div class='dev-tools layout horizontal justified'>
<paper-icon-button
icon='settings-remote' data-panel$='[[selectedDevService]]'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-ethernet' data-panel$='[[selectedDevState]]'
on-click='handleDevClick'></paper-icon-button>
<paper-icon-button
icon='settings-input-antenna' data-panel$='[[selectedDevEvent]]'
on-click='handleDevClick'></paper-icon-button>
</div>
</paper-menu>
</paper-header-panel>
<template is='dom-if' if='[[!hideStates]]'>
<partial-states
main narrow='[[narrow]]'
filter='[[stateFilter]]'>
<template is='dom-if' if='[[isSelectedStates]]'>
<partial-states main narrow='[[narrow]]'>
</partial-states>
</template>
<template is='dom-if' if='[[isSelectedLogbook]]'>
<partial-logbook main narrow='[[narrow]]'></partial-logbook>
</template>
@ -151,192 +49,83 @@
<script>
(function() {
var configGetters = window.hass.configGetters;
var entityGetters = window.hass.entityGetters;
var navigationGetters = window.hass.navigationGetters;
var authActions = window.hass.authActions;
var navigationActions = window.hass.navigationActions;
var uiUtil = window.hass.uiUtil;
var uiConstants = window.hass.uiConstants;
var entityDomainFilters = window.hass.util.entityDomainFilters;
var urlSync = window.hass.urlSync;
Polymer({
is: 'home-assistant-main',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
selected: {
type: String,
value: 'states',
},
stateFilter: {
type: String,
value: null,
},
narrow: {
type: Boolean,
},
activeFilters: {
type: Array,
value: [],
},
hasHistoryComponent: {
type: Boolean,
value: false,
},
hasLogbookComponent: {
type: Boolean,
value: false,
},
isStreaming: {
type: Boolean,
value: false,
},
hasStreamError: {
type: Boolean,
value: false,
},
hideStates: {
type: Boolean,
value: false,
},
selectedHistory: {
activePage: {
type: String,
value: 'history',
readOnly: true,
bindNuclear: navigationGetters.activePage,
observer: 'activePageChanged',
},
isSelectedStates: {
type: Boolean,
bindNuclear: navigationGetters.isActivePane('states'),
},
isSelectedHistory: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedHistory)',
},
selectedLogbook: {
type: String,
value: 'logbook',
readOnly: true,
bindNuclear: navigationGetters.isActivePane('history'),
},
isSelectedLogbook: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedLogbook)',
},
selectedDevEvent: {
type: String,
value: 'devEvent',
readOnly: true,
bindNuclear: navigationGetters.isActivePane('logbook'),
},
isSelectedDevEvent: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedDevEvent)',
},
selectedDevState: {
type: String,
value: 'devState',
readOnly: true,
bindNuclear: navigationGetters.isActivePane('devEvent'),
},
isSelectedDevState: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedDevState)',
},
selectedDevService: {
type: String,
value: 'devService',
readOnly: true,
bindNuclear: navigationGetters.isActivePane('devState'),
},
isSelectedDevService: {
type: Boolean,
computed: 'computeIsSelected(selected, selectedDevService)',
bindNuclear: navigationGetters.isActivePane('devService'),
},
},
listeners: {
'menu.core-select': 'menuSelect',
'open-menu': 'openDrawer',
},
stateStoreChanged: function(stateStore) {
this.activeFilters = stateStore.domains.filter(function(domain) {
return domain in uiConstants.STATE_FILTERS;
}).toArray();
},
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
this.hasLogbookComponent = componentStore.isLoaded('logbook');
},
menuSelect: function(ev, detail, sender) {
this.selectPanel(this.$.menu.selected);
},
handleDevClick: function(ev, detail, sender) {
// prevent it from highlighting first menu item
document.activeElement.blur();
this.selectPanel(ev.target.parentElement.dataset.panel);
},
selectPanel: function(newChoice) {
if (newChoice == 'logout') {
this.handleLogOut();
return;
} else if(newChoice == this.selected) {
return;
}
this.closeDrawer();
this.selected = newChoice;
if (newChoice.substr(0, 7) === 'states_') {
this.hideStates = false;
this.stateFilter = newChoice.substr(7);
} else {
this.hideStates = newChoice !== 'states';
this.stateFilter = null;
}
},
openDrawer: function() {
this.$.drawer.openDrawer();
},
closeDrawer: function() {
activePageChanged: function() {
this.$.drawer.closeDrawer();
},
handleLogOut: function() {
authActions.logOut();
attached: function() {
urlSync.startSync();
},
computeIsSelected: function(selected, selectedType) {
return selected === selectedType;
detached: function() {
urlSync.stopSync();
},
filterIcon: function(filter) {
return uiUtil.domainIcon(filter);
},
filterName: function(filter) {
return uiConstants.STATE_FILTERS[filter];
},
filterType: function(filter) {
return 'states_' + filter;
}
});
})();
</script>

View File

@ -13,6 +13,10 @@
<dom-module id="login-form">
<style>
:host {
white-space: nowrap;
}
#passwordDecorator {
display: block;
height: 57px;
@ -86,53 +90,50 @@
<script>
(function() {
var uiActions = window.hass.uiActions;
var authGetters = window.hass.authGetters;
Polymer({
is: 'login-form',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
isValidating: {
type: Boolean,
value: false,
observer: 'isValidatingChanged',
bindNuclear: authGetters.isValidating,
},
isInvalid: {
type: Boolean,
value: false,
bindNuclear: authGetters.isInvalidAttempt,
},
errorMessage: {
type: String,
value: '',
}
bindNuclear: authGetters.attemptErrorMessage,
},
},
listeners: {
'passwordInput.keydown': 'passwordKeyDown',
'keydown': 'passwordKeyDown',
'loginButton.click': 'validatePassword',
},
attached: function() {
this.focusPassword();
},
observers: [
'validatingChanged(isValidating, isInvalid)',
],
authStoreChanged: function(authStore) {
this.isValidating = authStore.isValidating;
if (authStore.lastAttemptInvalid) {
this.errorMessage = authStore.lastAttemptMessage;
this.isInvalid = true;
}
if (!this.isValidating) {
setTimeout(this.focusPassword.bind(this), 0);
validatingChanged: function(isValidating, isInvalid) {
if (!isValidating && !isInvalid) {
this.$.passwordInput.value = '';
}
},
focusPassword: function() {
this.$.passwordInput.focus();
isValidatingChanged: function(newVal) {
if (!newVal) {
this.async(function() { this.$.passwordInput.focus(); }.bind(this), 10);
}
},
passwordKeyDown: function(ev) {

View File

@ -6,6 +6,13 @@
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
<dom-module id='partial-base'>
<style>
:host {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
</style>
<template>
<paper-scroll-header-panel class='fit'>
<paper-toolbar>

View File

@ -80,7 +80,7 @@
return;
}
eventActions.fire(this.eventType, eventData);
eventActions.fireEvent(this.eventType, eventData);
},
computeFormClasses: function(narrow) {

View File

@ -50,8 +50,9 @@
<script>
(function() {
var stateStore = window.hass.stateStore;
var stateActions = window.hass.stateActions;
var reactor = window.hass.reactor;
var entityGetters = window.hass.entityGetters;
var entityActions = window.hass.entityActions;
Polymer({
is: 'partial-dev-set-state',
@ -83,7 +84,7 @@
},
entitySelected: function(ev) {
var state = stateStore.get(ev.detail.entityId);
var state = reactor.evaluate(entityGetters.byId(ev.detail.entityId));
this.entityId = state.entityId;
this.state = state.state;
@ -99,7 +100,11 @@
return;
}
stateActions.set(this.entityId, this.state, attr);
entityActions.save({
entityId: this.entityId,
state: this.state,
attributes: attr,
});
},
computeFormClasses: function(narrow) {

View File

@ -1,10 +1,13 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="./partial-base.html">
<link rel="import" href="../components/state-history-charts.html">
<link rel="import" href="../resources/pikaday-js.html">
<dom-module id="partial-history">
<style>
.content {
@ -14,6 +17,14 @@
.content.wide {
padding: 8px;
}
paper-input {
max-width: 200px;
}
.narrow paper-input {
margin-left: 8px;
}
</style>
<template>
<partial-base narrow="[[narrow]]">
@ -23,6 +34,9 @@
on-click="handleRefreshClick"></paper-icon-button>
<div class$="[[computeContentClasses(narrow)]]">
<paper-input label='Showing entries for' id='datePicker'
value='[[selectedDate]]'></paper-input>
<state-history-charts state-history="[[stateHistory]]"
is-loading-data="[[isLoadingData]]"></state-history-charts>
</div>
@ -31,43 +45,67 @@
</dom-module>
<script>
(function() {
var stateHistoryActions = window.hass.stateHistoryActions;
var entityHistoryGetters = window.hass.entityHistoryGetters;
var entityHistoryActions = window.hass.entityHistoryActions;
var uiActions = window.hass.uiActions;
Polymer({
is: 'partial-history',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
narrow: {
type: Boolean,
},
isDataLoaded: {
type: Boolean,
bindNuclear: entityHistoryGetters.hasDataForCurrentDate,
observer: 'isDataLoadedChanged',
},
stateHistory: {
type: Object,
bindNuclear: entityHistoryGetters.entityHistoryForCurrentDate,
},
isLoadingData: {
type: Boolean,
value: false,
bindNuclear: entityHistoryGetters.isLoadingEntityHistory,
},
selectedDate: {
type: String,
value: null,
bindNuclear: entityHistoryGetters.currentDate,
},
},
stateHistoryStoreChanged: function(stateHistoryStore) {
if (stateHistoryStore.isStale()) {
this.isLoadingData = true;
stateHistoryActions.fetchAll();
isDataLoadedChanged: function(newVal) {
if (!newVal) {
entityHistoryActions.fetchSelectedDate();
}
else {
this.isLoadingData = false;
}
this.stateHistory = stateHistoryStore.all;
},
handleRefreshClick: function() {
this.isLoadingData = true;
stateHistoryActions.fetchAll();
entityHistoryActions.fetchSelectedDate();
},
datepickerFocus: function() {
this.datePicker.adjustPosition();
this.datePicker.gotoDate(moment('2015-06-30').toDate());
},
attached: function() {
this.datePicker = new Pikaday({
field: this.$.datePicker.inputElement,
onSelect: entityHistoryActions.changeCurrentDate,
});
},
detached: function() {
this.datePicker.destroy();
},
computeContentClasses: function(narrow) {

View File

@ -1,17 +1,24 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="./partial-base.html">
<link rel="import" href="../components/ha-logbook.html">
<link rel="import" href="../components/loading-box.html">
<link rel="import" href="../resources/pikaday-js.html">
<dom-module id="partial-logbook">
<style>
.content {
background-color: white;
padding: 8px;
}
.selected-date-container {
padding: 0 16px;
}
paper-input {
max-width: 200px;
}
</style>
<template>
<partial-base narrow="[[narrow]]">
@ -20,20 +27,30 @@
<paper-icon-button icon="refresh" header-buttons
on-click="handleRefresh"></paper-icon-button>
<ha-logbook entries="[[entries]]"></ha-logbook>
<div>
<div class='selected-date-container'>
<paper-input label='Showing entries for' id='datePicker'
value='[[selectedDate]]' on-focus='datepickerFocus'></paper-input>
<loading-box hidden$='[[!isLoading]]'>Loading logbook entries</loading-box>
</div>
<ha-logbook entries="[[entries]]" hidden$='[[isLoading]]'></ha-logbook>
</div>
</partial-base>
</template>
</dom-module>
<script>
(function() {
var storeListenerMixIn = window.hass.storeListenerMixIn;
var logbookGetters = window.hass.logbookGetters;
var logbookActions = window.hass.logbookActions;
var uiActions = window.hass.uiActions;
var dateToStr = window.hass.util.dateToStr;
Polymer({
is: 'partial-logbook',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
narrow: {
@ -41,22 +58,61 @@
value: false,
},
selectedDate: {
type: String,
bindNuclear: logbookGetters.currentDate,
},
isLoading: {
type: Boolean,
bindNuclear: logbookGetters.isLoadingEntries,
},
isStale: {
type: Boolean,
bindNuclear: logbookGetters.isCurrentStale,
observer: 'isStaleChanged',
},
entries: {
type: Array,
value: [],
bindNuclear: [
logbookGetters.currentEntries,
function(entries) { return entries.toArray(); },
],
},
datePicker: {
type: Object,
},
},
logbookStoreChanged: function(logbookStore) {
if (logbookStore.isStale()) {
logbookActions.fetch();
isStaleChanged: function(newVal) {
if (newVal) {
// isLoading wouldn't update without async <_<
this.async(
function() { logbookActions.fetchDate(this.selectedDate); }, 10);
}
this.entries = logbookStore.all.toArray();
},
handleRefresh: function() {
logbookActions.fetch();
logbookActions.fetchDate(this.selectedDate);
},
datepickerFocus: function() {
this.datePicker.adjustPosition();
this.datePicker.gotoDate(moment('2015-06-30').toDate());
},
attached: function() {
this.datePicker = new Pikaday({
field: this.$.datePicker.inputElement,
onSelect: logbookActions.changeCurrentDate,
});
},
detached: function() {
this.datePicker.destroy();
},
});
})();

View File

@ -1,11 +1,11 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="./partial-base.html">
<link rel="import" href="../components/state-cards.html">
<link rel="import" href="../components/ha-voice-command-progress.html">
<dom-module id="partial-states">
<style>
@ -41,21 +41,24 @@
<template>
<partial-base narrow="[[narrow]]">
<span header-title>{{headerTitle}}</span>
<span header-title>[[computeHeaderTitle(filter)]]</span>
<span header-buttons>
<paper-icon-button icon="refresh" class$="[[computeRefreshButtonClass(isFetching)]]"
on-click="handleRefresh" hidden$="[[isStreaming]]"></paper-icon-button>
<paper-icon-button icon="[[listenButtonIcon]]" hidden$={{!canListen}}
<paper-icon-button
icon="refresh"
class$="[[computeRefreshButtonClass(isFetching)]]"
on-click="handleRefresh" hidden$="[[isStreaming]]"
></paper-icon-button>
<paper-icon-button
icon="[[computeListenButtonIcon(isListening)]]"
hidden$='[[!canListen]]'
on-click="handleListenClick"></paper-icon-button>
</span>
<div class='content-wrapper'>
<div class='listening' hidden$="[[!showListenInterface]]"
on-click="handleListenClick">
<iron-icon icon="av:hearing"></iron-icon> <span>{{finalTranscript}}</span>
<span class='interimTranscript'>[[interimTranscript]]</span>
<paper-spinner active$="[[isTransmitting]]" alt="Sending voice command to Home Assistant"></paper-spinner>
<ha-voice-command-progress></ha-voice-command-progress>
</div>
<state-cards states="[[states]]">
@ -75,28 +78,24 @@
<script>
(function(){
var configGetters = window.hass.configGetters;
var navigationGetters = window.hass.navigationGetters;
var voiceGetters = window.hass.voiceGetters;
var streamGetters = window.hass.streamGetters;
var serviceGetters = window.hass.serviceGetters;
var syncGetters = window.hass.syncGetters;
var syncActions = window.hass.syncActions;
var voiceActions = window.hass.voiceActions;
var stateStore = window.hass.stateStore;
var uiConstants = window.hass.uiConstants;
var entityDomainFilters = window.hass.util.entityDomainFilters;
Polymer({
is: 'partial-states',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
/**
* Title to show in the header
*/
headerTitle: {
type: String,
value: 'States',
},
/**
* If header is to be shown in narrow mode.
*/
narrow: {
type: Boolean,
value: false,
@ -104,110 +103,56 @@
filter: {
type: String,
value: null,
observer: 'filterChanged',
},
voiceSupported: {
type: Boolean,
value: voiceActions.isSupported(),
bindNuclear: navigationGetters.activeFilter,
},
isFetching: {
type: Boolean,
value: false,
bindNuclear: syncGetters.isFetching,
},
isStreaming: {
type: Boolean,
value: false,
bindNuclear: streamGetters.isStreamingEvents,
},
canListen: {
type: Boolean,
value: false,
bindNuclear: [
voiceGetters.isVoiceSupported,
configGetters.isComponentLoaded('conversation'),
function(isVoiceSupported, componentLoaded) {
return isVoiceSupported && componentLoaded;
}
]
},
isListening: {
type: Boolean,
value: false,
},
isTransmitting: {
type: Boolean,
value: false,
},
interimTranscript: {
type: String,
value: '',
},
finalTranscript: {
type: String,
value: '',
},
listenButtonIcon: {
type: String,
computed: 'computeListenButtonIcon(isListening)'
bindNuclear: voiceGetters.isListening,
},
showListenInterface: {
type: Boolean,
computed: 'computeShowListenInterface(isListening,isTransmitting)'
}
},
bindNuclear: [
voiceGetters.isListening,
voiceGetters.isTransmitting,
function(isListening, isTransmitting) {
return isListening || isTransmitting;
},
],
},
componentStoreChanged: function(componentStore) {
this.canListen = this.voiceSupported &&
componentStore.isLoaded('conversation');
},
stateStoreChanged: function() {
this.refreshStates();
},
syncStoreChanged: function(syncStore) {
this.isFetching = syncStore.isFetching;
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming;
},
voiceStoreChanged: function(voiceStore) {
this.isListening = voiceStore.isListening;
this.isTransmitting = voiceStore.isTransmitting;
this.finalTranscript = voiceStore.finalTranscript;
this.interimTranscript = voiceStore.interimTranscript.slice(
this.finalTranscript.length);
},
filterChanged: function() {
this.refreshStates();
this.headerTitle = uiConstants.STATE_FILTERS[this.filter] || 'States';
},
refreshStates: function() {
var states;
if (this.filter) {
var filter = this.filter;
states = stateStore.all.filter(function(state) {
return state.domain === filter;
});
} else {
// all but the STATE_FILTER keys
states = stateStore.all.filter(function(state) {
return !(state.domain in uiConstants.STATE_FILTERS);
});
}
this.states = states.toArray().filter(
function (el) {return !el.attributes.hidden;});
states: {
type: Array,
bindNuclear: [
navigationGetters.filteredStates,
// are here so a change to services causes a re-render.
// we need this to decide if we show toggles for states.
serviceGetters.entityMap,
function(states) { return states.toArray(); },
],
},
},
handleRefresh: function() {
@ -222,12 +167,12 @@
}
},
computeListenButtonIcon: function(isListening) {
return isListening ? 'av:mic-off' : 'av:mic';
computeHeaderTitle: function(filter) {
return filter ? entityDomainFilters[filter] : 'States';
},
computeShowListenInterface: function(isListening,isTransmitting) {
return isListening || isTransmitting;
computeListenButtonIcon: function(isListening) {
return isListening ? 'av:mic-off' : 'av:mic';
},
computeRefreshButtonClass: function(isFetching) {

View File

@ -1,30 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../dialogs/more-info-dialog.html">
<dom-module id="modal-manager">
<template>
<more-info-dialog id="moreInfoDialog"></more-info-dialog>
</template>
</dom-module>
<script>
(function() {
var uiConstants = window.hass.uiConstants,
dispatcher = window.hass.dispatcher;
Polymer({
is: 'modal-manager',
ready: function() {
dispatcher.register(function(payload) {
switch (payload.actionType) {
case uiConstants.ACTION_SHOW_DIALOG_MORE_INFO:
this.$.moreInfoDialog.show(payload.entityId);
break;
}
}.bind(this));
},
});
})();
</script>

View File

@ -15,33 +15,26 @@
<script>
(function() {
var notificationGetters = window.hass.notificationGetters;
Polymer({
is: 'notification-manager',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
text: {
type: String,
value: '',
},
lastId: {
type: Number,
bindNuclear: notificationGetters.lastNotificationMessage,
observer: 'showNotification',
},
},
notificationStoreChanged: function(notificationStore) {
if (notificationStore.hasNewNotifications(this.lastId)) {
var notification = notificationStore.lastNotification;
this.lastId = notification.id;
this.text = notification.message;
showNotification: function(newText) {
if (newText) {
this.$.toast.show();
}
},
}
});
})();
</script>

View File

@ -0,0 +1,40 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<script>
(function() {
var authGetters = window.hass.authGetters;
var streamGetters = window.hass.streamGetters;
Polymer({
is: 'preferences-manager',
behaviors: [nuclearObserver],
properties: {
authToken: {
type: String,
bindNuclear: authGetters.currentAuthToken,
observer: 'updateStorage',
},
useStreaming: {
type: String,
bindNuclear: ,
observer: 'updateStorage',
},
},
updateStorage: function() {
if (!('localStorage' in window)) {
return;
}
var storage = localStorage;
Object.keys(this.properties).forEach(function(prop) {
storage[prop] = this.prop;
});
},
});
})();
</script>

View File

@ -0,0 +1,80 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-button/paper-button.html">
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
<dom-module id='more-info-camera'>
<style>
:host .camera-image {
width:640px;
height:480px;
}
@media (max-width: 720px) {
:host .camera-image {
max-width: 100%;
height: initial
}
:host .camera-page {
max-width: 100%;
max-height: 100%;
}
}
@media (max-width: 620px) {
:host .camera-image {
max-width: 100%;
height: initial
}
}
:host .camera-page {
width:640px;
height:520px;
}
</style>
<template>
<div class='camera'>
<div id="camera_container" class="camera-container camera-page">
<img src="[[computeCameraImageUrl(dialogOpen)]]" id="camera_image" class="camera-image" />
</div>
</div>
</template>
</dom-module>
<script>
(function() {
var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['camera'];
Polymer({
is: 'more-info-camera',
properties: {
stateObj: {
type: Object
},
dialogOpen: {
type: Boolean,
},
camera_image_url: {
type: String,
}
},
computeCameraImageUrl: function(dialogOpen) {
return dialogOpen ?
this.stateObj.attributes['stream_url'] :
this.stateObj.attributes['still_image_url'];
},
});
})();
</script>
</polymer-element>

View File

@ -52,13 +52,14 @@
<script>
(function() {
var streamGetters = window.hass.streamGetters;
var syncActions = window.hass.syncActions;
var serviceActions = window.hass.serviceActions;
Polymer({
is: 'more-info-configurator',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
stateObj: {
@ -72,7 +73,7 @@
isStreaming: {
type: Boolean,
value: false,
bindNuclear: streamGetters.isStreamingEvents,
},
isConfigurable: {
@ -99,10 +100,6 @@
return stateObj.attributes.submit_caption || 'Set configuration';
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming;
},
submitClicked: function() {
this.isConfiguring = true;

View File

@ -8,6 +8,7 @@
<link rel='import' href='more-info-script.html'>
<link rel='import' href='more-info-light.html'>
<link rel='import' href='more-info-media_player.html'>
<link rel='import' href='more-info-camera.html'>
<dom-module id='more-info-content'>
<style>
@ -33,6 +34,7 @@
dialogOpen: {
type: Boolean,
value: false,
observer: 'dialogOpenChanged',
},
},

View File

@ -23,28 +23,36 @@
<script>
(function() {
var stateStore = window.hass.stateStore;
var entityGetters = window.hass.entityGetters;
var moreInfoGetters = window.hass.moreInfoGetters;
Polymer({
is: 'more-info-group',
behaviors: [StoreListenerBehavior],
behaviors: [nuclearObserver],
properties: {
stateObj: {
type: Object,
observer: 'updateStates',
},
states: {
type: Array,
value: [],
bindNuclear: [
moreInfoGetters.currentEntity,
entityGetters.entityMap,
function(currentEntity, entities) {
// weird bug??
if (!currentEntity) {
return;
}
return currentEntity.attributes.entity_id.map(
entities.get.bind(entities));
},
],
},
},
stateStoreChanged: function() {
this.updateStates();
},
updateStates: function() {
this.states = this.stateObj && this.stateObj.attributes.entity_id ?

View File

@ -1,7 +1,7 @@
<link rel='import' href='../bower_components/polymer/polymer.html'>
<link rel='import' href='../bower_components/paper-slider/paper-slider.html'>
<link rel='import' href='../bower_components/color-picker-element/dist/color-picker.html'>
<link rel='import' href='../components/ha-color-picker.html'>
<dom-module id='more-info-light'>
<style>
@ -13,7 +13,7 @@
transition: max-height .5s ease-in;
}
color-picker {
ha-color-picker {
display: block;
width: 350px;
margin: 0 auto;
@ -27,7 +27,7 @@
max-height: 40px;
}
.has-xy_color color-picker {
.has-xy_color ha-color-picker {
max-height: 500px;
}
</style>
@ -41,7 +41,7 @@
</paper-slider>
</div>
<color-picker on-colorselected='colorPicked' width='350' height='200'>
<ha-color-picker on-colorselected='colorPicked' width='350' height='200'>
</color-picker>
</div>
</template>
@ -73,7 +73,7 @@
this.brightnessSliderValue = newVal.attributes.brightness;
}
this.debounce('more-info-light-animation-finish', function() {
this.async(function() {
this.fire('iron-resize');
}.bind(this), 500);
},

View File

@ -127,7 +127,7 @@
},
stateObjChanged: function(newVal, oldVal) {
stateObjChanged: function(newVal) {
if (newVal) {
this.isOff = newVal.state == 'off';
this.isPlaying = newVal.state == 'playing';
@ -142,9 +142,7 @@
this.supportsTurnOff = (newVal.attributes.supported_media_commands & 256) !== 0;
}
this.debounce('more-info-volume-animation-finish', function() {
this.fire('iron-resize');
}.bind(this), 500);
this.async(function() { this.fire('iron-resize'); }.bind(this), 500);
},
computeClassNames: function(stateObj) {

View File

@ -41,7 +41,7 @@
<script>
(function() {
var constants = window.hass.constants;
var temperatureUnits = window.hass.util.temperatureUnits;
var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['away_mode'];
@ -76,7 +76,8 @@
this.targetTemperatureSliderValue = this.stateObj.state;
this.awayToggleChecked = this.stateObj.attributes.away_mode == 'on';
if (this.stateObj.attributes.unit_of_measurement === constants.UNIT_TEMP_F) {
if (this.stateObj.attributes.unit_of_measurement ===
temperatureUnits.UNIT_TEMP_F) {
this.tempMin = 45;
this.tempMax = 95;
} else {

View File

@ -4,58 +4,22 @@
(function() {
var DOMAINS_WITH_CARD = ['thermostat', 'configurator', 'scene', 'media_player'];
var DOMAINS_WITH_MORE_INFO = [
'light', 'group', 'sun', 'configurator', 'thermostat', 'script', 'media_player'
'light', 'group', 'sun', 'configurator', 'thermostat', 'script', 'media_player', 'camera'
];
var DOMAINS_HIDE_MORE_INFO = [
'sensor',
];
// Add some frontend specific helpers to the models
Object.defineProperties(window.hass.stateModel.prototype, {
// how to render the card for this state
cardType: {
get: function() {
console.warn('Deprecated method. Please use hass.uiUtil.stateCardType');
return window.hass.uiUtil.stateCardType(this);
}
},
// how to render the more info of this state
moreInfoType: {
get: function() {
console.warn('Deprecated method. Please use hass.uiUtil.stateMoreInfoType');
return window.hass.uiUtil.stateMoreInfoType(this);
}
},
});
var dispatcher = window.hass.dispatcher,
constants = window.hass.constants,
preferenceStore = window.hass.preferenceStore,
authActions = window.hass.authActions;
window.hass.uiConstants = {
ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO',
STATE_FILTERS: {
'group': 'Groups',
'script': 'Scripts',
'scene': 'Scenes',
},
};
var reactor = window.hass.reactor;
var serviceGetters = window.hass.serviceGetters;
var authActions = window.hass.authActions;
var preferences = window.hass.localStoragePreferences;
window.hass.uiActions = {
showMoreInfoDialog: function(entityId) {
dispatcher.dispatch({
actionType: window.hass.uiConstants.ACTION_SHOW_DIALOG_MORE_INFO,
entityId: entityId,
});
},
validateAuth: function(authToken, rememberLogin) {
validateAuth: function(authToken, rememberAuth) {
authActions.validate(authToken, {
useStreaming: preferenceStore.useStreaming,
rememberLogin: rememberLogin,
rememberAuth: rememberAuth,
useStreaming: preferences.useStreaming,
});
},
};
@ -65,7 +29,7 @@
stateCardType: function(state) {
if(DOMAINS_WITH_CARD.indexOf(state.domain) !== -1) {
return state.domain;
} else if(state.canToggle) {
} else if(reactor.evaluate(serviceGetters.canToggle(state.entityId))) {
return "toggle";
} else {
return "display";

View File

@ -0,0 +1,5 @@
<!--
Wrapping JS in an HTML file will prevent it from being loaded twice.
-->
<script src="../bower_components/lodash/lodash.min.js"></script>

View File

@ -2,7 +2,7 @@
Wrapping JS in an HTML file will prevent it from being loaded twice.
-->
<script src="../bower_components/moment/moment.js"></script>
<script src="../bower_components/moment/min/moment.min.js"></script>
<script>
window.hass.uiUtil.formatTime = function(dateObj) {

View File

@ -0,0 +1,2 @@
<script src="../bower_components/pikaday/pikaday.js"></script>
<link href="../bower_components/pikaday/css/pikaday.css" media="all" rel="stylesheet" />

View File

@ -1,21 +1,42 @@
<script>
(function() {
var NuclearObserver = function NuclearObserver(reactor) {
return {
var StoreListenerMixIn = window.hass.storeListenerMixIn;
attached: function() {
var component = this;
this.__unwatchFns = Object.keys(component.properties).reduce(
function(unwatchFns, key) {
if (!('bindNuclear' in component.properties[key])) {
return unwatchFns;
}
var getter = component.properties[key].bindNuclear;
window.StoreListenerBehavior = {
if (!getter) {
throw 'Undefined getter specified for key ' + key;
}
attached: function() {
StoreListenerMixIn.listenToStores(true, this);
},
// console.log(key, getter);
detached: function() {
StoreListenerMixIn.stopListeningToStores(this);
},
component[key] = reactor.evaluate(getter);
return unwatchFns.concat(reactor.observe(getter, function(val) {
// console.log('New value for', key, val);
component[key] = val;
}));
}, []);
},
detached: function() {
while (this.__unwatchFns.length) {
this.__unwatchFns.shift()();
}
},
};
};
window.nuclearObserver = NuclearObserver(window.hass.reactor);
})();
</script>

File diff suppressed because one or more lines are too long

View File

@ -9,12 +9,16 @@ from datetime import timedelta
from itertools import groupby
from collections import defaultdict
import homeassistant.util.dt as date_util
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
from homeassistant.const import HTTP_BAD_REQUEST
DOMAIN = 'history'
DEPENDENCIES = ['recorder', 'http']
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. """
@ -111,8 +115,7 @@ def setup(hass, config):
r'recent_states'),
_api_last_5_states)
hass.http.register_path(
'GET', re.compile(r'/api/history/period'), _api_history_period)
hass.http.register_path('GET', URL_HISTORY_PERIOD, _api_history_period)
return True
@ -128,10 +131,25 @@ def _api_last_5_states(handler, path_match, data):
def _api_history_period(handler, path_match, data):
""" Return history over a period of time. """
# 1 day for now..
start_time = date_util.utcnow() - timedelta(seconds=86400)
date_str = path_match.group('date')
one_day = timedelta(seconds=86400)
if date_str:
start_date = dt_util.date_str_to_date(date_str)
if start_date is None:
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
return
start_time = dt_util.as_utc(dt_util.start_of_local_day(start_date))
else:
start_time = dt_util.utcnow() - one_day
end_time = start_time + one_day
print("Fetchign", start_time, end_time)
entity_id = data.get('filter_entity_id')
handler.write_json(
state_changes_during_period(start_time, entity_id=entity_id).values())
state_changes_during_period(start_time, end_time, entity_id).values())

View File

@ -22,12 +22,14 @@ from homeassistant.const import (
# homeassistant constants
DOMAIN = "isy994"
DEPENDENCIES = []
REQUIREMENTS = ['PyISY>=1.0.5']
DISCOVER_LIGHTS = "isy994.lights"
DISCOVER_SWITCHES = "isy994.switches"
DISCOVER_SENSORS = "isy994.sensors"
ISY = None
SENSOR_STRING = 'Sensor'
HIDDEN_STRING = '{HIDE ME}'
CONF_TLS_VER = 'tls'
# setup logger
_LOGGER = logging.getLogger(__name__)
@ -42,7 +44,6 @@ def setup(hass, config):
import PyISY
except ImportError:
_LOGGER.error("Error while importing dependency PyISY.")
return False
# pylint: disable=global-statement
@ -74,10 +75,12 @@ def setup(hass, config):
global HIDDEN_STRING
SENSOR_STRING = str(config[DOMAIN].get('sensor_string', SENSOR_STRING))
HIDDEN_STRING = str(config[DOMAIN].get('hidden_string', HIDDEN_STRING))
tls_version = config[DOMAIN].get(CONF_TLS_VER, None)
# connect to ISY controller
global ISY
ISY = PyISY.ISY(addr, port, user, password, use_https=https, log=_LOGGER)
ISY = PyISY.ISY(addr, port, user, password, use_https=https,
tls_ver=tls_version, log=_LOGGER)
if not ISY.connected:
return False

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
DOMAIN = "keyboard"
DEPENDENCIES = []
REQUIREMENTS = ['pyuserinput>=0.1.9']
def volume_up(hass):

View File

@ -53,8 +53,10 @@ import os
import csv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
import homeassistant.util as util
import homeassistant.util.color as color_util
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.components import group, discovery, wink, isy994
@ -87,6 +89,10 @@ ATTR_FLASH = "flash"
FLASH_SHORT = "short"
FLASH_LONG = "long"
# Apply an effect to the light, can be EFFECT_COLORLOOP
ATTR_EFFECT = "effect"
EFFECT_COLORLOOP = "colorloop"
LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms
@ -96,6 +102,11 @@ DISCOVERY_PLATFORMS = {
discovery.services.PHILIPS_HUE: 'hue',
}
PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS,
'color_xy': ATTR_XY_COLOR,
}
_LOGGER = logging.getLogger(__name__)
@ -108,7 +119,8 @@ def is_on(hass, entity_id=None):
# pylint: disable=too-many-arguments
def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, profile=None, flash=None):
rgb_color=None, xy_color=None, profile=None, flash=None,
effect=None):
""" Turns all or specified light on. """
data = {
key: value for key, value in [
@ -119,6 +131,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color),
(ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
] if value is not None
}
@ -231,9 +244,9 @@ def setup(hass, config):
if len(rgb_color) == 3:
params[ATTR_XY_COLOR] = \
util.color_RGB_to_xy(int(rgb_color[0]),
int(rgb_color[1]),
int(rgb_color[2]))
color_util.color_RGB_to_xy(int(rgb_color[0]),
int(rgb_color[1]),
int(rgb_color[2]))
except (TypeError, ValueError):
# TypeError if rgb_color is not iterable
@ -247,11 +260,16 @@ def setup(hass, config):
elif dat[ATTR_FLASH] == FLASH_LONG:
params[ATTR_FLASH] = FLASH_LONG
if ATTR_EFFECT in dat:
if dat[ATTR_EFFECT] == EFFECT_COLORLOOP:
params[ATTR_EFFECT] = EFFECT_COLORLOOP
for light in target_lights:
light.turn_on(**params)
for light in target_lights:
light.update_ha_state(True)
if light.should_poll:
light.update_ha_state(True)
# Listen for light on and light off service calls
hass.services.register(DOMAIN, SERVICE_TURN_ON,
@ -261,3 +279,41 @@ def setup(hass, config):
handle_light_service)
return True
class Light(ToggleEntity):
""" Represents a light within Home Assistant. """
# pylint: disable=no-self-use
@property
def brightness(self):
""" Brightness of this light between 0..255. """
return None
@property
def color_xy(self):
""" XY color value [float, float]. """
return None
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
data = {}
if self.is_on:
for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop)
if value:
data[attr] = value
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data

View File

@ -7,9 +7,8 @@ Demo platform that implements lights.
"""
import random
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_XY_COLOR
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR)
LIGHT_COLORS = [
@ -22,16 +21,16 @@ LIGHT_COLORS = [
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo lights. """
add_devices_callback([
DemoLight("Bed Light", STATE_OFF),
DemoLight("Ceiling", STATE_ON),
DemoLight("Kitchen", STATE_ON)
DemoLight("Bed Light", False),
DemoLight("Ceiling", True),
DemoLight("Kitchen", True)
])
class DemoLight(ToggleEntity):
class DemoLight(Light):
""" Provides a demo switch. """
def __init__(self, name, state, xy=None, brightness=180):
self._name = name or DEVICE_DEFAULT_NAME
self._name = name
self._state = state
self._xy = xy or random.choice(LIGHT_COLORS)
self._brightness = brightness
@ -47,27 +46,23 @@ class DemoLight(ToggleEntity):
return self._name
@property
def state(self):
""" Returns the name of the device if any. """
return self._state
def brightness(self):
""" Brightness of this light between 0..255. """
return self._brightness
@property
def state_attributes(self):
""" Returns optional state attributes. """
if self.is_on:
return {
ATTR_BRIGHTNESS: self._brightness,
ATTR_XY_COLOR: self._xy,
}
def color_xy(self):
""" XY color value. """
return self._xy
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
self._state = STATE_ON
self._state = True
if ATTR_XY_COLOR in kwargs:
self._xy = kwargs[ATTR_XY_COLOR]
@ -75,6 +70,9 @@ class DemoLight(ToggleEntity):
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = STATE_OFF
self._state = False
self.update_ha_state()

View File

@ -6,12 +6,13 @@ from urllib.parse import urlparse
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
ATTR_FLASH, FLASH_LONG, FLASH_SHORT)
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_EFFECT,
EFFECT_COLORLOOP)
REQUIREMENTS = ['phue>=0.8']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
@ -131,7 +132,7 @@ def request_configuration(host, hass, add_devices_callback):
)
class HueLight(ToggleEntity):
class HueLight(Light):
""" Represents a Hue light """
def __init__(self, light_id, info, bridge, update_lights):
@ -149,19 +150,17 @@ class HueLight(ToggleEntity):
@property
def name(self):
""" Get the mame of the Hue light. """
return self.info.get('name', 'No name')
return self.info.get('name', DEVICE_DEFAULT_NAME)
@property
def state_attributes(self):
""" Returns optional state attributes. """
attr = {}
def brightness(self):
""" Brightness of this light between 0..255. """
return self.info['state']['bri']
if self.is_on:
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
if 'xy' in self.info['state']:
attr[ATTR_XY_COLOR] = self.info['state']['xy']
return attr
@property
def color_xy(self):
""" XY color value. """
return self.info['state'].get('xy')
@property
def is_on(self):
@ -194,6 +193,13 @@ class HueLight(ToggleEntity):
else:
command['alert'] = 'none'
effect = kwargs.get(ATTR_EFFECT)
if effect == EFFECT_COLORLOOP:
command['effect'] = 'colorloop'
else:
command['effect'] = 'none'
self.bridge.set_light(self.light_id, command)
def turn_off(self, **kwargs):

View File

@ -23,38 +23,28 @@ light:
"""
import logging
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import DEVICE_DEFAULT_NAME
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['ledcontroller>=1.0.7']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the LimitlessLED lights. """
try:
import ledcontroller
except ImportError:
_LOGGER.exception("Error while importing dependency ledcontroller.")
return
import ledcontroller
led = ledcontroller.LedController(config['host'])
lights = []
for i in range(1, 5):
if 'group_%d_name' % (i) in config:
lights.append(
LimitlessLED(
led,
i,
config['group_%d_name' % (i)]
)
)
lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
add_devices_callback(lights)
class LimitlessLED(ToggleEntity):
class LimitlessLED(Light):
""" Represents a LimitlessLED light """
def __init__(self, led, group, name):
@ -65,7 +55,7 @@ class LimitlessLED(ToggleEntity):
self.led.off(self.group)
self._name = name or DEVICE_DEFAULT_NAME
self._state = STATE_OFF
self._state = False
self._brightness = 100
@property
@ -79,33 +69,26 @@ class LimitlessLED(ToggleEntity):
return self._name
@property
def state(self):
""" Returns the name of the device if any. """
return self._state
@property
def state_attributes(self):
""" Returns optional state attributes. """
if self.is_on:
return {
ATTR_BRIGHTNESS: self._brightness,
}
def brightness(self):
return self._brightness
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
self._state = STATE_ON
self._state = True
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
self.led.set_brightness(self._brightness, self.group)
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = STATE_OFF
self._state = False
self.led.off(self.group)
self.update_ha_state()

View File

@ -1,9 +1,8 @@
""" Support for Tellstick lights. """
import logging
# pylint: disable=no-name-in-module, import-error
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
@ -27,7 +26,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(lights)
class TellstickLight(ToggleEntity):
class TellstickLight(Light):
""" Represents a tellstick light """
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF |
@ -38,7 +37,7 @@ class TellstickLight(ToggleEntity):
def __init__(self, tellstick):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
self.brightness = 0
self._brightness = 0
@property
def name(self):
@ -48,34 +47,28 @@ class TellstickLight(ToggleEntity):
@property
def is_on(self):
""" True if switch is on. """
return self.brightness > 0
return self._brightness > 0
@property
def brightness(self):
""" Brightness of this light between 0..255. """
return self._brightness
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()
self.brightness = 0
self._brightness = 0
def turn_on(self, **kwargs):
""" Turns the switch on. """
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is None:
self.brightness = 255
self._brightness = 255
else:
self.brightness = brightness
self._brightness = brightness
self.tellstick.dim(self.brightness)
@property
def state_attributes(self):
""" Returns optional state attributes. """
attr = {
ATTR_FRIENDLY_NAME: self.name
}
attr[ATTR_BRIGHTNESS] = int(self.brightness)
return attr
self.tellstick.dim(self._brightness)
def update(self):
""" Update state of the light. """
@ -83,12 +76,12 @@ class TellstickLight(ToggleEntity):
self.last_sent_command_mask)
if last_command == tellcore_constants.TELLSTICK_TURNON:
self.brightness = 255
self._brightness = 255
elif last_command == tellcore_constants.TELLSTICK_TURNOFF:
self.brightness = 0
self._brightness = 0
elif (last_command == tellcore_constants.TELLSTICK_DIM or
last_command == tellcore_constants.TELLSTICK_UP or
last_command == tellcore_constants.TELLSTICK_DOWN):
last_sent_value = self.tellstick.last_sent_value()
if last_sent_value is not None:
self.brightness = last_sent_value
self._brightness = last_sent_value

View File

@ -4,12 +4,14 @@ homeassistant.components.logbook
Parses events and generates a human log.
"""
from datetime import timedelta
from itertools import groupby
import re
from homeassistant import State, DOMAIN as HA_DOMAIN
from homeassistant.const import (
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
import homeassistant.components.sun as sun
@ -17,12 +19,10 @@ import homeassistant.components.sun as sun
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = '/api/logbook'
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
QUERY_EVENTS_AFTER = "SELECT * FROM events WHERE time_fired > ?"
QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
ORDER BY time_fired
"""
GROUP_BY_MINUTES = 15
@ -37,11 +37,26 @@ def setup(hass, config):
def _handle_get_logbook(handler, path_match, data):
""" Return logbook entries. """
start_today = dt_util.now().replace(hour=0, minute=0, second=0)
date_str = path_match.group('date')
handler.write_json(humanify(
recorder.query_events(
QUERY_EVENTS_AFTER, (dt_util.as_utc(start_today),))))
if date_str:
start_date = dt_util.date_str_to_date(date_str)
if start_date is None:
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
return
start_day = dt_util.start_of_local_day(start_date)
else:
start_day = dt_util.start_of_local_day()
end_day = start_day + timedelta(days=1)
events = recorder.query_events(
QUERY_EVENTS_BETWEEN,
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
handler.write_json(humanify(events))
class Entry(object):

View File

@ -10,7 +10,6 @@ import logging
try:
import pychromecast
import pychromecast.controllers.youtube as youtube
except ImportError:
pychromecast = None
@ -25,30 +24,41 @@ from homeassistant.components.media_player import (
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
REQUIREMENTS = ['pychromecast>=0.6.9']
CONF_IGNORE_CEC = 'ignore_cec'
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE
KNOWN_HOSTS = []
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the cast platform. """
global pychromecast # pylint: disable=invalid-name
if pychromecast is None:
import pychromecast as pychromecast_
pychromecast = pychromecast_
logger = logging.getLogger(__name__)
if pychromecast is None:
logger.error((
"Failed to import pychromecast. Did you maybe not install the "
"'pychromecast' dependency?"))
# import CEC IGNORE attributes
ignore_cec = config.get(CONF_IGNORE_CEC, [])
if isinstance(ignore_cec, list):
pychromecast.IGNORE_CEC += ignore_cec
else:
logger.error('Chromecast conig, %s must be a list.', CONF_IGNORE_CEC)
return False
hosts = []
if discovery_info:
if discovery_info and discovery_info[0] not in KNOWN_HOSTS:
hosts = [discovery_info[0]]
else:
hosts = (host_port[0] for host_port
in pychromecast.discover_chromecasts())
in pychromecast.discover_chromecasts()
if host_port[0] not in KNOWN_HOSTS)
casts = []
@ -57,6 +67,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
casts.append(CastDevice(host))
except pychromecast.ChromecastConnectionError:
pass
else:
KNOWN_HOSTS.append(host)
add_devices(casts)
@ -67,6 +79,7 @@ class CastDevice(MediaPlayerDevice):
# pylint: disable=too-many-public-methods
def __init__(self, host):
import pychromecast.controllers.youtube as youtube
self.cast = pychromecast.Chromecast(host)
self.youtube = youtube.YouTubeController()
self.cast.register_handler(self.youtube)

View File

@ -0,0 +1,307 @@
"""
homeassistant.components.media_player.kodi
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides an interface to the XBMC/Kodi JSON-RPC API
Configuration:
To use Kodi add something like this to your configuration:
media_player:
platform: kodi
name: Kodi
url: http://192.168.0.123/jsonrpc
user: kodi
password: my_secure_password
Variables:
name
*Optional
The name of the device
url
*Required
The URL of the XBMC/Kodi JSON-RPC API. Example: http://192.168.0.123/jsonrpc
user
*Optional
The XBMC/Kodi HTTP username
password
*Optional
The XBMC/Kodi HTTP password
"""
import urllib
import logging
from homeassistant.components.media_player import (
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK)
from homeassistant.const import (
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF)
try:
import jsonrpc_requests
except ImportError:
jsonrpc_requests = None
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['jsonrpc-requests>=0.1']
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the kodi platform. """
global jsonrpc_requests # pylint: disable=invalid-name
if jsonrpc_requests is None:
import jsonrpc_requests as jsonrpc_requests_
jsonrpc_requests = jsonrpc_requests_
add_devices([
KodiDevice(
config.get('name', 'Kodi'),
config.get('url'),
auth=(
config.get('user', ''),
config.get('password', ''))),
])
def _get_image_url(kodi_url):
""" Helper function that parses the thumbnail URLs used by Kodi """
url_components = urllib.parse.urlparse(kodi_url)
if url_components.scheme == 'image':
return urllib.parse.unquote(url_components.netloc)
class KodiDevice(MediaPlayerDevice):
""" Represents a XBMC/Kodi device. """
# pylint: disable=too-many-public-methods
def __init__(self, name, url, auth=None):
self._name = name
self._url = url
self._server = jsonrpc_requests.Server(url, auth=auth)
self._players = None
self._properties = None
self._item = None
self._app_properties = None
self.update()
@property
def name(self):
""" Returns the name of the device. """
return self._name
def _get_players(self):
""" Returns the active player objects or None """
try:
return self._server.Player.GetActivePlayers()
except jsonrpc_requests.jsonrpc.TransportError:
return None
@property
def state(self):
""" Returns the state of the device. """
if self._players is None:
return STATE_OFF
if len(self._players) == 0:
return STATE_IDLE
if self._properties['speed'] == 0:
return STATE_PAUSED
else:
return STATE_PLAYING
def update(self):
""" Retrieve latest state. """
self._players = self._get_players()
if self._players is not None and len(self._players) > 0:
player_id = self._players[0]['playerid']
assert isinstance(player_id, int)
self._properties = self._server.Player.GetProperties(
player_id,
['time', 'totaltime', 'speed']
)
self._item = self._server.Player.GetItem(
player_id,
['title', 'file', 'uniqueid', 'thumbnail', 'artist']
)['item']
self._app_properties = self._server.Application.GetProperties(
['volume', 'muted']
)
else:
self._properties = None
self._item = None
self._app_properties = None
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
if self._app_properties is not None:
return self._app_properties['volume'] / 100.0
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
if self._app_properties is not None:
return self._app_properties['muted']
@property
def media_content_id(self):
""" Content ID of current playing media. """
if self._item is not None:
return self._item['uniqueid']
@property
def media_content_type(self):
""" Content type of current playing media. """
if self._players is not None and len(self._players) > 0:
return self._players[0]['type']
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
if self._properties is not None:
total_time = self._properties['totaltime']
return (
total_time['hours'] * 3600 +
total_time['minutes'] * 60 +
total_time['seconds'])
@property
def media_image_url(self):
""" Image url of current playing media. """
if self._item is not None:
return _get_image_url(self._item['thumbnail'])
@property
def media_title(self):
""" Title of current playing media. """
# find a string we can use as a title
if self._item is not None:
return self._item.get(
'title',
self._item.get(
'label',
self._item.get(
'file',
'unknown')))
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_KODI
def turn_off(self):
""" turn_off media player. """
self._server.System.Shutdown()
self.update_ha_state()
def volume_up(self):
""" volume_up media player. """
assert self._server.Input.ExecuteAction('volumeup') == 'OK'
self.update_ha_state()
def volume_down(self):
""" volume_down media player. """
assert self._server.Input.ExecuteAction('volumedown') == 'OK'
self.update_ha_state()
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self._server.Application.SetVolume(int(volume * 100))
self.update_ha_state()
def mute_volume(self, mute):
""" mute (true) or unmute (false) media player. """
self._server.Application.SetMute(mute)
self.update_ha_state()
def _set_play_state(self, state):
""" Helper method for play/pause/toggle """
players = self._get_players()
if len(players) != 0:
self._server.Player.PlayPause(players[0]['playerid'], state)
self.update_ha_state()
def media_play_pause(self):
""" media_play_pause media player. """
self._set_play_state('toggle')
def media_play(self):
""" media_play media player. """
self._set_play_state(True)
def media_pause(self):
""" media_pause media player. """
self._set_play_state(False)
def _goto(self, direction):
""" Helper method used for previous/next track """
players = self._get_players()
if len(players) != 0:
self._server.Player.GoTo(players[0]['playerid'], direction)
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
self._goto('next')
def media_previous_track(self):
""" Send next track command. """
# first seek to position 0, Kodi seems to go to the beginning
# of the current track current track is not at the beginning
self.media_seek(0)
self._goto('previous')
def media_seek(self, position):
""" Send seek command. """
players = self._get_players()
time = {}
time['milliseconds'] = int((position % 1) * 1000)
position = int(position)
time['seconds'] = int(position % 60)
position /= 60
time['minutes'] = int(position % 60)
position /= 60
time['hours'] = int(position)
if len(players) != 0:
self._server.Player.Seek(players[0]['playerid'], time)
self.update_ha_state()
def turn_on(self):
""" turn the media player on. """
raise NotImplementedError()
def play_youtube(self, media_id):
""" Plays a YouTube media. """
raise NotImplementedError()

View File

@ -48,7 +48,7 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-mpd2>=0.5.4']
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
@ -62,12 +62,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config.get('port', 6600)
location = config.get('location', 'MPD')
global mpd # pylint: disable=invalid-name
if mpd is None:
_LOGGER.exception(
"Unable to import mpd2. "
"Did you maybe not install the 'python-mpd2' package?")
return False
import mpd as mpd_
mpd = mpd_
# pylint: disable=no-member
try:

View File

@ -0,0 +1,78 @@
"""
homeassistant.components.notify.file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
File notification service.
Configuration:
To use the File notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: file
filename: FILENAME
timestamp: 1 or 0
Variables:
filename
*Required
Name of the file to use. The file will be created if it doesn't exist and saved
in your config/ folder.
timestamp
*Required
Add a timestamp to the entry, valid entries are 1 or 0.
"""
import logging
import os
import homeassistant.util.dt as dt_util
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config):
""" Get the file notification service. """
if not validate_config(config,
{DOMAIN: ['filename',
'timestamp']},
_LOGGER):
return None
filename = config[DOMAIN]['filename']
timestamp = config[DOMAIN]['timestamp']
return FileNotificationService(hass, filename, timestamp)
# pylint: disable=too-few-public-methods
class FileNotificationService(BaseNotificationService):
""" Implements notification service for the File service. """
def __init__(self, hass, filename, add_timestamp):
self.filepath = os.path.join(hass.config.config_dir, filename)
self.add_timestamp = add_timestamp
def send_message(self, message="", **kwargs):
""" Send a message to a file. """
with open(self.filepath, 'a') as file:
if os.stat(self.filepath).st_size == 0:
title = '{} notifications (Log started: {})\n{}\n'.format(
kwargs.get(ATTR_TITLE),
dt_util.strip_microseconds(dt_util.utcnow()),
'-'*80)
file.write(title)
if self.add_timestamp == 1:
text = '{} {}\n'.format(dt_util.utcnow(), message)
file.write(text)
else:
text = '{}\n'.format(message)
file.write(text)

View File

@ -28,6 +28,7 @@ from homeassistant.components.notify import (
from homeassistant.const import CONF_API_KEY
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pushbullet.py>=0.7.1']
def get_service(hass, config):

View File

@ -42,6 +42,7 @@ from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
REQUIREMENTS = ['python-pushover>=0.2']
_LOGGER = logging.getLogger(__name__)

View File

@ -47,6 +47,8 @@ from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
REQUIREMENTS = ['sleekxmpp>=1.3.1']
def get_service(hass, config):
""" Get the Jabber (XMPP) notification service. """

View File

@ -0,0 +1,87 @@
"""
homeassistant.components.sensor.arduino
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for getting information from Arduino pins. Only analog pins are
supported.
Configuration:
sensor:
platform: arduino
pins:
7:
name: Door switch
type: analog
0:
name: Brightness
type: analog
Variables:
pins
*Required
An array specifying the digital pins to use on the Arduino board.
These are the variables for the pins array:
name
*Required
The name for the pin that will be used in the frontend.
type
*Required
The type of the pin: 'analog'.
"""
import logging
import homeassistant.components.arduino as arduino
from homeassistant.helpers.entity import Entity
from homeassistant.const import DEVICE_DEFAULT_NAME
DEPENDENCIES = ['arduino']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Arduino platform. """
# Verify that Arduino board is present
if arduino.BOARD is None:
_LOGGER.error('A connection has not been made to the Arduino board.')
return False
sensors = []
pins = config.get('pins')
for pinnum, pin in pins.items():
if pin.get('name'):
sensors.append(ArduinoSensor(pin.get('name'),
pinnum,
'analog'))
add_devices(sensors)
class ArduinoSensor(Entity):
""" Represents an Arduino Sensor. """
def __init__(self, name, pin, pin_type):
self._pin = pin
self._name = name or DEVICE_DEFAULT_NAME
self.pin_type = pin_type
self.direction = 'in'
self._value = None
arduino.BOARD.set_mode(self._pin, self.direction, self.pin_type)
@property
def state(self):
""" Returns the state of the sensor. """
return self._value
@property
def name(self):
""" Get the name of the sensor. """
return self._name
def update(self):
""" Get the latest value from the pin. """
self._value = arduino.BOARD.get_analog_inputs()[self._pin][1]

View File

@ -71,6 +71,7 @@ from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['blockchain>=1.1.2']
_LOGGER = logging.getLogger(__name__)
OPTION_TYPES = {
'wallet': ['Wallet balance', 'BTC'],
@ -113,7 +114,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"Unable to import blockchain. "
"Did you maybe not install the 'blockchain' package?")
return None
return False
wallet_id = config.get('wallet', None)
password = config.get('password', None)

View File

@ -0,0 +1,138 @@
"""
homeassistant.components.sensor.efergy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Monitors home energy use as measured by an efergy
engage hub using its (unofficial, undocumented) API.
Configuration:
To use the efergy sensor you will need to add something
like the following to your config/configuration.yaml
sensor:
platform: efergy
app_token: APP_TOKEN
utc_offset: UTC_OFFSET
monitored_variables:
- type: instant_readings
- type: budget
- type: cost
period: day
currency: $
Variables:
api_key
*Required
To get a new App Token, log in to your efergy account, go
to the Settings page, click on App tokens, and click "Add token".
utc_offset
*Required for some variables
Some variables (currently only the daily_cost) require that the
negative number of minutes your timezone is ahead/behind UTC time.
monitored_variables
*Required
An array specifying the variables to monitor.
period
*Optional
Some variables take a period argument. Valid options are "day",
1"week", "month", and "year"
currency
*Optional
This is used to display the cost/period as the unit when monitoring the
cost. It should correspond to the actual currency used in your dashboard.
"""
import logging
from requests import get
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'https://engage.efergy.com/mobile_proxy/'
SENSOR_TYPES = {
'instant_readings': ['Energy Usage', 'kW'],
'budget': ['Energy Budget', ''],
'cost': ['Energy Cost', ''],
}
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the efergy sensor. """
app_token = config.get("app_token")
if not app_token:
_LOGGER.error(
"Configuration Error"
"Please make sure you have configured your app token")
return None
utc_offset = str(config.get("utc_offset"))
dev = []
for variable in config['monitored_variables']:
if 'period' not in variable:
variable['period'] = ''
if 'currency' not in variable:
variable['currency'] = ''
if variable['type'] not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(EfergySensor(variable['type'], app_token, utc_offset,
variable['period'], variable['currency']))
add_devices(dev)
# pylint: disable=too-many-instance-attributes
class EfergySensor(Entity):
""" Implements an Efergy sensor. """
# pylint: disable=too-many-arguments
def __init__(self, sensor_type, app_token, utc_offset, period, currency):
self._name = SENSOR_TYPES[sensor_type][0]
self.type = sensor_type
self.app_token = app_token
self.utc_offset = utc_offset
self._state = None
self.period = period
self.currency = currency
if self.type == 'cost':
self._unit_of_measurement = self.currency + '/' + self.period
else:
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@property
def name(self):
""" Returns the name. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._unit_of_measurement
def update(self):
""" Gets the efergy monitor data from the web service """
if self.type == 'instant_readings':
url_string = _RESOURCE + 'getInstant?token=' + self.app_token
response = get(url_string)
self._state = response.json()['reading'] / 1000
elif self.type == 'budget':
url_string = _RESOURCE + 'getBudget?token=' + self.app_token
response = get(url_string)
self._state = response.json()['status']
elif self.type == 'cost':
url_string = _RESOURCE + 'getCost?token=' + self.app_token \
+ '&offset=' + self.utc_offset + '&period=' \
+ self.period
response = get(url_string)
self._state = response.json()['sum']
else:
self._state = 'Unknown'

View File

@ -0,0 +1,224 @@
"""
homeassistant.components.sensor.forecast
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Forecast.io service.
Configuration:
To use the Forecast sensor you will need to add something like the
following to your config/configuration.yaml
sensor:
platform: forecast
api_key: YOUR_APP_KEY
monitored_conditions:
- summary
- precip_type
- precip_intensity
- temperature
- dew_point
- wind_speed
- wind_bearing
- cloud_cover
- humidity
- pressure
- visibility
- ozone
Variables:
api_key
*Required
To retrieve this value log into your account at http://forecast.io/. You can
make 1000 requests per day. This means that you could create every 1.4 minute
one.
monitored_conditions
*Required
An array specifying the conditions to monitor.
These are the variables for the monitored_conditions array:
type
*Required
The condition you wish to monitor, see the configuration example above for a
list of all available conditions to monitor.
Details for the API : https://developer.forecast.io/docs/v2
"""
import logging
from datetime import timedelta
try:
import forecastio
except ImportError:
forecastio = None
from homeassistant.util import Throttle
from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT)
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'summary': ['Summary', ''],
'precip_type': ['Precip', ''],
'precip_intensity': ['Precip intensity', 'mm'],
'temperature': ['Temperature', ''],
'dew_point': ['Dew point', '°C'],
'wind_speed': ['Wind Speed', 'm/s'],
'wind_bearing': ['Wind Bearing', '°'],
'cloud_cover': ['Cloud coverage', '%'],
'humidity': ['Humidity', '%'],
'pressure': ['Pressure', 'mBar'],
'visibility': ['Visibility', 'km'],
'ozone': ['Ozone', ''],
}
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the Forecast.io sensor. """
global forecastio # pylint: disable=invalid-name
if forecastio is None:
import forecastio as forecastio_
forecastio = forecastio_
if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
unit = hass.config.temperature_unit
try:
forecast = forecastio.load_forecast(config.get(CONF_API_KEY, None),
hass.config.latitude,
hass.config.longitude)
forecast.currently()
except ValueError:
_LOGGER.error(
"Connection error "
"Please check your settings for Forecast.io.")
return False
data = ForeCastData(config.get(CONF_API_KEY, None),
hass.config.latitude,
hass.config.longitude)
dev = []
for variable in config['monitored_conditions']:
if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(ForeCastSensor(data, variable, unit))
add_devices(dev)
# pylint: disable=too-few-public-methods
class ForeCastSensor(Entity):
""" Implements an OpenWeatherMap sensor. """
def __init__(self, weather_data, sensor_type, unit):
self.client_name = 'Forecast'
self._name = SENSOR_TYPES[sensor_type][0]
self.forecast_client = weather_data
self._unit = unit
self.type = sensor_type
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self.update()
@property
def name(self):
return '{} - {}'.format(self.client_name, self._name)
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._unit_of_measurement
# pylint: disable=too-many-branches
def update(self):
""" Gets the latest data from Forecast.io and updates the states. """
self.forecast_client.update()
data = self.forecast_client.data
try:
if self.type == 'summary':
self._state = data.summary
# elif self.type == 'sunrise_time':
# self._state = data.sunriseTime
# elif self.type == 'sunset_time':
# self._state = data.sunsetTime
elif self.type == 'precip_intensity':
if data.precipIntensity == 0:
self._state = 'None'
self._unit_of_measurement = ''
else:
self._state = data.precipIntensity
elif self.type == 'precip_type':
if data.precipType is None:
self._state = 'None'
self._unit_of_measurement = ''
else:
self._state = data.precipType
elif self.type == 'dew_point':
if self._unit == TEMP_CELCIUS:
self._state = round(data.dewPoint, 1)
elif self._unit == TEMP_FAHRENHEIT:
self._state = round(data.dewPoint * 1.8 + 32.0, 1)
else:
self._state = round(data.dewPoint, 1)
elif self.type == 'temperature':
if self._unit == TEMP_CELCIUS:
self._state = round(data.temperature, 1)
elif self._unit == TEMP_FAHRENHEIT:
self._state = round(data.temperature * 1.8 + 32.0, 1)
else:
self._state = round(data.temperature, 1)
elif self.type == 'wind_speed':
self._state = data.windSpeed
elif self.type == 'wind_bearing':
self._state = data.windBearing
elif self.type == 'cloud_cover':
self._state = round(data.cloudCover * 100, 1)
elif self.type == 'humidity':
self._state = round(data.humidity * 100, 1)
elif self.type == 'pressure':
self._state = round(data.pressure, 1)
elif self.type == 'visibility':
self._state = data.visibility
elif self.type == 'ozone':
self._state = round(data.ozone, 1)
except forecastio.utils.PropertyUnavailable:
pass
class ForeCastData(object):
""" Gets the latest data from Forecast.io. """
def __init__(self, api_key, latitude, longitude):
self._api_key = api_key
self.latitude = latitude
self.longitude = longitude
self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from Forecast.io. """
forecast = forecastio.load_forecast(self._api_key,
self.latitude,
self.longitude)
self.data = forecast.currently()

View File

@ -39,6 +39,7 @@ ATTR_NODE_ID = "node_id"
ATTR_CHILD_ID = "child_id"
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyserial>=2.7']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -1,7 +1,6 @@
"""
homeassistant.components.sensor.openweathermap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OpenWeatherMap (OWM) service.
Configuration:
@ -12,7 +11,8 @@ following to your config/configuration.yaml
sensor:
platform: openweathermap
api_key: YOUR_APP_KEY
monitored_variables:
forecast: 0 or 1
monitored_conditions:
- weather
- temperature
- wind_speed
@ -28,15 +28,13 @@ api_key
*Required
To retrieve this value log into your account at http://openweathermap.org/
forecast
*Optional
Enables the forecast. The default is to display the current conditions.
monitored_conditions
*Required
An array specifying the variables to monitor.
These are the variables for the monitored_conditions array:
type
*Required
The variable you wish to monitor, see the configuration example above for a
*Optional
Conditions to monitor. See the configuration example above for a
list of all available conditions to monitor.
Details for the API : http://bugs.openweathermap.org/projects/api/wiki
@ -50,6 +48,7 @@ from homeassistant.util import Throttle
from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT)
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pywm>=2.2.1']
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'weather': ['Condition', ''],
@ -81,10 +80,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"Unable to import pyowm. "
"Did you maybe not install the 'PyOWM' package?")
return None
return False
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
unit = hass.config.temperature_unit
forecast = config.get('forecast', 0)
owm = OWM(config.get(CONF_API_KEY, None))
if not owm:
@ -93,13 +93,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"Please check your settings for OpenWeatherMap.")
return None
data = WeatherData(owm, hass.config.latitude, hass.config.longitude)
data = WeatherData(owm, forecast, hass.config.latitude,
hass.config.longitude)
dev = []
for variable in config['monitored_conditions']:
if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(OpenWeatherMapSensor(data, variable, unit))
try:
for variable in config['monitored_conditions']:
if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(OpenWeatherMapSensor(data, variable, unit))
except KeyError:
pass
if forecast == 1:
SENSOR_TYPES['forecast'] = ['Forecast', '']
dev.append(OpenWeatherMapSensor(data, 'forecast', unit))
add_devices(dev)
@ -108,11 +116,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class OpenWeatherMapSensor(Entity):
""" Implements an OpenWeatherMap sensor. """
def __init__(self, weather_data, sensor_type, unit):
self.client_name = 'Weather - '
def __init__(self, weather_data, sensor_type, temp_unit):
self.client_name = 'Weather'
self._name = SENSOR_TYPES[sensor_type][0]
self.owa_client = weather_data
self._unit = unit
self.temp_unit = temp_unit
self.type = sensor_type
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@ -120,7 +128,7 @@ class OpenWeatherMapSensor(Entity):
@property
def name(self):
return self.client_name + ' ' + self._name
return '{} {}'.format(self.client_name, self._name)
@property
def state(self):
@ -138,14 +146,15 @@ class OpenWeatherMapSensor(Entity):
self.owa_client.update()
data = self.owa_client.data
fc_data = self.owa_client.fc_data
if self.type == 'weather':
self._state = data.get_detailed_status()
elif self.type == 'temperature':
if self._unit == TEMP_CELCIUS:
if self.temp_unit == TEMP_CELCIUS:
self._state = round(data.get_temperature('celsius')['temp'],
1)
elif self._unit == TEMP_FAHRENHEIT:
elif self.temp_unit == TEMP_FAHRENHEIT:
self._state = round(data.get_temperature('fahrenheit')['temp'],
1)
else:
@ -161,29 +170,39 @@ class OpenWeatherMapSensor(Entity):
elif self.type == 'rain':
if data.get_rain():
self._state = round(data.get_rain()['3h'], 0)
self._unit_of_measurement = 'mm'
else:
self._state = 'not raining'
self._unit_of_measurement = ''
elif self.type == 'snow':
if data.get_snow():
self._state = round(data.get_snow(), 0)
self._unit_of_measurement = 'mm'
else:
self._state = 'not snowing'
self._unit_of_measurement = ''
elif self.type == 'forecast':
self._state = fc_data.get_weathers()[0].get_status()
class WeatherData(object):
""" Gets the latest data from OpenWeatherMap. """
def __init__(self, owm, latitude, longitude):
def __init__(self, owm, forecast, latitude, longitude):
self.owm = owm
self.forecast = forecast
self.latitude = latitude
self.longitude = longitude
self.data = None
self.fc_data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from OpenWeatherMap. """
obs = self.owm.weather_at_coords(self.latitude, self.longitude)
self.data = obs.get_weather()
if self.forecast == 1:
obs = self.owm.three_hours_forecast_at_coords(self.latitude,
self.longitude)
self.fc_data = obs.get_forecast()

View File

@ -122,7 +122,7 @@ class PublicTransportData(object):
try:
return [
dt_util.datetime_to_short_time_str(
dt_util.datetime_to_time_str(
dt_util.as_local(dt_util.utc_from_timestamp(
item['from']['departureTimestamp']))
)

View File

@ -21,9 +21,26 @@ sensor:
- type: 'memory_use_percent'
- type: 'memory_use'
- type: 'memory_free'
- type: 'swap_use_percent'
- type: 'swap_use'
- type: 'swap_free'
- type: 'network_in'
arg: 'eth0'
- type: 'network_out'
arg: 'eth0'
- type: 'packets_in'
arg: 'eth0'
- type: 'packets_out'
arg: 'eth0'
- type: 'ipv4_address'
arg: 'eth0'
- type: 'ipv6_address'
arg: 'eth0'
- type: 'processor_use'
- type: 'process'
arg: 'octave-cli'
- type: 'last_boot'
- type: 'since_last_boot'
Variables:
@ -42,13 +59,14 @@ arg
*Optional
Additional details for the type, eg. path, binary name, etc.
"""
import logging
import psutil
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
from homeassistant.const import STATE_ON, STATE_OFF
import psutil
import logging
REQUIREMENTS = ['psutil>=3.0.0']
SENSOR_TYPES = {
'disk_use_percent': ['Disk Use', '%'],
'disk_use': ['Disk Use', 'GiB'],
@ -58,6 +76,17 @@ SENSOR_TYPES = {
'memory_free': ['RAM Free', 'MiB'],
'processor_use': ['CPU Use', '%'],
'process': ['Process', ''],
'swap_use_percent': ['Swap Use', '%'],
'swap_use': ['Swap Use', 'GiB'],
'swap_free': ['Swap Free', 'GiB'],
'network_out': ['Sent', 'MiB'],
'network_in': ['Recieved', 'MiB'],
'packets_out': ['Packets sent', ''],
'packets_in': ['Packets recieved', ''],
'ipv4_address': ['IPv4 address', ''],
'ipv6_address': ['IPv6 address', ''],
'last_boot': ['Last Boot', ''],
'since_last_boot': ['Since Last Boot', '']
}
_LOGGER = logging.getLogger(__name__)
@ -103,6 +132,7 @@ class SystemMonitorSensor(Entity):
def unit_of_measurement(self):
return self._unit_of_measurement
# pylint: disable=too-many-branches
def update(self):
if self.type == 'disk_use_percent':
self._state = psutil.disk_usage(self.argument).percent
@ -120,6 +150,12 @@ class SystemMonitorSensor(Entity):
1024**2, 1)
elif self.type == 'memory_free':
self._state = round(psutil.virtual_memory().available / 1024**2, 1)
elif self.type == 'swap_use_percent':
self._state = psutil.swap_memory().percent
elif self.type == 'swap_use':
self._state = round(psutil.swap_memory().used / 1024**3, 1)
elif self.type == 'swap_free':
self._state = round(psutil.swap_memory().free / 1024**3, 1)
elif self.type == 'processor_use':
self._state = round(psutil.cpu_percent(interval=None))
elif self.type == 'process':
@ -127,3 +163,24 @@ class SystemMonitorSensor(Entity):
self._state = STATE_ON
else:
self._state = STATE_OFF
elif self.type == 'network_out':
self._state = round(psutil.net_io_counters(pernic=True)
[self.argument][0] / 1024**2, 1)
elif self.type == 'network_in':
self._state = round(psutil.net_io_counters(pernic=True)
[self.argument][1] / 1024**2, 1)
elif self.type == 'packets_out':
self._state = psutil.net_io_counters(pernic=True)[self.argument][2]
elif self.type == 'packets_in':
self._state = psutil.net_io_counters(pernic=True)[self.argument][3]
elif self.type == 'ipv4_address':
self._state = psutil.net_if_addrs()[self.argument][0][1]
elif self.type == 'ipv6_address':
self._state = psutil.net_if_addrs()[self.argument][1][1]
elif self.type == 'last_boot':
self._state = dt_util.datetime_to_date_str(
dt_util.as_local(
dt_util.utc_from_timestamp(psutil.boot_time())))
elif self.type == 'since_last_boot':
self._state = dt_util.utcnow() - dt_util.utc_from_timestamp(
psutil.boot_time())

View File

@ -12,24 +12,18 @@ following to your config/configuration.yaml
sensor:
platform: time_date
display_options:
- type: 'time'
- type: 'date'
- type: 'date_time'
- type: 'time_date'
- type: 'time_utc'
- type: 'beat'
- 'time'
- 'date'
- 'date_time'
- 'time_date'
- 'time_utc'
- 'beat'
Variables:
display_options
*Required
An array specifying the variables to display.
These are the variables for the display_options array.:
type
*Required
The variable you wish to display, see the configuration example above for a
The variable you wish to display. See the configuration example above for a
list of all available variables.
"""
import logging
@ -57,10 +51,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = []
for variable in config['display_options']:
if variable['type'] not in OPTION_TYPES:
_LOGGER.error('Option type: "%s" does not exist', variable['type'])
if variable not in OPTION_TYPES:
_LOGGER.error('Option type: "%s" does not exist', variable)
else:
dev.append(TimeDateSensor(variable['type']))
dev.append(TimeDateSensor(variable))
add_devices(dev)
@ -89,9 +83,9 @@ class TimeDateSensor(Entity):
""" Gets the latest data and updates the states. """
time_date = dt_util.utcnow()
time = dt_util.datetime_to_short_time_str(dt_util.as_local(time_date))
time_utc = dt_util.datetime_to_short_time_str(time_date)
date = dt_util.datetime_to_short_date_str(dt_util.as_local(time_date))
time = dt_util.datetime_to_time_str(dt_util.as_local(time_date))
time_utc = dt_util.datetime_to_time_str(time_date)
date = dt_util.datetime_to_date_str(dt_util.as_local(time_date))
# Calculate the beat (Swatch Internet Time) time without date.
hours, minutes, seconds = time_date.strftime('%H:%M:%S').split(':')

View File

@ -67,6 +67,7 @@ from transmissionrpc.error import TransmissionError
import logging
REQUIREMENTS = ['transmissionrpc>=0.11']
SENSOR_TYPES = {
'current_status': ['Status', ''],
'download_speed': ['Down Speed', 'MB/s'],

View File

@ -25,7 +25,7 @@ from datetime import timedelta
try:
import ephem
except ImportError:
# Error will be raised during setup
# Will be fixed during setup
ephem = None
import homeassistant.util.dt as dt_util
@ -33,6 +33,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.components.scheduler import ServiceEventListener
DEPENDENCIES = []
REQUIREMENTS = ['pyephem>=3.7']
DOMAIN = "sun"
ENTITY_ID = "sun.sun"
@ -100,9 +101,10 @@ def setup(hass, config):
""" Tracks the state of the sun. """
logger = logging.getLogger(__name__)
global ephem # pylint: disable=invalid-name
if ephem is None:
logger.exception("Error while importing dependency ephem.")
return False
import ephem as ephem_
ephem = ephem_
if None in (hass.config.latitude, hass.config.longitude):
logger.error("Latitude or longitude not set in Home Assistant config")

View File

@ -7,6 +7,7 @@ import logging
from datetime import timedelta
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
@ -33,6 +34,11 @@ DISCOVERY_PLATFORMS = {
isy994.DISCOVER_SWITCHES: 'isy994',
}
PROP_TO_ATTR = {
'current_power_mwh': ATTR_CURRENT_POWER_MWH,
'today_power_mw': ATTR_TODAY_MWH,
}
_LOGGER = logging.getLogger(__name__)
@ -74,10 +80,48 @@ def setup(hass, config):
else:
switch.turn_off()
switch.update_ha_state(True)
if switch.should_poll:
switch.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service)
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
return True
class SwitchDevice(ToggleEntity):
""" Represents a switch within Home Assistant. """
# pylint: disable=no-self-use
@property
def current_power_mwh(self):
""" Current power usage in mwh. """
return None
@property
def today_power_mw(self):
""" Today total power usage in mw. """
return None
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
data = {}
for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop)
if value:
data[attr] = value
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data

View File

@ -0,0 +1,93 @@
"""
homeassistant.components.switch.arduino
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for switching Arduino pins on and off. So fare only digital pins are
supported.
Configuration:
switch:
platform: arduino
pins:
11:
name: Fan Office
type: digital
12:
name: Light Desk
type: digital
Variables:
pins
*Required
An array specifying the digital pins to use on the Arduino board.
These are the variables for the pins array:
name
*Required
The name for the pin that will be used in the frontend.
type
*Required
The type of the pin: 'digital'.
"""
import logging
import homeassistant.components.arduino as arduino
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import DEVICE_DEFAULT_NAME
DEPENDENCIES = ['arduino']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Arduino platform. """
# Verify that Arduino board is present
if arduino.BOARD is None:
_LOGGER.error('A connection has not been made to the Arduino board.')
return False
switches = []
pins = config.get('pins')
for pinnum, pin in pins.items():
if pin.get('name'):
switches.append(ArduinoSwitch(pin.get('name'),
pinnum,
pin.get('type')))
add_devices(switches)
class ArduinoSwitch(SwitchDevice):
""" Represents an Arduino Switch. """
def __init__(self, name, pin, pin_type):
self._pin = pin
self._name = name or DEVICE_DEFAULT_NAME
self.pin_type = pin_type
self.direction = 'out'
self._state = False
arduino.BOARD.set_mode(self._pin, self.direction, self.pin_type)
@property
def name(self):
""" Get the name of the pin. """
return self._name
@property
def is_on(self):
""" Returns True if pin is high/on. """
return self._state
def turn_on(self):
""" Turns the pin to high/on. """
self._state = True
arduino.BOARD.set_digital_out_high(self._pin)
def turn_off(self):
""" Turns the pin to low/off. """
self._state = False
arduino.BOARD.set_digital_out_low(self._pin)

View File

@ -6,8 +6,7 @@ homeassistant.components.switch.command_switch
Allows to configure custom shell commands to turn a switch on/off.
"""
import logging
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.switch import SwitchDevice
import subprocess
_LOGGER = logging.getLogger(__name__)
@ -30,11 +29,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(devices)
class CommandSwitch(ToggleEntity):
class CommandSwitch(SwitchDevice):
""" Represents a switch that can be togggled using shell commands """
def __init__(self, name, command_on, command_off):
self._name = name or DEVICE_DEFAULT_NAME
self._state = STATE_OFF
self._name = name
self._state = False
self._command_on = command_on
self._command_off = command_off
@ -60,22 +59,19 @@ class CommandSwitch(ToggleEntity):
""" The name of the switch """
return self._name
@property
def state(self):
""" Returns the state of the switch. """
return self._state
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
if CommandSwitch._switch(self._command_on):
self._state = STATE_ON
self._state = True
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
if CommandSwitch._switch(self._command_off):
self._state = STATE_OFF
self._state = False
self.update_ha_state()

View File

@ -5,20 +5,20 @@ homeassistant.components.switch.demo
Demo platform that has two fake switches.
"""
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import DEVICE_DEFAULT_NAME
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo switches. """
add_devices_callback([
DemoSwitch('Ceiling', STATE_ON),
DemoSwitch('AC', STATE_OFF)
DemoSwitch('Ceiling', True),
DemoSwitch('AC', False)
])
class DemoSwitch(ToggleEntity):
class DemoSwitch(SwitchDevice):
""" Provides a demo switch. """
def __init__(self, name, state):
self._name = name or DEVICE_DEFAULT_NAME
@ -35,19 +35,27 @@ class DemoSwitch(ToggleEntity):
return self._name
@property
def state(self):
""" Returns the state of the device if any. """
return self._state
def current_power_mwh(self):
""" Current power usage in mwh. """
if self._state:
return 100
@property
def today_power_mw(self):
""" Today total power usage in mw. """
return 1500
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
self._state = STATE_ON
self._state = True
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = STATE_OFF
self._state = False
self.update_ha_state()

View File

@ -53,7 +53,7 @@ except ImportError:
hikvision.api = None
_LOGGING = logging.getLogger(__name__)
REQUIREMENTS = ['hikvision>=0.4']
# pylint: disable=too-many-arguments
# pylint: disable=too-many-instance-attributes

View File

@ -3,6 +3,12 @@ homeassistant.components.switch.tellstick
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Tellstick switches.
Because the tellstick sends its actions via radio and from most
receivers it's impossible to know if the signal was received or not.
Therefore you can configure the switch to try to send each signal repeatedly
with the config parameter signal_repetitions (default is 1).
signal_repetitions: 3
"""
import logging
@ -11,6 +17,8 @@ from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
SINGAL_REPETITIONS = 1
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@ -22,6 +30,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"Failed to import tellcore")
return
signal_repetitions = config.get('signal_repetitions', SINGAL_REPETITIONS)
core = telldus.TelldusCore()
switches_and_lights = core.devices()
@ -29,7 +39,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
for switch in switches_and_lights:
if not switch.methods(tellcore_constants.TELLSTICK_DIM):
switches.append(TellstickSwitchDevice(switch))
switches.append(TellstickSwitchDevice(switch, signal_repetitions))
add_devices_callback(switches)
@ -39,9 +49,10 @@ class TellstickSwitchDevice(ToggleEntity):
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF)
def __init__(self, tellstick):
def __init__(self, tellstick, signal_repetitions):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
self.signal_repetitions = signal_repetitions
@property
def name(self):
@ -63,8 +74,10 @@ class TellstickSwitchDevice(ToggleEntity):
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.tellstick.turn_on()
for _ in range(self.signal_repetitions):
self.tellstick.turn_on()
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()
for _ in range(self.signal_repetitions):
self.tellstick.turn_off()

View File

@ -54,6 +54,7 @@ from transmissionrpc.error import TransmissionError
import logging
_LOGGING = logging.getLogger(__name__)
REQUIREMENTS = ['transmissionrpc>=0.11']
# pylint: disable=unused-argument

View File

@ -6,9 +6,7 @@ Support for WeMo switches.
"""
import logging
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.switch import (
ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH)
from homeassistant.components.switch import SwitchDevice
# pylint: disable=unused-argument
@ -43,10 +41,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if isinstance(switch, pywemo.Switch)])
class WemoSwitch(ToggleEntity):
class WemoSwitch(SwitchDevice):
""" Represents a WeMo switch within Home Assistant. """
def __init__(self, wemo):
self.wemo = wemo
self.insight_params = None
@property
def unique_id(self):
@ -59,15 +58,16 @@ class WemoSwitch(ToggleEntity):
return self.wemo.name
@property
def state_attributes(self):
""" Returns optional state attributes. """
if self.wemo.model.startswith('Belkin Insight'):
cur_info = self.wemo.insight_params
def current_power_mwh(self):
""" Current power usage in mwh. """
if self.insight_params:
return self.insight_params['currentpower']
return {
ATTR_CURRENT_POWER_MWH: cur_info['currentpower'],
ATTR_TODAY_MWH: cur_info['todaymw']
}
@property
def today_power_mw(self):
""" Today total power usage in mw. """
if self.insight_params:
return self.insight_params['todaymw']
@property
def is_on(self):
@ -85,3 +85,5 @@ class WemoSwitch(ToggleEntity):
def update(self):
""" Update WeMo state. """
self.wemo.get_state(True)
if self.wemo.model.startswith('Belkin Insight'):
self.insight_params = self.wemo.insight_params

View File

@ -6,6 +6,8 @@ import logging
from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
REQUIREMENTS = ['python-nest>=2.3.1']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -93,4 +93,4 @@ class WinkToggleDevice(ToggleEntity):
def update(self):
""" Update state of the light. """
self.wink.wait_till_desired_reached()
self.wink.updateState()

View File

@ -13,6 +13,7 @@ from homeassistant.const import (
DOMAIN = "zwave"
DEPENDENCIES = []
REQUIREMENTS = ['pydispatcher>=2.0.5']
CONF_USB_STICK_PATH = "usb_path"
DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick"

View File

@ -11,7 +11,7 @@ from homeassistant import HomeAssistantError
from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME,
CONF_TIME_ZONE)
import homeassistant.util as util
import homeassistant.util.location as loc_util
_LOGGER = logging.getLogger(__name__)
@ -55,7 +55,7 @@ def create_default_config(config_dir, detect_location=True):
info = {attr: default for attr, default, *_ in DEFAULT_CONFIG}
location_info = detect_location and util.detect_location_info()
location_info = detect_location and loc_util.detect_location_info()
if location_info:
if location_info.use_fahrenheit:

View File

@ -146,3 +146,4 @@ HTTP_HEADER_CACHE_CONTROL = "Cache-Control"
HTTP_HEADER_EXPIRES = "Expires"
CONTENT_TYPE_JSON = "application/json"
CONTENT_TYPE_MULTIPART = 'multipart/x-mixed-replace; boundary={}'

View File

@ -16,8 +16,6 @@ import random
import string
from functools import wraps
import requests
# DEPRECATED AS OF 4/27/2015 - moved to homeassistant.util.dt package
# pylint: disable=unused-import
from .dt import ( # noqa
@ -64,46 +62,6 @@ def repr_helper(inp):
return str(inp)
# Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py
# License: Code is given as is. Use at your own risk and discretion.
# pylint: disable=invalid-name
def color_RGB_to_xy(R, G, B):
""" Convert from RGB color to XY color. """
if R + G + B == 0:
return 0, 0
var_R = (R / 255.)
var_G = (G / 255.)
var_B = (B / 255.)
if var_R > 0.04045:
var_R = ((var_R + 0.055) / 1.055) ** 2.4
else:
var_R /= 12.92
if var_G > 0.04045:
var_G = ((var_G + 0.055) / 1.055) ** 2.4
else:
var_G /= 12.92
if var_B > 0.04045:
var_B = ((var_B + 0.055) / 1.055) ** 2.4
else:
var_B /= 12.92
var_R *= 100
var_G *= 100
var_B *= 100
# Observer. = 2 deg, Illuminant = D65
X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
# Convert XYZ to xy, see CIE 1931 color space on wikipedia
return X / (X + Y + Z), Y / (X + Y + Z)
def convert(value, to_type, default=None):
""" Converts value to to_type, returns default if fails. """
try:
@ -154,32 +112,6 @@ def get_random_string(length=10):
return ''.join(generator.choice(source_chars) for _ in range(length))
LocationInfo = collections.namedtuple(
"LocationInfo",
['ip', 'country_code', 'country_name', 'region_code', 'region_name',
'city', 'zip_code', 'time_zone', 'latitude', 'longitude',
'use_fahrenheit'])
def detect_location_info():
""" Detect location information. """
try:
raw_info = requests.get(
'https://freegeoip.net/json/', timeout=5).json()
except requests.RequestException:
return
data = {key: raw_info.get(key) for key in LocationInfo._fields}
# From Wikipedia: Fahrenheit is used in the Bahamas, Belize,
# the Cayman Islands, Palau, and the United States and associated
# territories of American Samoa and the U.S. Virgin Islands
data['use_fahrenheit'] = data['country_code'] in (
'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI')
return LocationInfo(**data)
class OrderedEnum(enum.Enum):
""" Taken from Python 3.4.0 docs. """
# pylint: disable=no-init, too-few-public-methods

View File

@ -0,0 +1,41 @@
"""Color util methods."""
# Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py
# License: Code is given as is. Use at your own risk and discretion.
# pylint: disable=invalid-name
def color_RGB_to_xy(R, G, B):
""" Convert from RGB color to XY color. """
if R + G + B == 0:
return 0, 0
var_R = (R / 255.)
var_G = (G / 255.)
var_B = (B / 255.)
if var_R > 0.04045:
var_R = ((var_R + 0.055) / 1.055) ** 2.4
else:
var_R /= 12.92
if var_G > 0.04045:
var_G = ((var_G + 0.055) / 1.055) ** 2.4
else:
var_G /= 12.92
if var_B > 0.04045:
var_B = ((var_B + 0.055) / 1.055) ** 2.4
else:
var_B /= 12.92
var_R *= 100
var_G *= 100
var_B *= 100
# Observer. = 2 deg, Illuminant = D65
X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
# Convert XYZ to xy, see CIE 1931 color space on wikipedia
return X / (X + Y + Z), Y / (X + Y + Z)

View File

@ -9,9 +9,9 @@ import datetime as dt
import pytz
DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
DATE_SHORT_STR_FORMAT = "%Y-%m-%d"
TIME_SHORT_STR_FORMAT = "%H:%M"
DATETIME_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
DATE_STR_FORMAT = "%Y-%m-%d"
TIME_STR_FORMAT = "%H:%M"
UTC = DEFAULT_TIME_ZONE = pytz.utc
@ -34,7 +34,7 @@ def get_time_zone(time_zone_str):
def utcnow():
""" Get now in UTC time. """
return dt.datetime.now(pytz.utc)
return dt.datetime.now(UTC)
def now(time_zone=None):
@ -45,12 +45,12 @@ def now(time_zone=None):
def as_utc(dattim):
""" Return a datetime as UTC time.
Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE. """
if dattim.tzinfo == pytz.utc:
if dattim.tzinfo == UTC:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE)
return dattim.astimezone(pytz.utc)
return dattim.astimezone(UTC)
def as_local(dattim):
@ -58,17 +58,28 @@ def as_local(dattim):
if dattim.tzinfo == DEFAULT_TIME_ZONE:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=pytz.utc)
dattim = dattim.replace(tzinfo=UTC)
return dattim.astimezone(DEFAULT_TIME_ZONE)
def utc_from_timestamp(timestamp):
""" Returns a UTC time from a timestamp. """
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=pytz.utc)
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC)
def datetime_to_local_str(dattim, time_zone=None):
def start_of_local_day(dt_or_d=None):
""" Return local datetime object of start of day from date or datetime. """
if dt_or_d is None:
dt_or_d = now().date()
elif isinstance(dt_or_d, dt.datetime):
dt_or_d = dt_or_d.date()
return dt.datetime.combine(dt_or_d, dt.time()).replace(
tzinfo=DEFAULT_TIME_ZONE)
def datetime_to_local_str(dattim):
""" Converts datetime to specified time_zone and returns a string. """
return datetime_to_str(as_local(dattim))
@ -76,27 +87,27 @@ def datetime_to_local_str(dattim, time_zone=None):
def datetime_to_str(dattim):
""" Converts datetime to a string format.
@rtype : str
"""
return dattim.strftime(DATETIME_STR_FORMAT)
def datetime_to_time_str(dattim):
""" Converts datetime to a string containing only the time.
@rtype : str
"""
return dattim.strftime(TIME_STR_FORMAT)
def datetime_to_date_str(dattim):
""" Converts datetime to a string containing only the date.
@rtype : str
"""
return dattim.strftime(DATE_STR_FORMAT)
def datetime_to_short_time_str(dattim):
""" Converts datetime to a string format as short time.
@rtype : str
"""
return dattim.strftime(TIME_SHORT_STR_FORMAT)
def datetime_to_short_date_str(dattim):
""" Converts datetime to a string format as short date.
@rtype : str
"""
return dattim.strftime(DATE_SHORT_STR_FORMAT)
def str_to_datetime(dt_str):
""" Converts a string to a UTC datetime object.
@ -104,7 +115,15 @@ def str_to_datetime(dt_str):
"""
try:
return dt.datetime.strptime(
dt_str, DATE_STR_FORMAT).replace(tzinfo=pytz.utc)
dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc)
except ValueError: # If dt_str did not match our format
return None
def date_str_to_date(dt_str):
""" Converts a date string to a date object. """
try:
return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date()
except ValueError: # If dt_str did not match our format
return None

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