mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Merge pull request #202 from balloob/dev
Update master with latest changes
This commit is contained in:
commit
33b7585e0c
@ -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
4
.gitignore
vendored
@ -65,3 +65,7 @@ nosetests.xml
|
||||
.pydevproject
|
||||
|
||||
.python-version
|
||||
|
||||
# venv stuff
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
|
@ -1,3 +1,4 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- "3.4"
|
||||
|
57
README.md
57
README.md
@ -1,50 +1,45 @@
|
||||
# Home Assistant [](https://travis-ci.org/balloob/home-assistant) [](https://coveralls.io/r/balloob/home-assistant?branch=master) [](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.
|
||||
|
||||
[](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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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')
|
||||
|
143
homeassistant/components/arduino.py
Normal file
143
homeassistant/components/arduino.py
Normal 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()
|
227
homeassistant/components/camera/__init__.py
Normal file
227
homeassistant/components/camera/__init__.py
Normal 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)
|
||||
}
|
94
homeassistant/components/camera/generic.py
Normal file
94
homeassistant/components/camera/generic.py
Normal 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
|
@ -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"
|
||||
]
|
||||
})
|
||||
|
||||
|
117
homeassistant/components/device_tracker/tplink.py
Executable file
117
homeassistant/components/device_tracker/tplink.py
Executable 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
|
@ -22,6 +22,7 @@ from homeassistant.const import (
|
||||
|
||||
DOMAIN = "discovery"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['zeroconf>=0.16.0']
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
@ -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",
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -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});
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -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});
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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});
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -1,5 +1,7 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../resources/lodash.html">
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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];
|
||||
},
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -80,7 +80,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
eventActions.fire(this.eventType, eventData);
|
||||
eventActions.fireEvent(this.eventType, eventData);
|
||||
},
|
||||
|
||||
computeFormClasses: function(narrow) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -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 ?
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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";
|
||||
|
@ -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>
|
@ -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) {
|
||||
|
@ -0,0 +1,2 @@
|
||||
<script src="../bower_components/pikaday/pikaday.js"></script>
|
||||
<link href="../bower_components/pikaday/css/pikaday.css" media="all" rel="stylesheet" />
|
@ -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
@ -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())
|
||||
|
@ -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
|
||||
|
||||
|
@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
|
||||
DOMAIN = "keyboard"
|
||||
DEPENDENCIES = []
|
||||
REQUIREMENTS = ['pyuserinput>=0.1.9']
|
||||
|
||||
|
||||
def volume_up(hass):
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
307
homeassistant/components/media_player/kodi.py
Normal file
307
homeassistant/components/media_player/kodi.py
Normal 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()
|
@ -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:
|
||||
|
78
homeassistant/components/notify/file.py
Normal file
78
homeassistant/components/notify/file.py
Normal 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)
|
@ -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):
|
||||
|
@ -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__)
|
||||
|
||||
|
||||
|
@ -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. """
|
||||
|
87
homeassistant/components/sensor/arduino.py
Normal file
87
homeassistant/components/sensor/arduino.py
Normal 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]
|
@ -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)
|
||||
|
138
homeassistant/components/sensor/efergy.py
Normal file
138
homeassistant/components/sensor/efergy.py
Normal 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'
|
224
homeassistant/components/sensor/forecast.py
Normal file
224
homeassistant/components/sensor/forecast.py
Normal 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()
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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']))
|
||||
)
|
||||
|
@ -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())
|
||||
|
@ -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(':')
|
||||
|
@ -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'],
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
93
homeassistant/components/switch/arduino.py
Normal file
93
homeassistant/components/switch/arduino.py
Normal 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)
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -54,6 +54,7 @@ from transmissionrpc.error import TransmissionError
|
||||
import logging
|
||||
|
||||
_LOGGING = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['transmissionrpc>=0.11']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -93,4 +93,4 @@ class WinkToggleDevice(ToggleEntity):
|
||||
|
||||
def update(self):
|
||||
""" Update state of the light. """
|
||||
self.wink.wait_till_desired_reached()
|
||||
self.wink.updateState()
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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={}'
|
||||
|
@ -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
|
||||
|
41
homeassistant/util/color.py
Normal file
41
homeassistant/util/color.py
Normal 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)
|
@ -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
Loading…
x
Reference in New Issue
Block a user