mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 23:57:06 +00:00
commit
dc0f16c9dd
11
.coveragerc
11
.coveragerc
@ -3,6 +3,7 @@ source = homeassistant
|
||||
|
||||
omit =
|
||||
homeassistant/__main__.py
|
||||
homeassistant/scripts/*.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/apcupsd.py
|
||||
@ -87,8 +88,13 @@ omit =
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/switch/knx.py
|
||||
homeassistant/components/binary_sensor/knx.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
@ -120,6 +126,7 @@ omit =
|
||||
homeassistant/components/garage_door/rpi_gpio.py
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/blinksticklight.py
|
||||
homeassistant/components/light/hue.py
|
||||
@ -156,6 +163,7 @@ omit =
|
||||
homeassistant/components/notify/gntp.py
|
||||
homeassistant/components/notify/googlevoice.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
@ -185,6 +193,7 @@ omit =
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
@ -209,6 +218,7 @@ omit =
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
@ -220,6 +230,7 @@ omit =
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/thermostat/eq3btsmart.py
|
||||
|
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
5
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -15,7 +15,7 @@
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||
|
||||
If code communicates with devices:
|
||||
If code communicates with devices, web services, or a:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||
@ -26,8 +26,5 @@ If the code does not interact with devices:
|
||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
[fork]: http://stackoverflow.com/a/7244456
|
||||
[squash]: https://github.com/ginatrapani/todo.txt-android/wiki/Squash-All-Commits-Related-to-a-Single-Issue-into-a-Single-Commit
|
||||
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L16
|
||||
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L51
|
||||
|
||||
|
@ -9,79 +9,5 @@ The process is straight-forward.
|
||||
- Ensure tests work.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
||||
|
||||
Still interested? Then you should read the next sections and get more details.
|
||||
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
|
||||
|
||||
## Adding support for a new device
|
||||
|
||||
For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/).
|
||||
|
||||
After you finish adding support for your device:
|
||||
|
||||
- Check that all dependencies are included via the `REQUIREMENTS` variable in your platform/component and only imported inside functions that use them.
|
||||
- Add any new dependencies to `requirements_all.txt` if needed. Use `script/gen_requirements_all.py`.
|
||||
- Update the `.coveragerc` file to exclude your platform if there are no tests available or your new code uses a 3rd party library for communication with the device/service/sensor.
|
||||
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). It's OK to just add a docstring with configuration details (sample entry for `configuration.yaml` file and alike) to the file header as a start. Visit the [website documentation](https://home-assistant.io/developers/website/) for further information on contributing to [home-assistant.io](https://github.com/home-assistant/home-assistant.io).
|
||||
- Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `tox` or `script/lint`.
|
||||
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
||||
- Check for comments and suggestions on your Pull Request and keep an eye on the [CI output](https://travis-ci.org/home-assistant/home-assistant/).
|
||||
|
||||
If you add a platform for an existing component, there is usually no need for updating the frontend. Only if you've added a new component that should show up in the frontend, there are more steps needed:
|
||||
|
||||
- Update the file [`home-assistant-icons.html`](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html) with an icon for your domain ([pick one from this list](https://www.polymer-project.org/1.0/components/core-elements/demo.html#core-icon)).
|
||||
- Update the demo component with two states that it provides.
|
||||
- Add your component to `home-assistant.conf.example`.
|
||||
|
||||
Since you've updated `home-assistant-icons.html`, you've made changes to the frontend:
|
||||
|
||||
- Run `script/build_frontend`. This will build a new version of the frontend. Make sure you add the changed files `frontend.py` and `frontend.html` to the commit.
|
||||
|
||||
### Setting states
|
||||
|
||||
It is the responsibility of the component to maintain the states of the devices in your domain. Each device should be a single state and, if possible, a group should be provided that tracks the combined state of the devices.
|
||||
|
||||
A state can have several attributes that will help the frontend in displaying your state:
|
||||
|
||||
- `friendly_name`: this name will be used as the name of the device
|
||||
- `entity_picture`: this picture will be shown instead of the domain icon
|
||||
- `unit_of_measurement`: this will be appended to the state in the interface
|
||||
- `hidden`: This is a suggestion to the frontend on if the state should be hidden
|
||||
|
||||
These attributes are defined in [homeassistant.components](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/__init__.py#L25).
|
||||
|
||||
### Proper Visibility Handling
|
||||
|
||||
Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/helpers/entity.py) class. If this is done, visibility will be handled for you.
|
||||
You can set a suggestion for your entity's visibility by setting the hidden property by doing something similar to the following.
|
||||
|
||||
```python
|
||||
self.hidden = True
|
||||
```
|
||||
|
||||
This will SUGGEST that the active frontend hides the entity. This requires that the active frontend support hidden cards (the default frontend does) and that the value of hidden be included in your attributes dictionary (see above). The Entity abstract class will take care of this for you.
|
||||
|
||||
Remember: The suggestion set by your component's code will always be overwritten by user settings in the configuration.yaml file. This is why you may set hidden to be False, but the property may remain True (or vice-versa).
|
||||
|
||||
### Working on the frontend
|
||||
|
||||
The frontend is composed of [Polymer](https://www.polymer-project.org) web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the *http-component* in your config.
|
||||
|
||||
When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works.
|
||||
|
||||
## Testing your code
|
||||
|
||||
To test your code before submission, used the `tox` tool.
|
||||
|
||||
```bash
|
||||
> pip install -U tox
|
||||
> tox
|
||||
```
|
||||
|
||||
This will run unit tests against python 3.4 and 3.5 (if both are available locally), as well as run a set of tests which validate `pep8` and `pylint` style of the code.
|
||||
|
||||
You can optionally run tests on only one tox target using the `-e` option to select an environment.
|
||||
|
||||
For instance `tox -e lint` will run the linters only, `tox -e py34` will run unit tests only on python 3.4.
|
||||
|
||||
### Notes on PyLint and PEP8 validation
|
||||
|
||||
In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change.
|
||||
|
12
README.rst
12
README.rst
@ -18,7 +18,7 @@ tutorials and documentation.
|
||||
|
||||
|screenshot-states|
|
||||
|
||||
Examples of devices it can interface it:
|
||||
Examples of devices Home Assistant can interface with:
|
||||
|
||||
- Monitoring connected devices to a wireless router:
|
||||
`OpenWrt <https://openwrt.org/>`__,
|
||||
@ -61,11 +61,11 @@ Examples of devices it can interface it:
|
||||
- `See full list of supported
|
||||
devices <https://home-assistant.io/components/>`__
|
||||
|
||||
Built home automation on top of your devices:
|
||||
Build 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 on the lights when people get home after sunset
|
||||
- Turn on lights slowly during sunset to compensate for less light
|
||||
- Turn off all lights and devices when everybody leaves the house
|
||||
- Offers a `REST API <https://home-assistant.io/developers/api/>`__
|
||||
and can interface with MQTT for easy integration with other projects
|
||||
@ -75,10 +75,10 @@ Built home automation on top of your devices:
|
||||
(NMA) <http://www.notifymyandroid.com/>`__,
|
||||
`PushBullet <https://www.pushbullet.com/>`__,
|
||||
`PushOver <https://pushover.net/>`__, `Slack <https://slack.com/>`__,
|
||||
`Telegram <https://telegram.org/>`__, and `Jabber
|
||||
`Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber
|
||||
(XMPP) <http://xmpp.org>`__
|
||||
|
||||
The system is built modular so support for other devices or actions can
|
||||
The system is built using a modular approach so support for other devices or actions can
|
||||
be implemented easily. See also the `section on
|
||||
architecture <https://home-assistant.io/developers/architecture/>`__
|
||||
and the `section on creating your own
|
||||
|
@ -7,6 +7,9 @@ homeassistant:
|
||||
latitude: 32.87336
|
||||
longitude: 117.22743
|
||||
|
||||
# Impacts weather/sunrise data
|
||||
elevation: 665
|
||||
|
||||
# C for Celsius, F for Fahrenheit
|
||||
temperature_unit: C
|
||||
|
||||
@ -22,8 +25,8 @@ http:
|
||||
# Set to 1 to enable development mode
|
||||
# development: 1
|
||||
|
||||
# Enable the frontend
|
||||
frontend:
|
||||
# enable the frontend
|
||||
|
||||
light:
|
||||
# platform: hue
|
||||
@ -33,17 +36,12 @@ wink:
|
||||
access_token: 'YOUR_TOKEN'
|
||||
|
||||
device_tracker:
|
||||
# The following types are available: ddwrt, netgear, tomato, luci,
|
||||
# and nmap_tracker
|
||||
# The following tracker are available:
|
||||
# https://home-assistant.io/components/#presence-detection
|
||||
platform: netgear
|
||||
host: 192.168.1.1
|
||||
username: admin
|
||||
password: PASSWORD
|
||||
# http_id is needed for Tomato routers only
|
||||
# http_id: ABCDEFGHH
|
||||
# For nmap_tracker, only the IP addresses to scan are needed:
|
||||
# hosts: 192.168.1.1/24 # netmask prefix notation or
|
||||
# hosts: 192.168.1.1-255 # address range
|
||||
|
||||
chromecast:
|
||||
|
||||
@ -74,24 +72,25 @@ device_sun_light_trigger:
|
||||
# A comma separated list of states that have to be tracked as a single group
|
||||
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
|
||||
# You can also have groups within groups.
|
||||
# https://home-assistant.io/components/group/
|
||||
group:
|
||||
Home:
|
||||
- group.living_room
|
||||
- group.kitchen
|
||||
living_room:
|
||||
- light.Bowl
|
||||
- light.Ceiling
|
||||
- light.TV_back_light
|
||||
kitchen:
|
||||
- light.fan_bulb_1
|
||||
- light.fan_bulb_2
|
||||
children:
|
||||
- device_tracker.child_1
|
||||
- device_tracker.child_2
|
||||
default_view:
|
||||
view: yes
|
||||
entities:
|
||||
- group.awesome_people
|
||||
- group.climate
|
||||
|
||||
process:
|
||||
# items are which processes to look for: <entity_id>: <search string within ps>
|
||||
xbmc: XBMC.App
|
||||
kitchen:
|
||||
name: Kitchen
|
||||
entities:
|
||||
- switch.kitchen_pin_3
|
||||
upstairs:
|
||||
name: Kids
|
||||
icon: mdi:account-multiple
|
||||
view: yes
|
||||
entities:
|
||||
- input_boolean.notify_home
|
||||
- camera.demo_camera
|
||||
|
||||
example:
|
||||
|
||||
@ -105,6 +104,7 @@ browser:
|
||||
|
||||
keyboard:
|
||||
|
||||
# https://home-assistant.io/getting-started/automation/
|
||||
automation:
|
||||
- alias: 'Rule 1 Light on in the evening'
|
||||
trigger:
|
||||
@ -126,7 +126,6 @@ automation:
|
||||
entity_id: group.living_room
|
||||
|
||||
- alias: 'Rule 2 - Away Mode'
|
||||
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
@ -139,6 +138,14 @@ automation:
|
||||
|
||||
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
|
||||
# Each sensor label should be unique or your sensors might not load correctly.
|
||||
# Another way to do is to collect all entries under one "sensor:"
|
||||
# sensor:
|
||||
# - platform: mqtt
|
||||
# name: "MQTT Sensor 1"
|
||||
# - platform: mqtt
|
||||
# name: "MQTT Sensor 2"
|
||||
#
|
||||
# Details: https://home-assistant.io/getting-started/devices/
|
||||
|
||||
sensor:
|
||||
platform: systemmonitor
|
||||
@ -149,14 +156,6 @@ sensor:
|
||||
arg: '/home'
|
||||
- type: 'disk_use'
|
||||
arg: '/home'
|
||||
- type: 'disk_free'
|
||||
arg: '/'
|
||||
- type: 'memory_use_percent'
|
||||
- type: 'memory_use'
|
||||
- type: 'memory_free'
|
||||
- type: 'processor_use'
|
||||
- type: 'process'
|
||||
arg: 'octave-cli'
|
||||
|
||||
sensor 2:
|
||||
platform: forecast
|
||||
@ -166,14 +165,6 @@ sensor 2:
|
||||
- precip_type
|
||||
- precip_intensity
|
||||
- temperature
|
||||
- dew_point
|
||||
- wind_speed
|
||||
- wind_bearing
|
||||
- cloud_cover
|
||||
- humidity
|
||||
- pressure
|
||||
- visibility
|
||||
- ozone
|
||||
|
||||
script:
|
||||
# Turns on the bedroom lights and then the living room lights 1 minute later
|
||||
|
@ -7,7 +7,6 @@ import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
from homeassistant.const import (
|
||||
__version__,
|
||||
@ -110,22 +109,14 @@ def get_arguments():
|
||||
type=int,
|
||||
default=None,
|
||||
help='Enables daily log rotation and keeps up to the specified days')
|
||||
parser.add_argument(
|
||||
'--install-osx',
|
||||
action='store_true',
|
||||
help='Installs as a service on OS X and loads on boot.')
|
||||
parser.add_argument(
|
||||
'--uninstall-osx',
|
||||
action='store_true',
|
||||
help='Uninstalls from OS X.')
|
||||
parser.add_argument(
|
||||
'--restart-osx',
|
||||
action='store_true',
|
||||
help='Restarts on OS X.')
|
||||
parser.add_argument(
|
||||
'--runner',
|
||||
action='store_true',
|
||||
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
|
||||
parser.add_argument(
|
||||
'--script',
|
||||
nargs=argparse.REMAINDER,
|
||||
help='Run one of the embedded scripts')
|
||||
if os.name == "posix":
|
||||
parser.add_argument(
|
||||
'--daemon',
|
||||
@ -196,46 +187,6 @@ def write_pid(pid_file):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def install_osx():
|
||||
"""Setup to run via launchd on OS X."""
|
||||
with os.popen('which hass') as inp:
|
||||
hass_path = inp.read().strip()
|
||||
|
||||
with os.popen('whoami') as inp:
|
||||
user = inp.read().strip()
|
||||
|
||||
cwd = os.path.dirname(__file__)
|
||||
template_path = os.path.join(cwd, 'startup', 'launchd.plist')
|
||||
|
||||
with open(template_path, 'r', encoding='utf-8') as inp:
|
||||
plist = inp.read()
|
||||
|
||||
plist = plist.replace("$HASS_PATH$", hass_path)
|
||||
plist = plist.replace("$USER$", user)
|
||||
|
||||
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
|
||||
|
||||
try:
|
||||
with open(path, 'w', encoding='utf-8') as outp:
|
||||
outp.write(plist)
|
||||
except IOError as err:
|
||||
print('Unable to write to ' + path, err)
|
||||
return
|
||||
|
||||
os.popen('launchctl load -w -F ' + path)
|
||||
|
||||
print("Home Assistant has been installed. \
|
||||
Open it here: http://localhost:8123")
|
||||
|
||||
|
||||
def uninstall_osx():
|
||||
"""Unload from launchd on OS X."""
|
||||
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
|
||||
os.popen('launchctl unload ' + path)
|
||||
|
||||
print("Home Assistant has been uninstalled.")
|
||||
|
||||
|
||||
def closefds_osx(min_fd, max_fd):
|
||||
"""Make sure file descriptors get closed when we restart.
|
||||
|
||||
@ -358,23 +309,13 @@ def main():
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
if args.script is not None:
|
||||
from homeassistant import scripts
|
||||
return scripts.run(args.script)
|
||||
|
||||
config_dir = os.path.join(os.getcwd(), args.config)
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# OS X launchd functions
|
||||
if args.install_osx:
|
||||
install_osx()
|
||||
return 0
|
||||
if args.uninstall_osx:
|
||||
uninstall_osx()
|
||||
return 0
|
||||
if args.restart_osx:
|
||||
uninstall_osx()
|
||||
# A small delay is needed on some systems to let the unload finish.
|
||||
time.sleep(0.5)
|
||||
install_osx()
|
||||
return 0
|
||||
|
||||
# Daemon functions
|
||||
if args.pid_file:
|
||||
check_pid(args.pid_file)
|
||||
|
124
homeassistant/components/alarm_control_panel/simplisafe.py
Normal file
124
homeassistant/components/alarm_control_panel/simplisafe.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""
|
||||
Interfaces with SimpliSafe alarm control panel.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['https://github.com/w1ll1am23/simplisafe-python/archive/'
|
||||
'586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#'
|
||||
'simplisafe-python==0.0.1']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is None or password is None:
|
||||
_LOGGER.error('Must specify username and password!')
|
||||
return False
|
||||
|
||||
add_devices([SimpliSafeAlarm(
|
||||
config.get('name', "SimpliSafe"),
|
||||
username,
|
||||
password,
|
||||
config.get('code'))])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, name, username, password, code):
|
||||
"""Initialize the SimpliSafe alarm."""
|
||||
from simplisafe import SimpliSafe
|
||||
self.simplisafe = SimpliSafe(username, password)
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
self._id = self.simplisafe.get_id()
|
||||
status = self.simplisafe.get_state()
|
||||
if status == 'Off':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif status == 'Home':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'Away':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll the SimpliSafe API."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return 'Alarm {}'.format(self._id)
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""One or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
self.simplisafe.get_location()
|
||||
status = self.simplisafe.get_state()
|
||||
|
||||
if status == 'Off':
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif status == 'Home':
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'Away':
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
self.simplisafe.set_state('off')
|
||||
_LOGGER.info('SimpliSafe alarm disarming')
|
||||
self.update()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
self.simplisafe.set_state('home')
|
||||
_LOGGER.info('SimpliSafe alarm arming home')
|
||||
self.update()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
return
|
||||
self.simplisafe.set_state('away')
|
||||
_LOGGER.info('SimpliSafe alarm arming away')
|
||||
self.update()
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered for %s', state)
|
||||
return check
|
@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink sensor devices."""
|
||||
"""Setup Envisalink binary sensor devices."""
|
||||
_configured_zones = discovery_info['zones']
|
||||
for zone_num in _configured_zones:
|
||||
_device_config_data = ZONE_SCHEMA(_configured_zones[zone_num])
|
||||
@ -33,7 +33,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""Representation of an envisalink Binary Sensor."""
|
||||
"""Representation of an Envisalink binary sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
||||
|
24
homeassistant/components/binary_sensor/knx.py
Normal file
24
homeassistant/components/binary_sensor/knx.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""
|
||||
Contains functionality to use a KNX group address as a binary.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.knx/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.knx import (
|
||||
KNXConfig, KNXGroupAddress)
|
||||
|
||||
DEPENDENCIES = ["knx"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup the KNX binary sensor platform."""
|
||||
add_entities([
|
||||
KNXSwitch(hass, KNXConfig(config))
|
||||
])
|
||||
|
||||
|
||||
class KNXSwitch(KNXGroupAddress, BinarySensorDevice):
|
||||
"""Representation of a KNX binary sensor device."""
|
||||
|
||||
pass
|
@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup nx584 sensors."""
|
||||
"""Setup nx584 binary sensor platform."""
|
||||
from nx584 import client as nx584_client
|
||||
|
||||
host = config.get('host', 'localhost:5007')
|
||||
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/sensor.wink/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
@ -12,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
@ -24,7 +25,7 @@ SENSOR_TYPES = {
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink platform."""
|
||||
"""Setup the Wink binary sensor platform."""
|
||||
import pywink
|
||||
|
||||
if discovery_info is None:
|
||||
@ -42,9 +43,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
|
||||
for key in pywink.get_keys():
|
||||
add_devices([WinkBinarySensorDevice(key)])
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink sensor."""
|
||||
"""Representation of a Wink binary sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
@ -53,6 +57,14 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
self._unit_of_measurement = self.wink.UNIT
|
||||
self.capability = self.wink.capability()
|
||||
|
||||
def _pubnub_update(self, message, channel):
|
||||
if 'data' in message:
|
||||
json_data = json.dumps(message.get('data'))
|
||||
else:
|
||||
json_data = message
|
||||
self.wink.pubnub_update(json.loads(json_data))
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
@ -12,7 +12,7 @@ DEPENDENCIES = ["zigbee"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
"""Setup the ZigBee binary sensor platform."""
|
||||
add_entities([
|
||||
ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))
|
||||
])
|
||||
|
@ -8,6 +8,7 @@ import logging
|
||||
import datetime
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
@ -31,7 +32,7 @@ DEVICE_MAPPINGS = {
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Z-Wave platform for sensors."""
|
||||
"""Setup the Z-Wave platform for binary sensors."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
@ -61,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
|
||||
|
||||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
"""Representation of a binary sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, sensor_class):
|
||||
@ -97,7 +98,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
self.update_ha_state()
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
||||
"""Representation of a stateless sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, sensor_value, sensor_class, hass, re_arm_sec=60):
|
||||
|
@ -18,7 +18,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class DemoCamera(Camera):
|
||||
"""A Demo camera."""
|
||||
"""The representation of a Demo camera."""
|
||||
|
||||
def __init__(self, name):
|
||||
"""Initialize demo camera component."""
|
||||
|
@ -89,7 +89,7 @@ class WelcomeData(object):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.update()
|
||||
if not self.home:
|
||||
for home in self.welcomedata.cameras.keys():
|
||||
for home in self.welcomedata.cameras:
|
||||
for camera in self.welcomedata.cameras[home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
else:
|
||||
|
@ -27,7 +27,7 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.10.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.11.0']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@ -67,8 +67,8 @@ def setup(hass, config):
|
||||
}, blocking=True)
|
||||
|
||||
else:
|
||||
logger.error(
|
||||
'Got unsupported command %s from text %s', command, text)
|
||||
logger.error('Got unsupported command %s from text %s',
|
||||
command, text)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PROCESS, process,
|
||||
schema=SERVICE_PROCESS_SCHEMA)
|
||||
|
@ -377,12 +377,16 @@ def load_config(path, hass, consider_home, home_range):
|
||||
"""Load devices from YAML configuration file."""
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
return [
|
||||
Device(hass, consider_home, home_range, device.get('track', False),
|
||||
str(dev_id).lower(), str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
for dev_id, device in load_yaml_config_file(path).items()]
|
||||
try:
|
||||
return [
|
||||
Device(hass, consider_home, home_range, device.get('track', False),
|
||||
str(dev_id).lower(), str(device.get('mac')).upper(),
|
||||
device.get('name'), device.get('picture'),
|
||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
||||
for dev_id, device in load_yaml_config_file(path).items()]
|
||||
except HomeAssistantError:
|
||||
# When YAML file could not be loaded/did not contain a dict
|
||||
return []
|
||||
|
||||
|
||||
def setup_scanner_platform(hass, config, scanner, see_device):
|
||||
|
@ -62,8 +62,9 @@ def get_scanner(hass, config):
|
||||
_LOGGER):
|
||||
return None
|
||||
elif CONF_PASSWORD not in config[DOMAIN] and \
|
||||
'ssh_key' not in config[DOMAIN] and \
|
||||
'pub_key' not in config[DOMAIN]:
|
||||
_LOGGER.error("Either a public key or password must be provided")
|
||||
_LOGGER.error('Either a private key or password must be provided')
|
||||
return None
|
||||
|
||||
scanner = AsusWrtDeviceScanner(config[DOMAIN])
|
||||
@ -83,8 +84,8 @@ class AsusWrtDeviceScanner(object):
|
||||
"""Initialize the scanner."""
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = str(config[CONF_USERNAME])
|
||||
self.password = str(config.get(CONF_PASSWORD, ""))
|
||||
self.pub_key = str(config.get('pub_key', ""))
|
||||
self.password = str(config.get(CONF_PASSWORD, ''))
|
||||
self.ssh_key = str(config.get('ssh_key', config.get('pub_key', '')))
|
||||
self.protocol = config.get('protocol')
|
||||
self.mode = config.get('mode')
|
||||
|
||||
@ -120,7 +121,7 @@ class AsusWrtDeviceScanner(object):
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info("Checking ARP")
|
||||
_LOGGER.info('Checking ARP')
|
||||
data = self.get_asuswrt_data()
|
||||
if not data:
|
||||
return False
|
||||
@ -138,12 +139,12 @@ class AsusWrtDeviceScanner(object):
|
||||
|
||||
try:
|
||||
ssh = pxssh.pxssh()
|
||||
if self.pub_key:
|
||||
ssh.login(self.host, self.username, ssh_key=self.pub_key)
|
||||
if self.ssh_key:
|
||||
ssh.login(self.host, self.username, ssh_key=self.ssh_key)
|
||||
elif self.password:
|
||||
ssh.login(self.host, self.username, self.password)
|
||||
else:
|
||||
_LOGGER.error('No password or public key specified')
|
||||
_LOGGER.error('No password or private key specified')
|
||||
return None
|
||||
ssh.sendline(_IP_NEIGH_CMD)
|
||||
ssh.prompt()
|
||||
@ -195,16 +196,16 @@ class AsusWrtDeviceScanner(object):
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
except EOFError:
|
||||
_LOGGER.error("Unexpected response from router")
|
||||
_LOGGER.error('Unexpected response from router')
|
||||
return None
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.error("Connection refused by router, is telnet enabled?")
|
||||
_LOGGER.error('Connection refused by router, is telnet enabled?')
|
||||
return None
|
||||
except socket.gaierror as exc:
|
||||
_LOGGER.error("Socket exception: %s", exc)
|
||||
_LOGGER.error('Socket exception: %s', exc)
|
||||
return None
|
||||
except OSError as exc:
|
||||
_LOGGER.error("OSError: %s", exc)
|
||||
_LOGGER.error('OSError: %s', exc)
|
||||
return None
|
||||
|
||||
def get_asuswrt_data(self):
|
||||
@ -232,7 +233,7 @@ class AsusWrtDeviceScanner(object):
|
||||
match = _WL_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse wl row: %s", lease)
|
||||
_LOGGER.warning('Could not parse wl row: %s', lease)
|
||||
continue
|
||||
|
||||
host = ''
|
||||
@ -242,7 +243,7 @@ class AsusWrtDeviceScanner(object):
|
||||
if match.group('mac').lower() in arp.decode('utf-8'):
|
||||
arp_match = _ARP_REGEX.search(arp.decode('utf-8'))
|
||||
if not arp_match:
|
||||
_LOGGER.warning("Could not parse arp row: %s", arp)
|
||||
_LOGGER.warning('Could not parse arp row: %s', arp)
|
||||
continue
|
||||
|
||||
devices[arp_match.group('ip')] = {
|
||||
@ -256,7 +257,7 @@ class AsusWrtDeviceScanner(object):
|
||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse lease row: %s", lease)
|
||||
_LOGGER.warning('Could not parse lease row: %s', lease)
|
||||
continue
|
||||
|
||||
# For leases where the client doesn't set a hostname, ensure it
|
||||
@ -275,7 +276,7 @@ class AsusWrtDeviceScanner(object):
|
||||
for neighbor in result.neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
|
||||
_LOGGER.warning('Could not parse neighbor row: %s', neighbor)
|
||||
continue
|
||||
if match.group('ip') in devices:
|
||||
devices[match.group('ip')]['status'] = match.group('status')
|
||||
|
@ -16,6 +16,7 @@ REQUIREMENTS = ['urllib3', 'unifi==1.2.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
CONF_SITE_ID = 'site_id'
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
@ -32,6 +33,7 @@ def get_scanner(hass, config):
|
||||
host = this_config.get(CONF_HOST, 'localhost')
|
||||
username = this_config.get(CONF_USERNAME)
|
||||
password = this_config.get(CONF_PASSWORD)
|
||||
site_id = this_config.get(CONF_SITE_ID, 'default')
|
||||
|
||||
try:
|
||||
port = int(this_config.get(CONF_PORT, 8443))
|
||||
@ -40,7 +42,7 @@ def get_scanner(hass, config):
|
||||
return False
|
||||
|
||||
try:
|
||||
ctrl = Controller(host, username, password, port, 'v4')
|
||||
ctrl = Controller(host, username, password, port, 'v4', site_id)
|
||||
except urllib.error.HTTPError as ex:
|
||||
_LOGGER.error('Failed to connect to unifi: %s', ex)
|
||||
return False
|
||||
|
@ -149,7 +149,7 @@ def setup(hass, base_config):
|
||||
EVL_CONTROLLER.stop()
|
||||
|
||||
def start_envisalink(event):
|
||||
"""Startup process for the envisalink."""
|
||||
"""Startup process for the Envisalink."""
|
||||
EVL_CONTROLLER.start()
|
||||
for _ in range(10):
|
||||
if 'success' in _connect_status:
|
||||
@ -175,7 +175,7 @@ def setup(hass, base_config):
|
||||
if not _result:
|
||||
return False
|
||||
|
||||
# Load sub-components for envisalink
|
||||
# Load sub-components for Envisalink
|
||||
if _partitions:
|
||||
load_platform(hass, 'alarm_control_panel', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
@ -191,7 +191,7 @@ def setup(hass, base_config):
|
||||
|
||||
|
||||
class EnvisalinkDevice(Entity):
|
||||
"""Representation of an envisalink devicetity."""
|
||||
"""Representation of an Envisalink device."""
|
||||
|
||||
def __init__(self, name, info, controller):
|
||||
"""Initialize the device."""
|
||||
|
@ -1,9 +1,9 @@
|
||||
"""Handle the frontend for Home Assistant."""
|
||||
import os
|
||||
|
||||
from . import version, mdi_version
|
||||
from homeassistant.components import api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from . import version, mdi_version
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
@ -76,11 +76,17 @@ class IndexView(HomeAssistantView):
|
||||
def get(self, request, entity_id=None):
|
||||
"""Serve the index view."""
|
||||
if self.hass.wsgi.development:
|
||||
core_url = 'home-assistant-polymer/build/_core_compiled.js'
|
||||
ui_url = 'home-assistant-polymer/src/home-assistant.html'
|
||||
core_url = '/static/home-assistant-polymer/build/_core_compiled.js'
|
||||
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
||||
map_url = ('/static/home-assistant-polymer/src/layouts/'
|
||||
'partial-map.html')
|
||||
dev_url = ('/static/home-assistant-polymer/src/entry-points/'
|
||||
'dev-tools.html')
|
||||
else:
|
||||
core_url = 'core-{}.js'.format(version.CORE)
|
||||
ui_url = 'frontend-{}.html'.format(version.UI)
|
||||
core_url = '/static/core-{}.js'.format(version.CORE)
|
||||
ui_url = '/static/frontend-{}.html'.format(version.UI)
|
||||
map_url = '/static/partial-map-{}.html'.format(version.MAP)
|
||||
dev_url = '/static/dev-tools-{}.html'.format(version.DEV)
|
||||
|
||||
# auto login if no password was set
|
||||
if self.hass.config.api.api_password is None:
|
||||
@ -88,14 +94,14 @@ class IndexView(HomeAssistantView):
|
||||
else:
|
||||
auth = 'false'
|
||||
|
||||
icons_url = 'mdi-{}.html'.format(mdi_version.VERSION)
|
||||
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
|
||||
|
||||
template = self.templates.get_template('index.html')
|
||||
|
||||
# pylint is wrong
|
||||
# pylint: disable=no-member
|
||||
resp = template.render(
|
||||
core_url=core_url, ui_url=ui_url, auth=auth,
|
||||
icons_url=icons_url, icons=mdi_version.VERSION)
|
||||
core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth,
|
||||
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION)
|
||||
|
||||
return self.Response(resp, mimetype='text/html')
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
|
||||
VERSION = "9ee3d4466a65bef35c2c8974e91b37c0"
|
||||
VERSION = "758957b7ea989d6beca60e218ea7f7dd"
|
||||
|
@ -64,8 +64,12 @@
|
||||
document
|
||||
.getElementById('ha-init-skeleton')
|
||||
.classList.add('error');
|
||||
}
|
||||
window.noAuth = {{ auth }}
|
||||
};
|
||||
window.noAuth = {{ auth }};
|
||||
window.deferredLoading = {
|
||||
map: '{{ map_url }}',
|
||||
dev: '{{ dev_url }}',
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body fullbleed>
|
||||
@ -76,9 +80,9 @@
|
||||
</div>
|
||||
<home-assistant icons='{{ icons }}'></home-assistant>
|
||||
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
|
||||
<script src='/static/{{ core_url }}'></script>
|
||||
<link rel='import' href='/static/{{ ui_url }}' onerror='initError()' async>
|
||||
<link rel='import' href='/static/{{ icons_url }}' async>
|
||||
<script src='{{ core_url }}'></script>
|
||||
<link rel='import' href='{{ ui_url }}' onerror='initError()' async>
|
||||
<link rel='import' href='{{ icons_url }}' async>
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
'registerElement' in document &&
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
||||
CORE = "db0bb387f4d3bcace002d62b94baa348"
|
||||
UI = "5b306b7e7d36799b7b67f592cbe94703"
|
||||
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
|
||||
UI = "805f8dda70419b26daabc8e8f625127f"
|
||||
MAP = "c922306de24140afd14f857f927bf8f0"
|
||||
DEV = "b7079ac3121b95b9856e5603a6d8a263"
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
homeassistant/components/frontend/www_static/dev-tools.html.gz
Normal file
BIN
homeassistant/components/frontend/www_static/dev-tools.html.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
||||
Subproject commit 1e1a3a1c845713508d21d7c1cb87a7ecee6222aa
|
||||
Subproject commit 5e7f2fdbe849c43ba1c7dd647e5f948894c3118e
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
homeassistant/components/frontend/www_static/partial-map.html.gz
Normal file
BIN
homeassistant/components/frontend/www_static/partial-map.html.gz
Normal file
Binary file not shown.
@ -29,7 +29,7 @@
|
||||
|
||||
|
||||
/* eslint-disable quotes, comma-spacing */
|
||||
var PrecacheConfig = [["/","70eeeca780a5f23c7632c2876dd1795a"],["/devEvent","70eeeca780a5f23c7632c2876dd1795a"],["/devInfo","70eeeca780a5f23c7632c2876dd1795a"],["/devService","70eeeca780a5f23c7632c2876dd1795a"],["/devState","70eeeca780a5f23c7632c2876dd1795a"],["/devTemplate","70eeeca780a5f23c7632c2876dd1795a"],["/history","70eeeca780a5f23c7632c2876dd1795a"],["/logbook","70eeeca780a5f23c7632c2876dd1795a"],["/map","70eeeca780a5f23c7632c2876dd1795a"],["/states","70eeeca780a5f23c7632c2876dd1795a"],["/static/core-db0bb387f4d3bcace002d62b94baa348.js","f938163a392465dc87af3a0094376621"],["/static/frontend-5b306b7e7d36799b7b67f592cbe94703.html","70eeeca780a5f23c7632c2876dd1795a"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
|
||||
var PrecacheConfig = [["/","d2c67846acf9a583c29798c30503cbf1"],["/devEvent","c4cdd84093404ee3fe0896070ebde97f"],["/devInfo","c4cdd84093404ee3fe0896070ebde97f"],["/devService","c4cdd84093404ee3fe0896070ebde97f"],["/devState","c4cdd84093404ee3fe0896070ebde97f"],["/devTemplate","c4cdd84093404ee3fe0896070ebde97f"],["/history","d2c67846acf9a583c29798c30503cbf1"],["/logbook","d2c67846acf9a583c29798c30503cbf1"],["/map","df0c87260b6dd990477cda43a2440b1c"],["/states","d2c67846acf9a583c29798c30503cbf1"],["/static/core-7d80cc0e4dea6bc20fa2889be0b3cd15.js","1f35577e9f32a86a03944e5e8d15eab2"],["/static/dev-tools-b7079ac3121b95b9856e5603a6d8a263.html","4ba7c57b48c9d28a1e0d9d7624b83700"],["/static/frontend-805f8dda70419b26daabc8e8f625127f.html","d8eeb403baf5893de8404beec0135d96"],["/static/mdi-758957b7ea989d6beca60e218ea7f7dd.html","4c32b01a3a5b194630963ff7ec4df36f"],["/static/partial-map-c922306de24140afd14f857f927bf8f0.html","853772ea26ac2f4db0f123e20c1ca160"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
|
||||
/* eslint-enable quotes, comma-spacing */
|
||||
var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-';
|
||||
|
||||
|
Binary file not shown.
@ -57,7 +57,7 @@ class RPiGPIOGarageDoor(GarageDoorDevice):
|
||||
self._relay_pin = relay_pin
|
||||
self._state_pin = state_pin
|
||||
rpi_gpio.setup_output(self._relay_pin)
|
||||
rpi_gpio.setup_input(self._state_pin, 'DOWN')
|
||||
rpi_gpio.setup_input(self._state_pin, 'UP')
|
||||
rpi_gpio.write_output(self._relay_pin, True)
|
||||
|
||||
@property
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.garage_door import GarageDoorDevice
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -9,8 +9,8 @@ from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
|
||||
from homeassistant.components import recorder, script
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import recorder, script
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'history'
|
||||
@ -27,13 +27,12 @@ def last_5_states(entity_id):
|
||||
"""Return the last 5 states for entity_id."""
|
||||
entity_id = entity_id.lower()
|
||||
|
||||
query = """
|
||||
SELECT * FROM states WHERE entity_id=? AND
|
||||
last_changed=last_updated
|
||||
ORDER BY state_id DESC LIMIT 0, 5
|
||||
"""
|
||||
|
||||
return recorder.query_states(query, (entity_id, ))
|
||||
states = recorder.get_model('States')
|
||||
return recorder.execute(
|
||||
recorder.query('States').filter(
|
||||
(states.entity_id == entity_id) &
|
||||
(states.last_changed == states.last_updated)
|
||||
).order_by(states.state_id.desc()).limit(5))
|
||||
|
||||
|
||||
def get_significant_states(start_time, end_time=None, entity_id=None):
|
||||
@ -44,48 +43,42 @@ def get_significant_states(start_time, end_time=None, entity_id=None):
|
||||
as well as all states from certain domains (for instance
|
||||
thermostat so that we get current temperature in our graphs).
|
||||
"""
|
||||
where = """
|
||||
(domain IN ({}) OR last_changed=last_updated)
|
||||
AND domain NOT IN ({}) AND last_updated > ?
|
||||
""".format(",".join("'%s'" % x for x in SIGNIFICANT_DOMAINS),
|
||||
",".join("'%s'" % x for x in IGNORE_DOMAINS))
|
||||
|
||||
data = [start_time]
|
||||
states = recorder.get_model('States')
|
||||
query = recorder.query('States').filter(
|
||||
(states.domain.in_(SIGNIFICANT_DOMAINS) |
|
||||
(states.last_changed == states.last_updated)) &
|
||||
((~states.domain.in_(IGNORE_DOMAINS)) &
|
||||
(states.last_updated > start_time)))
|
||||
|
||||
if end_time is not None:
|
||||
where += "AND last_updated < ? "
|
||||
data.append(end_time)
|
||||
query = query.filter(states.last_updated < end_time)
|
||||
|
||||
if entity_id is not None:
|
||||
where += "AND entity_id = ? "
|
||||
data.append(entity_id.lower())
|
||||
query = query.filter_by(entity_id=entity_id.lower())
|
||||
|
||||
query = ("SELECT * FROM states WHERE {} "
|
||||
"ORDER BY entity_id, last_updated ASC").format(where)
|
||||
|
||||
states = (state for state in recorder.query_states(query, data)
|
||||
if _is_significant(state))
|
||||
states = (
|
||||
state for state in recorder.execute(
|
||||
query.order_by(states.entity_id, states.last_updated))
|
||||
if _is_significant(state))
|
||||
|
||||
return states_to_json(states, start_time, entity_id)
|
||||
|
||||
|
||||
def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
||||
"""Return states changes during UTC period start_time - end_time."""
|
||||
where = "last_changed=last_updated AND last_changed > ? "
|
||||
data = [start_time]
|
||||
states = recorder.get_model('States')
|
||||
query = recorder.query('States').filter(
|
||||
(states.last_changed == states.last_updated) &
|
||||
(states.last_changed > start_time))
|
||||
|
||||
if end_time is not None:
|
||||
where += "AND last_changed < ? "
|
||||
data.append(end_time)
|
||||
query = query.filter(states.last_updated < end_time)
|
||||
|
||||
if entity_id is not None:
|
||||
where += "AND entity_id = ? "
|
||||
data.append(entity_id.lower())
|
||||
query = query.filter_by(entity_id=entity_id.lower())
|
||||
|
||||
query = ("SELECT * FROM states WHERE {} "
|
||||
"ORDER BY entity_id, last_changed ASC").format(where)
|
||||
|
||||
states = recorder.query_states(query, data)
|
||||
states = recorder.execute(
|
||||
query.order_by(states.entity_id, states.last_updated))
|
||||
|
||||
return states_to_json(states, start_time, entity_id)
|
||||
|
||||
@ -99,24 +92,27 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
|
||||
if run is None:
|
||||
return []
|
||||
|
||||
where = run.where_after_start_run + "AND created < ? "
|
||||
where_data = [utc_point_in_time]
|
||||
from sqlalchemy import and_, func
|
||||
|
||||
states = recorder.get_model('States')
|
||||
most_recent_state_ids = recorder.query(
|
||||
func.max(states.state_id).label('max_state_id')
|
||||
).filter(
|
||||
(states.created >= run.start) &
|
||||
(states.created < utc_point_in_time)
|
||||
)
|
||||
|
||||
if entity_ids is not None:
|
||||
where += "AND entity_id IN ({}) ".format(
|
||||
",".join(['?'] * len(entity_ids)))
|
||||
where_data.extend(entity_ids)
|
||||
most_recent_state_ids = most_recent_state_ids.filter(
|
||||
states.entity_id.in_(entity_ids))
|
||||
|
||||
query = """
|
||||
SELECT * FROM states
|
||||
INNER JOIN (
|
||||
SELECT max(state_id) AS max_state_id
|
||||
FROM states WHERE {}
|
||||
GROUP BY entity_id)
|
||||
WHERE state_id = max_state_id
|
||||
""".format(where)
|
||||
most_recent_state_ids = most_recent_state_ids.group_by(
|
||||
states.entity_id).subquery()
|
||||
|
||||
return recorder.query_states(query, where_data)
|
||||
query = recorder.query('States').join(most_recent_state_ids, and_(
|
||||
states.state_id == most_recent_state_ids.c.max_state_id))
|
||||
|
||||
return recorder.execute(query)
|
||||
|
||||
|
||||
def states_to_json(states, start_time, entity_id):
|
||||
|
@ -4,71 +4,120 @@ Support for Homematic devices.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/homematic/
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from functools import partial
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN
|
||||
from homeassistant.helpers import discovery
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN,
|
||||
CONF_USERNAME, CONF_PASSWORD)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ['pyhomematic==0.1.8']
|
||||
REQUIREMENTS = ["pyhomematic==0.1.9"]
|
||||
|
||||
HOMEMATIC = None
|
||||
HOMEMATIC_LINK_DELAY = 0.5
|
||||
|
||||
DISCOVER_SWITCHES = "homematic.switch"
|
||||
DISCOVER_LIGHTS = "homematic.light"
|
||||
DISCOVER_SENSORS = "homematic.sensor"
|
||||
DISCOVER_BINARY_SENSORS = "homematic.binary_sensor"
|
||||
DISCOVER_ROLLERSHUTTER = "homematic.rollershutter"
|
||||
DISCOVER_THERMOSTATS = "homematic.thermostat"
|
||||
DISCOVER_SWITCHES = 'homematic.switch'
|
||||
DISCOVER_LIGHTS = 'homematic.light'
|
||||
DISCOVER_SENSORS = 'homematic.sensor'
|
||||
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
|
||||
DISCOVER_ROLLERSHUTTER = 'homematic.rollershutter'
|
||||
DISCOVER_THERMOSTATS = 'homematic.thermostat'
|
||||
|
||||
ATTR_DISCOVER_DEVICES = "devices"
|
||||
ATTR_PARAM = "param"
|
||||
ATTR_CHANNEL = "channel"
|
||||
ATTR_NAME = "name"
|
||||
ATTR_ADDRESS = "address"
|
||||
ATTR_DISCOVER_DEVICES = 'devices'
|
||||
ATTR_PARAM = 'param'
|
||||
ATTR_CHANNEL = 'channel'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_ADDRESS = 'address'
|
||||
|
||||
EVENT_KEYPRESS = "homematic.keypress"
|
||||
EVENT_KEYPRESS = 'homematic.keypress'
|
||||
EVENT_IMPULSE = 'homematic.impulse'
|
||||
|
||||
SERVICE_VIRTUALKEY = 'virtualkey'
|
||||
|
||||
HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"],
|
||||
DISCOVER_LIGHTS: ["Dimmer"],
|
||||
DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2",
|
||||
"RemoteMotion", "ThermostatWall", "AreaThermostat",
|
||||
"RotaryHandleSensor", "WaterSensor"],
|
||||
DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"],
|
||||
DISCOVER_BINARY_SENSORS: ["ShutterContact", "Smoke", "SmokeV2",
|
||||
"Motion", "MotionV2", "RemoteMotion"],
|
||||
DISCOVER_ROLLERSHUTTER: ["Blind"]
|
||||
DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'],
|
||||
DISCOVER_LIGHTS: ['Dimmer'],
|
||||
DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2',
|
||||
'RemoteMotion', 'ThermostatWall', 'AreaThermostat',
|
||||
'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas',
|
||||
'LuxSensor'],
|
||||
DISCOVER_THERMOSTATS: ['Thermostat', 'ThermostatWall', 'MAXThermostat'],
|
||||
DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion',
|
||||
'MotionV2', 'RemoteMotion'],
|
||||
DISCOVER_ROLLERSHUTTER: ['Blind']
|
||||
}
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE = [
|
||||
"ACTUAL_TEMPERATURE"
|
||||
'ACTUAL_TEMPERATURE'
|
||||
]
|
||||
|
||||
HM_ATTRIBUTE_SUPPORT = {
|
||||
"LOWBAT": ["Battery", {0: "High", 1: "Low"}],
|
||||
"ERROR": ["Sabotage", {0: "No", 1: "Yes"}],
|
||||
"RSSI_DEVICE": ["RSSI", {}],
|
||||
"VALVE_STATE": ["Valve", {}],
|
||||
"BATTERY_STATE": ["Battery", {}],
|
||||
"CONTROL_MODE": ["Mode", {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost"}],
|
||||
"POWER": ["Power", {}],
|
||||
"CURRENT": ["Current", {}],
|
||||
"VOLTAGE": ["Voltage", {}]
|
||||
'LOWBAT': ['Battery', {0: 'High', 1: 'Low'}],
|
||||
'ERROR': ['Sabotage', {0: 'No', 1: 'Yes'}],
|
||||
'RSSI_DEVICE': ['RSSI', {}],
|
||||
'VALVE_STATE': ['Valve', {}],
|
||||
'BATTERY_STATE': ['Battery', {}],
|
||||
'CONTROL_MODE': ['Mode', {0: 'Auto', 1: 'Manual', 2: 'Away', 3: 'Boost'}],
|
||||
'POWER': ['Power', {}],
|
||||
'CURRENT': ['Current', {}],
|
||||
'VOLTAGE': ['Voltage', {}]
|
||||
}
|
||||
|
||||
HM_PRESS_EVENTS = [
|
||||
"PRESS_SHORT",
|
||||
"PRESS_LONG",
|
||||
"PRESS_CONT",
|
||||
"PRESS_LONG_RELEASE"
|
||||
'PRESS_SHORT',
|
||||
'PRESS_LONG',
|
||||
'PRESS_CONT',
|
||||
'PRESS_LONG_RELEASE'
|
||||
]
|
||||
|
||||
HM_IMPULSE_EVENTS = [
|
||||
'SEQUENCE_OK'
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RESOLVENAMES_OPTIONS = [
|
||||
'metadata',
|
||||
'json',
|
||||
'xml',
|
||||
False
|
||||
]
|
||||
|
||||
CONF_LOCAL_IP = 'local_ip'
|
||||
CONF_LOCAL_PORT = 'local_port'
|
||||
CONF_REMOTE_IP = 'remote_ip'
|
||||
CONF_REMOTE_PORT = 'remote_port'
|
||||
CONF_RESOLVENAMES = 'resolvenames'
|
||||
CONF_DELAY = 'delay'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_LOCAL_IP): vol.Coerce(str),
|
||||
vol.Optional(CONF_LOCAL_PORT, default=8943):
|
||||
vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1, max=65535)),
|
||||
vol.Required(CONF_REMOTE_IP): vol.Coerce(str),
|
||||
vol.Optional(CONF_REMOTE_PORT, default=2001):
|
||||
vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_RESOLVENAMES, default=False):
|
||||
vol.In(CONF_RESOLVENAMES_OPTIONS),
|
||||
vol.Optional(CONF_USERNAME, default="Admin"): vol.Coerce(str),
|
||||
vol.Optional(CONF_PASSWORD, default=""): vol.Coerce(str),
|
||||
vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float)
|
||||
})
|
||||
|
||||
SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
|
||||
vol.Required(ATTR_ADDRESS): vol.Coerce(str),
|
||||
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
|
||||
vol.Required(ATTR_PARAM): vol.Coerce(str)
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
@ -77,14 +126,14 @@ def setup(hass, config):
|
||||
|
||||
from pyhomematic import HMConnection
|
||||
|
||||
local_ip = config[DOMAIN].get("local_ip", None)
|
||||
local_port = config[DOMAIN].get("local_port", 8943)
|
||||
remote_ip = config[DOMAIN].get("remote_ip", None)
|
||||
remote_port = config[DOMAIN].get("remote_port", 2001)
|
||||
resolvenames = config[DOMAIN].get("resolvenames", False)
|
||||
username = config[DOMAIN].get("username", "Admin")
|
||||
password = config[DOMAIN].get("password", "")
|
||||
HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5)
|
||||
local_ip = config[DOMAIN][0].get(CONF_LOCAL_IP)
|
||||
local_port = config[DOMAIN][0].get(CONF_LOCAL_PORT)
|
||||
remote_ip = config[DOMAIN][0].get(CONF_REMOTE_IP)
|
||||
remote_port = config[DOMAIN][0].get(CONF_REMOTE_PORT)
|
||||
resolvenames = config[DOMAIN][0].get(CONF_RESOLVENAMES)
|
||||
username = config[DOMAIN][0].get(CONF_USERNAME)
|
||||
password = config[DOMAIN][0].get(CONF_PASSWORD)
|
||||
HOMEMATIC_LINK_DELAY = config[DOMAIN][0].get(CONF_DELAY)
|
||||
|
||||
if remote_ip is None or local_ip is None:
|
||||
_LOGGER.error("Missing remote CCU/Homegear or local address")
|
||||
@ -109,6 +158,15 @@ def setup(hass, config):
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop)
|
||||
hass.config.components.append(DOMAIN)
|
||||
|
||||
# regeister homematic services
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY,
|
||||
_hm_service_virtualkey,
|
||||
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
|
||||
SCHEMA_SERVICE_VIRTUALKEY)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -302,7 +360,7 @@ def _hm_event_handler(hass, device, caller, attribute, value):
|
||||
_LOGGER.debug("Event %s for %s channel %i", attribute,
|
||||
hmdevice.NAME, channel)
|
||||
|
||||
# a keypress event
|
||||
# keypress event
|
||||
if attribute in HM_PRESS_EVENTS:
|
||||
hass.bus.fire(EVENT_KEYPRESS, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
@ -311,9 +369,42 @@ def _hm_event_handler(hass, device, caller, attribute, value):
|
||||
})
|
||||
return
|
||||
|
||||
# impulse event
|
||||
if attribute in HM_IMPULSE_EVENTS:
|
||||
hass.bus.fire(EVENT_KEYPRESS, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
ATTR_CHANNEL: channel
|
||||
})
|
||||
return
|
||||
|
||||
_LOGGER.warning("Event is unknown and not forwarded to HA")
|
||||
|
||||
|
||||
def _hm_service_virtualkey(call):
|
||||
"""Callback for handle virtualkey services."""
|
||||
address = call.data.get(ATTR_ADDRESS)
|
||||
channel = call.data.get(ATTR_CHANNEL)
|
||||
param = call.data.get(ATTR_PARAM)
|
||||
|
||||
if address not in HOMEMATIC.devices:
|
||||
_LOGGER.error("%s not found for service virtualkey!", address)
|
||||
return
|
||||
hmdevice = HOMEMATIC.devices.get(address)
|
||||
|
||||
# if param exists for this device
|
||||
if param not in hmdevice.ACTIONNODE:
|
||||
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
||||
return
|
||||
|
||||
# channel exists?
|
||||
if channel > hmdevice.ELEMENT:
|
||||
_LOGGER.error("%i is not a channel in hm device %s", channel, address)
|
||||
return
|
||||
|
||||
# call key
|
||||
hmdevice.actionNodeData(param, 1, channel)
|
||||
|
||||
|
||||
class HMDevice(Entity):
|
||||
"""The Homematic device base object."""
|
||||
|
||||
@ -465,7 +556,7 @@ class HMDevice(Entity):
|
||||
channel = self._channel
|
||||
# Prepare for subscription
|
||||
try:
|
||||
if int(channel) > 0:
|
||||
if int(channel) >= 0:
|
||||
channels_to_sub.update({int(channel): True})
|
||||
except (ValueError, TypeError):
|
||||
_LOGGER("Invalid channel in metadata from %s",
|
||||
|
80
homeassistant/components/joaoapps_join.py
Normal file
80
homeassistant/components/joaoapps_join.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""
|
||||
Component for Joaoapps Join services.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/join/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import CONF_NAME, CONF_API_KEY
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/nkgilley/python-join-api/archive/'
|
||||
'3e1e849f1af0b4080f551b62270c6d244d5fbcbd.zip#python-join-api==0.0.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'joaoapps_join'
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_API_KEY): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def setup(hass, config):
|
||||
"""Setup Join services."""
|
||||
from pyjoin import (get_devices, ring_device, set_wallpaper, send_sms,
|
||||
send_file, send_url, send_notification)
|
||||
device_id = config[DOMAIN].get(CONF_DEVICE_ID)
|
||||
api_key = config[DOMAIN].get(CONF_API_KEY)
|
||||
name = config[DOMAIN].get(CONF_NAME)
|
||||
if api_key:
|
||||
if not get_devices(api_key):
|
||||
_LOGGER.error("Error connecting to Join, check API key")
|
||||
return False
|
||||
|
||||
def ring_service(service):
|
||||
"""Service to ring devices."""
|
||||
ring_device(device_id, api_key=api_key)
|
||||
|
||||
def set_wallpaper_service(service):
|
||||
"""Service to set wallpaper on devices."""
|
||||
set_wallpaper(device_id, url=service.data.get('url'), api_key=api_key)
|
||||
|
||||
def send_file_service(service):
|
||||
"""Service to send files to devices."""
|
||||
send_file(device_id, url=service.data.get('url'), api_key=api_key)
|
||||
|
||||
def send_url_service(service):
|
||||
"""Service to open url on devices."""
|
||||
send_url(device_id, url=service.data.get('url'), api_key=api_key)
|
||||
|
||||
def send_tasker_service(service):
|
||||
"""Service to open url on devices."""
|
||||
send_notification(device_id=device_id,
|
||||
text=service.data.get('command'),
|
||||
api_key=api_key)
|
||||
|
||||
def send_sms_service(service):
|
||||
"""Service to send sms from devices."""
|
||||
send_sms(device_id=device_id,
|
||||
sms_number=service.data.get('number'),
|
||||
sms_text=service.data.get('message'),
|
||||
api_key=api_key)
|
||||
|
||||
name = name.lower().replace(" ", "_") + "_" if name else ""
|
||||
hass.services.register(DOMAIN, name + 'ring', ring_service)
|
||||
hass.services.register(DOMAIN, name + 'set_wallpaper',
|
||||
set_wallpaper_service)
|
||||
hass.services.register(DOMAIN, name + 'send_sms', send_sms_service)
|
||||
hass.services.register(DOMAIN, name + 'send_file', send_file_service)
|
||||
hass.services.register(DOMAIN, name + 'send_url', send_url_service)
|
||||
hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service)
|
||||
return True
|
295
homeassistant/components/knx.py
Normal file
295
homeassistant/components/knx.py
Normal file
@ -0,0 +1,295 @@
|
||||
"""
|
||||
Support for KNX components.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/knx/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DOMAIN = "knx"
|
||||
REQUIREMENTS = ['knxip==0.3.0']
|
||||
|
||||
EVENT_KNX_FRAME_RECEIVED = "knx_frame_received"
|
||||
|
||||
CONF_HOST = "host"
|
||||
CONF_PORT = "port"
|
||||
|
||||
DEFAULT_PORT = "3671"
|
||||
|
||||
KNXTUNNEL = None
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the connection to the KNX IP interface."""
|
||||
global KNXTUNNEL
|
||||
|
||||
from knxip.ip import KNXIPTunnel
|
||||
from knxip.core import KNXException
|
||||
|
||||
host = config[DOMAIN].get(CONF_HOST, None)
|
||||
|
||||
if host is None:
|
||||
_LOGGER.debug("Will try to auto-detect KNX/IP gateway")
|
||||
host = "0.0.0.0"
|
||||
|
||||
try:
|
||||
port = int(config[DOMAIN].get(CONF_PORT, DEFAULT_PORT))
|
||||
except ValueError:
|
||||
_LOGGER.exception("Can't parse KNX IP interface port")
|
||||
return False
|
||||
|
||||
KNXTUNNEL = KNXIPTunnel(host, port)
|
||||
try:
|
||||
KNXTUNNEL.connect()
|
||||
except KNXException as ex:
|
||||
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
|
||||
KNXTUNNEL = None
|
||||
return False
|
||||
|
||||
_LOGGER.info("KNX IP tunnel to %s:%i established", host, port)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_tunnel)
|
||||
return True
|
||||
|
||||
|
||||
def close_tunnel(_data):
|
||||
"""Close the NKX tunnel connection on shutdown."""
|
||||
global KNXTUNNEL
|
||||
|
||||
KNXTUNNEL.disconnect()
|
||||
KNXTUNNEL = None
|
||||
|
||||
|
||||
class KNXConfig(object):
|
||||
"""Handle the fetching of configuration from the config file."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the configuration."""
|
||||
from knxip.core import parse_group_address
|
||||
|
||||
self.config = config
|
||||
self.should_poll = config.get("poll", True)
|
||||
self._address = parse_group_address(config.get("address"))
|
||||
if self.config.get("state_address"):
|
||||
self._state_address = parse_group_address(
|
||||
self.config.get("state_address"))
|
||||
else:
|
||||
self._state_address = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name given to the entity."""
|
||||
return self.config["name"]
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""The address of the device as an integer value.
|
||||
|
||||
3 types of addresses are supported:
|
||||
integer - 0-65535
|
||||
2 level - a/b
|
||||
3 level - a/b/c
|
||||
"""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def state_address(self):
|
||||
"""The group address the device sends its current state to.
|
||||
|
||||
Some KNX devices can send the current state to a seperate
|
||||
group address. This makes send e.g. when an actuator can
|
||||
be switched but also have a timer functionality.
|
||||
"""
|
||||
return self._state_address
|
||||
|
||||
|
||||
class KNXGroupAddress(Entity):
|
||||
"""Representation of devices connected to a KNX group address."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the device."""
|
||||
self._config = config
|
||||
self._state = False
|
||||
self._data = None
|
||||
_LOGGER.debug("Initalizing KNX group address %s", self.address)
|
||||
|
||||
def handle_knx_message(addr, data):
|
||||
"""Handle an incoming KNX frame.
|
||||
|
||||
Handle an incoming frame and update our status if it contains
|
||||
information relating to this device.
|
||||
"""
|
||||
if (addr == self.state_address) or (addr == self.address):
|
||||
self._state = data
|
||||
self.update_ha_state()
|
||||
|
||||
KNXTUNNEL.register_listener(self.address, handle_knx_message)
|
||||
if self.state_address:
|
||||
KNXTUNNEL.register_listener(self.state_address, handle_knx_message)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The entity's display name."""
|
||||
return self._config.name
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""The entity's configuration."""
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the state of the polling, if needed."""
|
||||
return self._config.should_poll
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the value is not 0 is on, else False."""
|
||||
if self.should_poll:
|
||||
self.update()
|
||||
return self._state != 0
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the KNX group address."""
|
||||
return self._config.address
|
||||
|
||||
@property
|
||||
def state_address(self):
|
||||
"""Return the KNX group address."""
|
||||
return self._config.state_address
|
||||
|
||||
@property
|
||||
def cache(self):
|
||||
"""The name given to the entity."""
|
||||
return self._config.config.get("cache", True)
|
||||
|
||||
def group_write(self, value):
|
||||
"""Write to the group address."""
|
||||
KNXTUNNEL.group_write(self.address, [value])
|
||||
|
||||
def update(self):
|
||||
"""Get the state from KNX bus or cache."""
|
||||
from knxip.core import KNXException
|
||||
|
||||
try:
|
||||
if self.state_address:
|
||||
res = KNXTUNNEL.group_read(self.state_address,
|
||||
use_cache=self.cache)
|
||||
else:
|
||||
res = KNXTUNNEL.group_read(self.address,
|
||||
use_cache=self.cache)
|
||||
|
||||
if res:
|
||||
self._state = res[0]
|
||||
self._data = res
|
||||
else:
|
||||
_LOGGER.debug("Unable to read from KNX address: %s (None)",
|
||||
self.address)
|
||||
|
||||
except KNXException:
|
||||
_LOGGER.exception("Unable to read from KNX address: %s",
|
||||
self.address)
|
||||
return False
|
||||
|
||||
|
||||
class KNXMultiAddressDevice(KNXGroupAddress):
|
||||
"""Representation of devices connected to a multiple KNX group address.
|
||||
|
||||
This is needed for devices like dimmers or shutter actuators as they have
|
||||
to be controlled by multiple group addresses.
|
||||
"""
|
||||
|
||||
names = {}
|
||||
values = {}
|
||||
|
||||
def __init__(self, hass, config, required, optional=None):
|
||||
"""Initialize the device.
|
||||
|
||||
The namelist argument lists the required addresses. E.g. for a dimming
|
||||
actuators, the namelist might look like:
|
||||
onoff_address: 0/0/1
|
||||
brightness_address: 0/0/2
|
||||
"""
|
||||
from knxip.core import parse_group_address, KNXException
|
||||
|
||||
super().__init__(self, hass, config)
|
||||
|
||||
self.config = config
|
||||
|
||||
# parse required addresses
|
||||
for name in required:
|
||||
paramname = name + "_address"
|
||||
addr = self._config.config.get(paramname)
|
||||
if addr is None:
|
||||
_LOGGER.exception("Required KNX group address %s missing",
|
||||
paramname)
|
||||
raise KNXException("Group address missing in configuration")
|
||||
addr = parse_group_address(addr)
|
||||
self.names[addr] = name
|
||||
|
||||
# parse optional addresses
|
||||
for name in optional:
|
||||
paramname = name + "_address"
|
||||
addr = self._config.config.get(paramname)
|
||||
if addr:
|
||||
try:
|
||||
addr = parse_group_address(addr)
|
||||
except KNXException:
|
||||
_LOGGER.exception("Cannot parse group address %s", addr)
|
||||
self.names[addr] = name
|
||||
|
||||
def handle_frame(frame):
|
||||
"""Handle an incoming KNX frame.
|
||||
|
||||
Handle an incoming frame and update our status if it contains
|
||||
information relating to this device.
|
||||
"""
|
||||
addr = frame.data[0]
|
||||
|
||||
if addr in self.names:
|
||||
self.values[addr] = frame.data[1]
|
||||
self.update_ha_state()
|
||||
|
||||
hass.bus.listen(EVENT_KNX_FRAME_RECEIVED, handle_frame)
|
||||
|
||||
def group_write_address(self, name, value):
|
||||
"""Write to the group address with the given name."""
|
||||
KNXTUNNEL.group_write(self.address, [value])
|
||||
|
||||
def has_attribute(self, name):
|
||||
"""Check if the attribute with the given name is defined.
|
||||
|
||||
This is mostly important for optional addresses.
|
||||
"""
|
||||
for attributename, dummy_attribute in self.names.items():
|
||||
if attributename == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def value(self, name):
|
||||
"""Return the value to a given named attribute."""
|
||||
from knxip.core import KNXException
|
||||
|
||||
addr = None
|
||||
for attributename, attributeaddress in self.names.items():
|
||||
if attributename == name:
|
||||
addr = attributeaddress
|
||||
|
||||
if addr is None:
|
||||
_LOGGER.exception("Attribute %s undefined", name)
|
||||
return False
|
||||
|
||||
try:
|
||||
res = KNXTUNNEL.group_read(addr, use_cache=self.cache)
|
||||
except KNXException:
|
||||
_LOGGER.exception("Unable to read from KNX address: %s",
|
||||
addr)
|
||||
return False
|
||||
|
||||
return res
|
@ -67,12 +67,13 @@ PROP_TO_ATTR = {
|
||||
|
||||
# Service call validation schemas
|
||||
VALID_TRANSITION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900))
|
||||
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
|
||||
|
||||
LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
ATTR_PROFILE: str,
|
||||
ATTR_TRANSITION: VALID_TRANSITION,
|
||||
ATTR_BRIGHTNESS: cv.byte,
|
||||
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||
ATTR_COLOR_NAME: str,
|
||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||
vol.Coerce(tuple)),
|
||||
|
@ -18,7 +18,7 @@ LIGHT_TEMPS = [240, 380]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup demo light platform."""
|
||||
"""Setup the demo light platform."""
|
||||
add_devices_callback([
|
||||
DemoLight("Bed Light", False),
|
||||
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
|
||||
@ -27,7 +27,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
|
||||
class DemoLight(Light):
|
||||
"""Provide a demo light."""
|
||||
"""Represenation of a demo light."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, state, rgb=None, ct=None, brightness=180):
|
||||
|
@ -129,7 +129,7 @@ def setup_bridge(host, hass, add_devices_callback, filename,
|
||||
new_lights = []
|
||||
|
||||
api_name = api.get('config').get('name')
|
||||
if api_name == 'RaspBee-GW':
|
||||
if api_name in ('RaspBee-GW', 'deCONZ-GW'):
|
||||
bridge_type = 'deconz'
|
||||
else:
|
||||
bridge_type = 'hue'
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.util import color as color_util
|
||||
from homeassistant.util.color import \
|
||||
color_temperature_mired_to_kelvin as mired_to_kelvin
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
@ -14,16 +14,27 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
|
||||
color_temperature_mired_to_kelvin, color_temperature_to_rgb
|
||||
color_temperature_mired_to_kelvin, color_temperature_to_rgb, \
|
||||
color_rgb_to_rgbw, color_rgbw_to_rgb
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AEOTEC = 0x86
|
||||
AEOTEC_ZW098_LED_BULB = 0x62
|
||||
AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB)
|
||||
|
||||
COLOR_CHANNEL_WARM_WHITE = 0x01
|
||||
COLOR_CHANNEL_COLD_WHITE = 0x02
|
||||
COLOR_CHANNEL_RED = 0x04
|
||||
COLOR_CHANNEL_GREEN = 0x08
|
||||
COLOR_CHANNEL_BLUE = 0x10
|
||||
|
||||
WORKAROUND_ZW098 = 'zw098'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098
|
||||
}
|
||||
|
||||
# Generate midpoint color temperatures for bulbs that have limited
|
||||
# support for white light colors
|
||||
TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN
|
||||
@ -161,6 +172,7 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
self._color_channels = None
|
||||
self._rgb = None
|
||||
self._ct = None
|
||||
self._zw098 = None
|
||||
|
||||
# Here we attempt to find a zwave color value with the same instance
|
||||
# id as the dimmer value. Currently zwave nodes that change colors
|
||||
@ -182,6 +194,17 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
if self._value_color_channels is None:
|
||||
raise ValueError("Color Channels not found.")
|
||||
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16))
|
||||
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
|
||||
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
|
||||
self._zw098 = 1
|
||||
|
||||
super().__init__(value)
|
||||
|
||||
def update_properties(self):
|
||||
@ -218,11 +241,10 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
else:
|
||||
cold_white = 0
|
||||
|
||||
# Color temperature. With two white channels, only two color
|
||||
# temperatures are supported for the bulb. The channel values
|
||||
# Color temperature. With the AEOTEC ZW098 bulb, only two color
|
||||
# temperatures are supported. The warm and cold channel values
|
||||
# indicate brightness for warm/cold color temperature.
|
||||
if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and
|
||||
self._color_channels & COLOR_CHANNEL_COLD_WHITE):
|
||||
if self._zw098:
|
||||
if warm_white > 0:
|
||||
self._ct = TEMP_WARM_HASS
|
||||
self._rgb = ct_to_rgb(self._ct)
|
||||
@ -233,17 +255,11 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
# RGB color is being used. Just report midpoint.
|
||||
self._ct = TEMP_MID_HASS
|
||||
|
||||
# If only warm white is reported 0-255 is color temperature.
|
||||
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
||||
self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * (
|
||||
warm_white / 255)
|
||||
self._rgb = ct_to_rgb(self._ct)
|
||||
self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=warm_white))
|
||||
|
||||
# If only cold white is reported 0-255 is negative color temperature.
|
||||
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
||||
self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * (
|
||||
(255 - cold_white) / 255)
|
||||
self._rgb = ct_to_rgb(self._ct)
|
||||
self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=cold_white))
|
||||
|
||||
# If no rgb channels supported, report None.
|
||||
if not (self._color_channels & COLOR_CHANNEL_RED or
|
||||
@ -266,10 +282,10 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
rgbw = None
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
# With two white channels, only two color temperatures are
|
||||
# supported for the bulb.
|
||||
if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and
|
||||
self._color_channels & COLOR_CHANNEL_COLD_WHITE):
|
||||
# Color temperature. With the AEOTEC ZW098 bulb, only two color
|
||||
# temperatures are supported. The warm and cold channel values
|
||||
# indicate brightness for warm/cold color temperature.
|
||||
if self._zw098:
|
||||
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
|
||||
self._ct = TEMP_WARM_HASS
|
||||
rgbw = b'#000000FF00'
|
||||
@ -277,29 +293,20 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
self._ct = TEMP_COLD_HASS
|
||||
rgbw = b'#00000000FF'
|
||||
|
||||
# If only warm white is reported 0-255 is color temperature
|
||||
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
||||
rgbw = b'#000000'
|
||||
temp = (
|
||||
(kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) /
|
||||
(HASS_COLOR_MAX - HASS_COLOR_MIN) * 255)
|
||||
rgbw += format(int(temp)).encode('utf-8')
|
||||
|
||||
# If only cold white is reported 0-255 is negative color temp
|
||||
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
||||
rgbw = b'#000000'
|
||||
temp = (
|
||||
255 - (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) /
|
||||
(HASS_COLOR_MAX - HASS_COLOR_MIN) * 255)
|
||||
rgbw += format(int(temp)).encode('utf-8')
|
||||
|
||||
elif ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
|
||||
rgbw = b'#'
|
||||
for colorval in self._rgb:
|
||||
rgbw += format(colorval, '02x').encode('utf-8')
|
||||
rgbw += b'0000'
|
||||
if (not self._zw098 and (
|
||||
self._color_channels & COLOR_CHANNEL_WARM_WHITE or
|
||||
self._color_channels & COLOR_CHANNEL_COLD_WHITE)):
|
||||
rgbw = b'#'
|
||||
for colorval in color_rgb_to_rgbw(*self._rgb):
|
||||
rgbw += format(colorval, '02x').encode('utf-8')
|
||||
rgbw += b'00'
|
||||
else:
|
||||
rgbw = b'#'
|
||||
for colorval in self._rgb:
|
||||
rgbw += format(colorval, '02x').encode('utf-8')
|
||||
rgbw += b'0000'
|
||||
|
||||
if rgbw is None:
|
||||
_LOGGER.warning("rgbw string was not generated for turn_on")
|
||||
|
@ -39,7 +39,7 @@ class VerisureDoorlock(LockDevice):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the lock."""
|
||||
return 'Lock {}'.format(self._id)
|
||||
return '{}'.format(hub.lock_status[self._id].location)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -11,27 +11,23 @@ from itertools import groupby
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import recorder, sun
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON)
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.core import State
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
|
||||
DOMAIN = "logbook"
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
|
||||
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
|
||||
|
||||
QUERY_EVENTS_BETWEEN = """
|
||||
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
|
||||
"""
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
|
||||
@ -98,11 +94,14 @@ class LogbookView(HomeAssistantView):
|
||||
else:
|
||||
start_day = dt_util.start_of_local_day()
|
||||
|
||||
start_day = dt_util.as_utc(start_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)))
|
||||
events = recorder.get_model('Events')
|
||||
query = recorder.query('Events').filter(
|
||||
(events.time_fired > start_day) &
|
||||
(events.time_fired < end_day))
|
||||
events = recorder.execute(query)
|
||||
|
||||
return self.json(humanify(events))
|
||||
|
||||
|
@ -31,6 +31,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
SERVICE_SELECT_SOURCE = 'select_source'
|
||||
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
|
||||
|
||||
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
|
||||
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
|
||||
@ -75,6 +76,7 @@ SUPPORT_PLAY_MEDIA = 512
|
||||
SUPPORT_VOLUME_STEP = 1024
|
||||
SUPPORT_SELECT_SOURCE = 2048
|
||||
SUPPORT_STOP = 4096
|
||||
SUPPORT_CLEAR_PLAYLIST = 8192
|
||||
|
||||
# simple services that only take entity_id(s) as optional argument
|
||||
SERVICE_TO_METHOD = {
|
||||
@ -89,7 +91,8 @@ SERVICE_TO_METHOD = {
|
||||
SERVICE_MEDIA_STOP: 'media_stop',
|
||||
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
|
||||
SERVICE_SELECT_SOURCE: 'select_source'
|
||||
SERVICE_SELECT_SOURCE: 'select_source',
|
||||
SERVICE_CLEAR_PLAYLIST: 'clear_playlist'
|
||||
}
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
@ -272,6 +275,12 @@ def select_source(hass, source, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data)
|
||||
|
||||
|
||||
def clear_playlist(hass, entity_id=None):
|
||||
"""Send the media player the command for clear playlist."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track states and offer events for media_players."""
|
||||
component = EntityComponent(
|
||||
@ -542,6 +551,10 @@ class MediaPlayerDevice(Entity):
|
||||
"""Select input source."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def clear_playlist(self):
|
||||
"""Clear players playlist."""
|
||||
raise NotImplementedError()
|
||||
|
||||
# No need to overwrite these.
|
||||
@property
|
||||
def support_pause(self):
|
||||
@ -588,6 +601,11 @@ class MediaPlayerDevice(Entity):
|
||||
"""Boolean if select source command supported."""
|
||||
return bool(self.supported_media_commands & SUPPORT_SELECT_SOURCE)
|
||||
|
||||
@property
|
||||
def support_clear_playlist(self):
|
||||
"""Boolean if clear playlist command supported."""
|
||||
return bool(self.supported_media_commands & SUPPORT_CLEAR_PLAYLIST)
|
||||
|
||||
def toggle(self):
|
||||
"""Toggle the power on the media player."""
|
||||
if self.state in [STATE_OFF, STATE_IDLE]:
|
||||
|
@ -17,8 +17,8 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/aparraga/braviarc/archive/0.3.2.zip'
|
||||
'#braviarc==0.3.2']
|
||||
'https://github.com/aparraga/braviarc/archive/0.3.3.zip'
|
||||
'#braviarc==0.3.3']
|
||||
|
||||
BRAVIA_CONFIG_FILE = 'bravia.conf'
|
||||
CLIENTID_PREFIX = 'HomeAssistant'
|
||||
@ -220,20 +220,24 @@ class BraviaTVDevice(MediaPlayerDevice):
|
||||
self._refresh_volume()
|
||||
self._refresh_channels()
|
||||
|
||||
playing_info = self._braviarc.get_playing_info()
|
||||
if playing_info is None or len(playing_info) == 0:
|
||||
self._state = STATE_OFF
|
||||
else:
|
||||
power_status = self._braviarc.get_power_status()
|
||||
if power_status == 'active':
|
||||
self._state = STATE_ON
|
||||
self._program_name = playing_info.get('programTitle')
|
||||
self._channel_name = playing_info.get('title')
|
||||
self._program_media_type = playing_info.get(
|
||||
'programMediaType')
|
||||
self._channel_number = playing_info.get('dispNum')
|
||||
self._source = playing_info.get('source')
|
||||
self._content_uri = playing_info.get('uri')
|
||||
self._duration = playing_info.get('durationSec')
|
||||
self._start_date_time = playing_info.get('startDateTime')
|
||||
playing_info = self._braviarc.get_playing_info()
|
||||
if playing_info is None or len(playing_info) == 0:
|
||||
self._channel_name = 'App'
|
||||
else:
|
||||
self._program_name = playing_info.get('programTitle')
|
||||
self._channel_name = playing_info.get('title')
|
||||
self._program_media_type = playing_info.get(
|
||||
'programMediaType')
|
||||
self._channel_number = playing_info.get('dispNum')
|
||||
self._source = playing_info.get('source')
|
||||
self._content_uri = playing_info.get('uri')
|
||||
self._duration = playing_info.get('durationSec')
|
||||
self._start_date_time = playing_info.get('startDateTime')
|
||||
else:
|
||||
self._state = STATE_OFF
|
||||
|
||||
except Exception as exception_instance: # pylint: disable=broad-except
|
||||
_LOGGER.error(exception_instance)
|
||||
|
@ -8,7 +8,7 @@ from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, MediaPlayerDevice)
|
||||
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ YOUTUBE_PLAYER_SUPPORT = \
|
||||
|
||||
MUSIC_PLAYER_SUPPORT = \
|
||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST
|
||||
|
||||
NETFLIX_PLAYER_SUPPORT = \
|
||||
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
@ -214,12 +214,12 @@ class DemoMusicPlayer(AbstractDemoPlayer):
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Return the title of current playing media."""
|
||||
return self.tracks[self._cur_track][1]
|
||||
return self.tracks[self._cur_track][1] if len(self.tracks) > 0 else ""
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Return the artist of current playing media (Music track only)."""
|
||||
return self.tracks[self._cur_track][0]
|
||||
return self.tracks[self._cur_track][0] if len(self.tracks) > 0 else ""
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
@ -257,6 +257,13 @@ class DemoMusicPlayer(AbstractDemoPlayer):
|
||||
self._cur_track += 1
|
||||
self.update_ha_state()
|
||||
|
||||
def clear_playlist(self):
|
||||
"""Clear players playlist."""
|
||||
self.tracks = []
|
||||
self._cur_track = 0
|
||||
self._player_state = STATE_OFF
|
||||
self.update_ha_state()
|
||||
|
||||
|
||||
class DemoTVShowPlayer(AbstractDemoPlayer):
|
||||
"""A Demo media player that only supports YouTube."""
|
||||
|
@ -122,7 +122,11 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
||||
try:
|
||||
devices = plexserver.clients()
|
||||
except plexapi.exceptions.BadRequest:
|
||||
_LOGGER.exception("Error listing plex devices")
|
||||
_LOGGER.exception('Error listing plex devices')
|
||||
return
|
||||
except OSError:
|
||||
_LOGGER.error(
|
||||
'Could not connect to plex server at http://%s', host)
|
||||
return
|
||||
|
||||
new_plex_clients = []
|
||||
@ -148,7 +152,7 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
||||
try:
|
||||
sessions = plexserver.sessions()
|
||||
except plexapi.exceptions.BadRequest:
|
||||
_LOGGER.exception("Error listing plex sessions")
|
||||
_LOGGER.exception('Error listing plex sessions')
|
||||
return
|
||||
|
||||
plex_sessions.clear()
|
||||
@ -166,7 +170,7 @@ def request_configuration(host, hass, add_devices_callback):
|
||||
# We got an error if this method is called while we are configuring
|
||||
if host in _CONFIGURING:
|
||||
configurator.notify_errors(
|
||||
_CONFIGURING[host], "Failed to register, please try again.")
|
||||
_CONFIGURING[host], 'Failed to register, please try again.')
|
||||
|
||||
return
|
||||
|
||||
@ -175,10 +179,10 @@ def request_configuration(host, hass, add_devices_callback):
|
||||
setup_plexserver(host, data.get('token'), hass, add_devices_callback)
|
||||
|
||||
_CONFIGURING[host] = configurator.request_config(
|
||||
hass, "Plex Media Server", plex_configuration_callback,
|
||||
hass, 'Plex Media Server', plex_configuration_callback,
|
||||
description=('Enter the X-Plex-Token'),
|
||||
description_image="/static/images/config_plex_mediaserver.png",
|
||||
submit_caption="Confirm",
|
||||
description_image='/static/images/config_plex_mediaserver.png',
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}]
|
||||
)
|
||||
|
||||
@ -201,7 +205,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of this plex client."""
|
||||
return "{}.{}".format(
|
||||
return '{}.{}'.format(
|
||||
self.__class__, self.device.machineIdentifier or self.device.name)
|
||||
|
||||
@property
|
||||
|
@ -15,8 +15,8 @@ from homeassistant.const import (
|
||||
CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME)
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/bah2830/python-roku/archive/3.1.1.zip'
|
||||
'#python-roku==3.1.1']
|
||||
'https://github.com/bah2830/python-roku/archive/3.1.2.zip'
|
||||
'#roku==3.1.2']
|
||||
|
||||
KNOWN_HOSTS = []
|
||||
DEFAULT_PORT = 8060
|
||||
@ -45,8 +45,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
rokus = []
|
||||
for host in hosts:
|
||||
rokus.append(RokuDevice(host))
|
||||
KNOWN_HOSTS.append(host)
|
||||
new_roku = RokuDevice(host)
|
||||
|
||||
if new_roku.name is None:
|
||||
_LOGGER.error("Unable to initialize roku at %s", host)
|
||||
else:
|
||||
rokus.append(RokuDevice(host))
|
||||
KNOWN_HOSTS.append(host)
|
||||
|
||||
add_devices(rokus)
|
||||
|
||||
@ -61,6 +66,11 @@ class RokuDevice(MediaPlayerDevice):
|
||||
from roku import Roku
|
||||
|
||||
self.roku = Roku(host)
|
||||
self.roku_name = None
|
||||
self.ip_address = host
|
||||
self.channels = []
|
||||
self.current_app = None
|
||||
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
@ -78,7 +88,7 @@ class RokuDevice(MediaPlayerDevice):
|
||||
self.current_app = None
|
||||
except (requests.exceptions.ConnectionError,
|
||||
requests.exceptions.ReadTimeout):
|
||||
self.current_app = None
|
||||
_LOGGER.error("Unable to connect to roku at %s", self.ip_address)
|
||||
|
||||
def get_source_list(self):
|
||||
"""Get the list of applications to be used as sources."""
|
||||
|
@ -146,6 +146,14 @@ select_source:
|
||||
description: Name of the source to switch to. Platform dependent.
|
||||
example: 'video1'
|
||||
|
||||
clear_playlist:
|
||||
description: Send the media player the command to clear players playlist.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites to change source on
|
||||
example: 'media_player.living_room_chromecast'
|
||||
|
||||
sonos_group_players:
|
||||
description: Send Sonos media player the command for grouping all players into one (party mode).
|
||||
|
||||
|
@ -12,7 +12,8 @@ from os import path
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
|
||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
@ -31,13 +32,17 @@ _REQUESTS_LOGGER.setLevel(logging.ERROR)
|
||||
|
||||
SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
|
||||
SUPPORT_SEEK
|
||||
SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST | SUPPORT_SELECT_SOURCE
|
||||
|
||||
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
|
||||
SERVICE_UNJOIN = 'sonos_unjoin'
|
||||
SERVICE_SNAPSHOT = 'sonos_snapshot'
|
||||
SERVICE_RESTORE = 'sonos_restore'
|
||||
|
||||
SUPPORT_SOURCE_LINEIN = 'Line-in'
|
||||
SUPPORT_SOURCE_TV = 'TV'
|
||||
SUPPORT_SOURCE_RADIO = 'Radio'
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, too-many-locals
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@ -162,12 +167,12 @@ class SonosDevice(MediaPlayerDevice):
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, player):
|
||||
"""Initialize the Sonos device."""
|
||||
from soco.snapshot import Snapshot
|
||||
|
||||
self.hass = hass
|
||||
self.volume_increment = 5
|
||||
super(SonosDevice, self).__init__()
|
||||
self._player = player
|
||||
self.update()
|
||||
from soco.snapshot import Snapshot
|
||||
self.soco_snapshot = Snapshot(self._player)
|
||||
|
||||
@property
|
||||
@ -261,6 +266,10 @@ class SonosDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
if self._player.is_playing_line_in:
|
||||
return SUPPORT_SOURCE_LINEIN
|
||||
if self._player.is_playing_tv:
|
||||
return SUPPORT_SOURCE_TV
|
||||
if 'artist' in self._trackinfo and 'title' in self._trackinfo:
|
||||
return '{artist} - {title}'.format(
|
||||
artist=self._trackinfo['artist'],
|
||||
@ -290,6 +299,36 @@ class SonosDevice(MediaPlayerDevice):
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
self._player.mute = mute
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
if source == SUPPORT_SOURCE_LINEIN:
|
||||
self._player.switch_to_line_in()
|
||||
elif source == SUPPORT_SOURCE_TV:
|
||||
self._player.switch_to_tv()
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
source = []
|
||||
|
||||
# generate list of supported device
|
||||
source.append(SUPPORT_SOURCE_LINEIN)
|
||||
source.append(SUPPORT_SOURCE_TV)
|
||||
source.append(SUPPORT_SOURCE_RADIO)
|
||||
|
||||
return source
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Name of the current input source."""
|
||||
if self._player.is_playing_line_in:
|
||||
return SUPPORT_SOURCE_LINEIN
|
||||
if self._player.is_playing_tv:
|
||||
return SUPPORT_SOURCE_TV
|
||||
if self._player.is_playing_radio:
|
||||
return SUPPORT_SOURCE_RADIO
|
||||
return None
|
||||
|
||||
@only_if_coordinator
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
@ -320,6 +359,11 @@ class SonosDevice(MediaPlayerDevice):
|
||||
"""Send seek command."""
|
||||
self._player.seek(str(datetime.timedelta(seconds=int(position))))
|
||||
|
||||
@only_if_coordinator
|
||||
def clear_playlist(self):
|
||||
"""Clear players playlist."""
|
||||
self._player.clear_queue()
|
||||
|
||||
@only_if_coordinator
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
|
@ -17,8 +17,9 @@ from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED,
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, ATTR_INPUT_SOURCE,
|
||||
SERVICE_SELECT_SOURCE, MediaPlayerDevice)
|
||||
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
|
||||
ATTR_INPUT_SOURCE, SERVICE_SELECT_SOURCE, SERVICE_CLEAR_PLAYLIST,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK,
|
||||
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
|
||||
@ -346,9 +347,12 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
||||
ATTR_MEDIA_VOLUME_MUTED in self._attrs:
|
||||
flags |= SUPPORT_VOLUME_MUTE
|
||||
|
||||
if SUPPORT_SELECT_SOURCE in self._cmds:
|
||||
if SERVICE_SELECT_SOURCE in self._cmds:
|
||||
flags |= SUPPORT_SELECT_SOURCE
|
||||
|
||||
if SERVICE_CLEAR_PLAYLIST in self._cmds:
|
||||
flags |= SUPPORT_CLEAR_PLAYLIST
|
||||
|
||||
return flags
|
||||
|
||||
@property
|
||||
@ -424,6 +428,10 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
||||
data = {ATTR_INPUT_SOURCE: source}
|
||||
self._call_service(SERVICE_SELECT_SOURCE, data)
|
||||
|
||||
def clear_playlist(self):
|
||||
"""Clear players playlist."""
|
||||
self._call_service(SERVICE_CLEAR_PLAYLIST)
|
||||
|
||||
def update(self):
|
||||
"""Update state in HA."""
|
||||
for child_name in self._children:
|
||||
|
@ -38,7 +38,7 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_MESSAGE): cv.template,
|
||||
vol.Optional(ATTR_TITLE, default=ATTR_TITLE_DEFAULT): cv.string,
|
||||
vol.Optional(ATTR_TARGET): cv.string,
|
||||
vol.Optional(ATTR_DATA): dict, # nobody seems to be using this (yet)
|
||||
vol.Optional(ATTR_DATA): dict,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
62
homeassistant/components/notify/joaoapps_join.py
Normal file
62
homeassistant/components/notify/joaoapps_join.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""
|
||||
Join platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.join/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_DATA, ATTR_TITLE, BaseNotificationService)
|
||||
from homeassistant.const import CONF_PLATFORM, CONF_NAME, CONF_API_KEY
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/nkgilley/python-join-api/archive/'
|
||||
'3e1e849f1af0b4080f551b62270c6d244d5fbcbd.zip#python-join-api==0.0.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DEVICE_ID = 'device_id'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'joaoapps_join',
|
||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_API_KEY): cv.string
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def get_service(hass, config):
|
||||
"""Get the Join notification service."""
|
||||
device_id = config.get(CONF_DEVICE_ID)
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
if api_key:
|
||||
from pyjoin import get_devices
|
||||
if not get_devices(api_key):
|
||||
_LOGGER.error("Error connecting to Join, check API key")
|
||||
return False
|
||||
return JoinNotificationService(device_id, api_key)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class JoinNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for Join."""
|
||||
|
||||
def __init__(self, device_id, api_key=None):
|
||||
"""Initialize the service."""
|
||||
self._device_id = device_id
|
||||
self._api_key = api_key
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
from pyjoin import send_notification
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
data = kwargs.get(ATTR_DATA) or {}
|
||||
send_notification(device_id=self._device_id,
|
||||
text=message,
|
||||
title=title,
|
||||
icon=data.get('icon'),
|
||||
smallicon=data.get('smallicon'),
|
||||
api_key=self._api_key)
|
@ -13,3 +13,7 @@ notify:
|
||||
target:
|
||||
description: Target of the notification. Optional depending on the platform
|
||||
example: platform specific
|
||||
|
||||
data:
|
||||
description: Extended information for notification. Optional depending on the platform
|
||||
example: platform specific
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['slacker==0.9.17']
|
||||
REQUIREMENTS = ['slacker==0.9.21']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -30,8 +30,7 @@ def get_service(hass, config):
|
||||
config[CONF_API_KEY])
|
||||
|
||||
except slacker.Error:
|
||||
_LOGGER.exception(
|
||||
"Slack authentication failed")
|
||||
_LOGGER.exception("Slack authentication failed")
|
||||
return None
|
||||
|
||||
|
||||
|
@ -4,17 +4,27 @@ Telegram platform for notify component.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.telegram/
|
||||
"""
|
||||
import io
|
||||
import logging
|
||||
import urllib
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TITLE, DOMAIN, BaseNotificationService)
|
||||
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-telegram-bot==4.2.1']
|
||||
REQUIREMENTS = ['python-telegram-bot==4.3.3']
|
||||
|
||||
ATTR_PHOTO = "photo"
|
||||
ATTR_FILE = "file"
|
||||
ATTR_URL = "url"
|
||||
ATTR_CAPTION = "caption"
|
||||
ATTR_USERNAME = "username"
|
||||
ATTR_PASSWORD = "password"
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
@ -54,9 +64,51 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
import telegram
|
||||
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
data = kwargs.get(ATTR_DATA, {})
|
||||
|
||||
# send message
|
||||
try:
|
||||
self.bot.sendMessage(chat_id=self._chat_id,
|
||||
text=title + " " + message)
|
||||
except telegram.error.TelegramError:
|
||||
_LOGGER.exception("Error sending message.")
|
||||
return
|
||||
|
||||
# send photo
|
||||
if ATTR_PHOTO in data:
|
||||
# if not a list
|
||||
if not isinstance(data[ATTR_PHOTO], list):
|
||||
photos = [data[ATTR_PHOTO]]
|
||||
else:
|
||||
photos = data[ATTR_PHOTO]
|
||||
|
||||
try:
|
||||
for photo_data in photos:
|
||||
caption = photo_data.get(ATTR_CAPTION, None)
|
||||
|
||||
# file is a url
|
||||
if ATTR_URL in photo_data:
|
||||
# use http authenticate
|
||||
if ATTR_USERNAME in photo_data and\
|
||||
ATTR_PASSWORD in photo_data:
|
||||
req = requests.get(
|
||||
photo_data[ATTR_URL],
|
||||
auth=HTTPBasicAuth(photo_data[ATTR_USERNAME],
|
||||
photo_data[ATTR_PASSWORD])
|
||||
)
|
||||
else:
|
||||
req = requests.get(photo_data[ATTR_URL])
|
||||
file_id = io.BytesIO(req.content)
|
||||
elif ATTR_FILE in photo_data:
|
||||
file_id = open(photo_data[ATTR_FILE], "rb")
|
||||
else:
|
||||
_LOGGER.error("No url or path is set for photo!")
|
||||
continue
|
||||
|
||||
self.bot.sendPhoto(chat_id=self._chat_id,
|
||||
photo=file_id, caption=caption)
|
||||
|
||||
except (OSError, IOError, telegram.error.TelegramError,
|
||||
urllib.error.HTTPError):
|
||||
_LOGGER.exception("Error sending photo.")
|
||||
return
|
||||
|
@ -4,6 +4,7 @@ A component which is collecting configuration errors.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/persistent_notification/
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@ -12,6 +13,7 @@ from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template, config_validation as cv
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
DOMAIN = 'persistent_notification'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@ -33,7 +35,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create(hass, message, title=None, notification_id=None):
|
||||
"""Turn all or specified light off."""
|
||||
"""Generate a notification."""
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
(ATTR_TITLE, title),
|
||||
@ -74,7 +76,10 @@ def setup(hass, config):
|
||||
|
||||
hass.states.set(entity_id, message, attr)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_CREATE, create_service, {},
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_CREATE, create_service,
|
||||
descriptions[DOMAIN][SERVICE_CREATE],
|
||||
SCHEMA_SERVICE_CREATE)
|
||||
|
||||
return True
|
||||
|
@ -1,529 +0,0 @@
|
||||
"""
|
||||
Support for recording details.
|
||||
|
||||
Component that records all events and state changes. Allows other components
|
||||
to query this database.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/recorder/
|
||||
"""
|
||||
import atexit
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
import sqlite3
|
||||
import threading
|
||||
from datetime import date, datetime, timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
EVENT_TIME_CHANGED, MATCH_ALL)
|
||||
from homeassistant.core import Event, EventOrigin, State
|
||||
from homeassistant.remote import JSONEncoder
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
|
||||
DOMAIN = "recorder"
|
||||
|
||||
DB_FILE = 'home-assistant.db'
|
||||
|
||||
RETURN_ROWCOUNT = "rowcount"
|
||||
RETURN_LASTROWID = "lastrowid"
|
||||
RETURN_ONE_ROW = "one_row"
|
||||
|
||||
CONF_PURGE_DAYS = "purge_days"
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1)),
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
_INSTANCE = None
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def query(sql_query, arguments=None):
|
||||
"""Query the database."""
|
||||
_verify_instance()
|
||||
|
||||
return _INSTANCE.query(sql_query, arguments)
|
||||
|
||||
|
||||
def query_states(state_query, arguments=None):
|
||||
"""Query the database and return a list of states."""
|
||||
return [
|
||||
row for row in
|
||||
(row_to_state(row) for row in query(state_query, arguments))
|
||||
if row is not None]
|
||||
|
||||
|
||||
def query_events(event_query, arguments=None):
|
||||
"""Query the database and return a list of states."""
|
||||
return [
|
||||
row for row in
|
||||
(row_to_event(row) for row in query(event_query, arguments))
|
||||
if row is not None]
|
||||
|
||||
|
||||
def row_to_state(row):
|
||||
"""Convert a database row to a state."""
|
||||
try:
|
||||
return State(
|
||||
row[1], row[2], json.loads(row[3]),
|
||||
dt_util.utc_from_timestamp(row[4]),
|
||||
dt_util.utc_from_timestamp(row[5]))
|
||||
except ValueError:
|
||||
# When json.loads fails
|
||||
_LOGGER.exception("Error converting row to state: %s", row)
|
||||
return None
|
||||
|
||||
|
||||
def row_to_event(row):
|
||||
"""Convert a databse row to an event."""
|
||||
try:
|
||||
return Event(row[1], json.loads(row[2]), EventOrigin(row[3]),
|
||||
dt_util.utc_from_timestamp(row[5]))
|
||||
except ValueError:
|
||||
# When json.loads fails
|
||||
_LOGGER.exception("Error converting row to event: %s", row)
|
||||
return None
|
||||
|
||||
|
||||
def run_information(point_in_time=None):
|
||||
"""Return information about current run.
|
||||
|
||||
There is also the run that covers point_in_time.
|
||||
"""
|
||||
_verify_instance()
|
||||
|
||||
if point_in_time is None or point_in_time > _INSTANCE.recording_start:
|
||||
return RecorderRun()
|
||||
|
||||
run = _INSTANCE.query(
|
||||
"SELECT * FROM recorder_runs WHERE start<? AND END>?",
|
||||
(point_in_time, point_in_time), return_value=RETURN_ONE_ROW)
|
||||
|
||||
return RecorderRun(run) if run else None
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the recorder."""
|
||||
# pylint: disable=global-statement
|
||||
global _INSTANCE
|
||||
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
|
||||
_INSTANCE = Recorder(hass, purge_days=purge_days)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class RecorderRun(object):
|
||||
"""Representation of a recorder run."""
|
||||
|
||||
def __init__(self, row=None):
|
||||
"""Initialize the recorder run."""
|
||||
self.end = None
|
||||
|
||||
if row is None:
|
||||
self.start = _INSTANCE.recording_start
|
||||
self.closed_incorrect = False
|
||||
else:
|
||||
self.start = dt_util.utc_from_timestamp(row[1])
|
||||
|
||||
if row[2] is not None:
|
||||
self.end = dt_util.utc_from_timestamp(row[2])
|
||||
|
||||
self.closed_incorrect = bool(row[3])
|
||||
|
||||
def entity_ids(self, point_in_time=None):
|
||||
"""Return the entity ids that existed in this run.
|
||||
|
||||
Specify point_in_time if you want to know which existed at that point
|
||||
in time inside the run.
|
||||
"""
|
||||
where = self.where_after_start_run
|
||||
where_data = []
|
||||
|
||||
if point_in_time is not None or self.end is not None:
|
||||
where += "AND created < ? "
|
||||
where_data.append(point_in_time or self.end)
|
||||
|
||||
return [row[0] for row in query(
|
||||
"SELECT entity_id FROM states WHERE {}"
|
||||
"GROUP BY entity_id".format(where), where_data)]
|
||||
|
||||
@property
|
||||
def where_after_start_run(self):
|
||||
"""Return SQL WHERE clause.
|
||||
|
||||
Selection of the rows created after the start of the run.
|
||||
"""
|
||||
return "created >= {} ".format(_adapt_datetime(self.start))
|
||||
|
||||
@property
|
||||
def where_limit_to_run(self):
|
||||
"""Return a SQL WHERE clause.
|
||||
|
||||
For limiting the results to this run.
|
||||
"""
|
||||
where = self.where_after_start_run
|
||||
|
||||
if self.end is not None:
|
||||
where += "AND created < {} ".format(_adapt_datetime(self.end))
|
||||
|
||||
return where
|
||||
|
||||
|
||||
class Recorder(threading.Thread):
|
||||
"""A threaded recorder class."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, hass, purge_days):
|
||||
"""Initialize the recorder."""
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.hass = hass
|
||||
self.purge_days = purge_days
|
||||
self.conn = None
|
||||
self.queue = queue.Queue()
|
||||
self.quit_object = object()
|
||||
self.lock = threading.Lock()
|
||||
self.recording_start = dt_util.utcnow()
|
||||
self.utc_offset = dt_util.now().utcoffset().total_seconds()
|
||||
self.db_path = self.hass.config.path(DB_FILE)
|
||||
|
||||
def start_recording(event):
|
||||
"""Start recording."""
|
||||
self.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_recording)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
|
||||
hass.bus.listen(MATCH_ALL, self.event_listener)
|
||||
|
||||
def run(self):
|
||||
"""Start processing events to save."""
|
||||
self._setup_connection()
|
||||
self._setup_run()
|
||||
if self.purge_days is not None:
|
||||
track_point_in_utc_time(self.hass,
|
||||
lambda now: self._purge_old_data(),
|
||||
dt_util.utcnow() + timedelta(minutes=5))
|
||||
|
||||
while True:
|
||||
event = self.queue.get()
|
||||
|
||||
if event == self.quit_object:
|
||||
self._close_run()
|
||||
self._close_connection()
|
||||
self.queue.task_done()
|
||||
return
|
||||
|
||||
elif event.event_type == EVENT_TIME_CHANGED:
|
||||
self.queue.task_done()
|
||||
continue
|
||||
|
||||
event_id = self.record_event(event)
|
||||
|
||||
if event.event_type == EVENT_STATE_CHANGED:
|
||||
self.record_state(
|
||||
event.data['entity_id'], event.data.get('new_state'),
|
||||
event_id)
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
def event_listener(self, event):
|
||||
"""Listen for new events and put them in the process queue."""
|
||||
self.queue.put(event)
|
||||
|
||||
def shutdown(self, event):
|
||||
"""Tell the recorder to shut down."""
|
||||
self.queue.put(self.quit_object)
|
||||
self.block_till_done()
|
||||
|
||||
def record_state(self, entity_id, state, event_id):
|
||||
"""Save a state to the database."""
|
||||
now = dt_util.utcnow()
|
||||
|
||||
# State got deleted
|
||||
if state is None:
|
||||
state_state = ''
|
||||
state_domain = ''
|
||||
state_attr = '{}'
|
||||
last_changed = last_updated = now
|
||||
else:
|
||||
state_domain = state.domain
|
||||
state_state = state.state
|
||||
state_attr = json.dumps(dict(state.attributes))
|
||||
last_changed = state.last_changed
|
||||
last_updated = state.last_updated
|
||||
|
||||
info = (
|
||||
entity_id, state_domain, state_state, state_attr,
|
||||
last_changed, last_updated,
|
||||
now, self.utc_offset, event_id)
|
||||
|
||||
self.query(
|
||||
"""
|
||||
INSERT INTO states (
|
||||
entity_id, domain, state, attributes, last_changed, last_updated,
|
||||
created, utc_offset, event_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
info)
|
||||
|
||||
def record_event(self, event):
|
||||
"""Save an event to the database."""
|
||||
info = (
|
||||
event.event_type, json.dumps(event.data, cls=JSONEncoder),
|
||||
str(event.origin), dt_util.utcnow(), event.time_fired,
|
||||
self.utc_offset
|
||||
)
|
||||
|
||||
return self.query(
|
||||
"INSERT INTO events ("
|
||||
"event_type, event_data, origin, created, time_fired, utc_offset"
|
||||
") VALUES (?, ?, ?, ?, ?, ?)", info, RETURN_LASTROWID)
|
||||
|
||||
def query(self, sql_query, data=None, return_value=None):
|
||||
"""Query the database."""
|
||||
try:
|
||||
with self.conn, self.lock:
|
||||
_LOGGER.debug("Running query %s", sql_query)
|
||||
|
||||
cur = self.conn.cursor()
|
||||
|
||||
if data is not None:
|
||||
cur.execute(sql_query, data)
|
||||
else:
|
||||
cur.execute(sql_query)
|
||||
|
||||
if return_value == RETURN_ROWCOUNT:
|
||||
return cur.rowcount
|
||||
elif return_value == RETURN_LASTROWID:
|
||||
return cur.lastrowid
|
||||
elif return_value == RETURN_ONE_ROW:
|
||||
return cur.fetchone()
|
||||
else:
|
||||
return cur.fetchall()
|
||||
|
||||
except (sqlite3.IntegrityError, sqlite3.OperationalError,
|
||||
sqlite3.ProgrammingError):
|
||||
_LOGGER.exception(
|
||||
"Error querying the database using: %s", sql_query)
|
||||
return []
|
||||
|
||||
def block_till_done(self):
|
||||
"""Block till all events processed."""
|
||||
self.queue.join()
|
||||
|
||||
def _setup_connection(self):
|
||||
"""Ensure database is ready to fly."""
|
||||
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
|
||||
# Make sure the database is closed whenever Python exits
|
||||
# without the STOP event being fired.
|
||||
atexit.register(self._close_connection)
|
||||
|
||||
# Have datetime objects be saved as integers.
|
||||
sqlite3.register_adapter(date, _adapt_datetime)
|
||||
sqlite3.register_adapter(datetime, _adapt_datetime)
|
||||
|
||||
# Validate we are on the correct schema or that we have to migrate.
|
||||
cur = self.conn.cursor()
|
||||
|
||||
def save_migration(migration_id):
|
||||
"""Save and commit a migration to the database."""
|
||||
cur.execute('INSERT INTO schema_version VALUES (?, ?)',
|
||||
(migration_id, dt_util.utcnow()))
|
||||
self.conn.commit()
|
||||
_LOGGER.info("Database migrated to version %d", migration_id)
|
||||
|
||||
try:
|
||||
cur.execute('SELECT max(migration_id) FROM schema_version;')
|
||||
migration_id = cur.fetchone()[0] or 0
|
||||
|
||||
except sqlite3.OperationalError:
|
||||
# The table does not exist.
|
||||
cur.execute('CREATE TABLE schema_version ('
|
||||
'migration_id integer primary key, performed integer)')
|
||||
migration_id = 0
|
||||
|
||||
if migration_id < 1:
|
||||
cur.execute("""
|
||||
CREATE TABLE recorder_runs (
|
||||
run_id integer primary key,
|
||||
start integer,
|
||||
end integer,
|
||||
closed_incorrect integer default 0,
|
||||
created integer)
|
||||
""")
|
||||
|
||||
cur.execute("""
|
||||
CREATE TABLE events (
|
||||
event_id integer primary key,
|
||||
event_type text,
|
||||
event_data text,
|
||||
origin text,
|
||||
created integer)
|
||||
""")
|
||||
cur.execute(
|
||||
'CREATE INDEX events__event_type ON events(event_type)')
|
||||
|
||||
cur.execute("""
|
||||
CREATE TABLE states (
|
||||
state_id integer primary key,
|
||||
entity_id text,
|
||||
state text,
|
||||
attributes text,
|
||||
last_changed integer,
|
||||
last_updated integer,
|
||||
created integer)
|
||||
""")
|
||||
cur.execute('CREATE INDEX states__entity_id ON states(entity_id)')
|
||||
|
||||
save_migration(1)
|
||||
|
||||
if migration_id < 2:
|
||||
cur.execute("""
|
||||
ALTER TABLE events
|
||||
ADD COLUMN time_fired integer
|
||||
""")
|
||||
|
||||
cur.execute('UPDATE events SET time_fired=created')
|
||||
|
||||
save_migration(2)
|
||||
|
||||
if migration_id < 3:
|
||||
utc_offset = self.utc_offset
|
||||
|
||||
cur.execute("""
|
||||
ALTER TABLE recorder_runs
|
||||
ADD COLUMN utc_offset integer
|
||||
""")
|
||||
|
||||
cur.execute("""
|
||||
ALTER TABLE events
|
||||
ADD COLUMN utc_offset integer
|
||||
""")
|
||||
|
||||
cur.execute("""
|
||||
ALTER TABLE states
|
||||
ADD COLUMN utc_offset integer
|
||||
""")
|
||||
|
||||
cur.execute("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
|
||||
cur.execute("UPDATE events SET utc_offset=?", [utc_offset])
|
||||
cur.execute("UPDATE states SET utc_offset=?", [utc_offset])
|
||||
|
||||
save_migration(3)
|
||||
|
||||
if migration_id < 4:
|
||||
# We had a bug where we did not save utc offset for recorder runs.
|
||||
cur.execute(
|
||||
"""UPDATE recorder_runs SET utc_offset=?
|
||||
WHERE utc_offset IS NULL""", [self.utc_offset])
|
||||
|
||||
cur.execute("""
|
||||
ALTER TABLE states
|
||||
ADD COLUMN event_id integer
|
||||
""")
|
||||
|
||||
save_migration(4)
|
||||
|
||||
if migration_id < 5:
|
||||
# Add domain so that thermostat graphs look right.
|
||||
try:
|
||||
cur.execute("""
|
||||
ALTER TABLE states
|
||||
ADD COLUMN domain text
|
||||
""")
|
||||
except sqlite3.OperationalError:
|
||||
# We had a bug in this migration for a while on dev.
|
||||
# Without this, dev-users will have to throw away their db.
|
||||
pass
|
||||
|
||||
# TravisCI has Python compiled against an old version of SQLite3
|
||||
# which misses the instr method.
|
||||
self.conn.create_function(
|
||||
"instr", 2,
|
||||
lambda string, substring: string.find(substring) + 1)
|
||||
|
||||
# Populate domain with defaults.
|
||||
cur.execute("""
|
||||
UPDATE states
|
||||
set domain=substr(entity_id, 0, instr(entity_id, '.'))
|
||||
""")
|
||||
|
||||
# Add indexes we are going to use a lot on selects.
|
||||
cur.execute("""
|
||||
CREATE INDEX states__state_changes ON
|
||||
states (last_changed, last_updated, entity_id)""")
|
||||
cur.execute("""
|
||||
CREATE INDEX states__significant_changes ON
|
||||
states (domain, last_updated, entity_id)""")
|
||||
save_migration(5)
|
||||
|
||||
def _close_connection(self):
|
||||
"""Close connection to the database."""
|
||||
_LOGGER.info("Closing database")
|
||||
atexit.unregister(self._close_connection)
|
||||
self.conn.close()
|
||||
|
||||
def _setup_run(self):
|
||||
"""Log the start of the current run."""
|
||||
if self.query("""UPDATE recorder_runs SET end=?, closed_incorrect=1
|
||||
WHERE end IS NULL""", (self.recording_start, ),
|
||||
return_value=RETURN_ROWCOUNT):
|
||||
|
||||
_LOGGER.warning("Found unfinished sessions")
|
||||
|
||||
self.query(
|
||||
"""INSERT INTO recorder_runs (start, created, utc_offset)
|
||||
VALUES (?, ?, ?)""",
|
||||
(self.recording_start, dt_util.utcnow(), self.utc_offset))
|
||||
|
||||
def _close_run(self):
|
||||
"""Save end time for current run."""
|
||||
self.query(
|
||||
"UPDATE recorder_runs SET end=? WHERE start=?",
|
||||
(dt_util.utcnow(), self.recording_start))
|
||||
|
||||
def _purge_old_data(self):
|
||||
"""Purge events and states older than purge_days ago."""
|
||||
if not self.purge_days or self.purge_days < 1:
|
||||
_LOGGER.debug("purge_days set to %s, will not purge any old data.",
|
||||
self.purge_days)
|
||||
return
|
||||
|
||||
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
|
||||
|
||||
_LOGGER.info("Purging events created before %s", purge_before)
|
||||
deleted_rows = self.query(
|
||||
sql_query="DELETE FROM events WHERE created < ?;",
|
||||
data=(int(purge_before.timestamp()),),
|
||||
return_value=RETURN_ROWCOUNT)
|
||||
_LOGGER.debug("Deleted %s events", deleted_rows)
|
||||
|
||||
_LOGGER.info("Purging states created before %s", purge_before)
|
||||
deleted_rows = self.query(
|
||||
sql_query="DELETE FROM states WHERE created < ?;",
|
||||
data=(int(purge_before.timestamp()),),
|
||||
return_value=RETURN_ROWCOUNT)
|
||||
_LOGGER.debug("Deleted %s states", deleted_rows)
|
||||
|
||||
# Execute sqlite vacuum command to free up space on disk
|
||||
self.query("VACUUM;")
|
||||
|
||||
|
||||
def _adapt_datetime(datetimestamp):
|
||||
"""Turn a datetime into an integer for in the DB."""
|
||||
return dt_util.as_utc(datetimestamp).timestamp()
|
||||
|
||||
|
||||
def _verify_instance():
|
||||
"""Throw error if recorder not initialized."""
|
||||
if _INSTANCE is None:
|
||||
raise RuntimeError("Recorder not initialized.")
|
337
homeassistant/components/recorder/__init__.py
Normal file
337
homeassistant/components/recorder/__init__.py
Normal file
@ -0,0 +1,337 @@
|
||||
"""
|
||||
Support for recording details.
|
||||
|
||||
Component that records all events and state changes. Allows other components
|
||||
to query this database.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/recorder/
|
||||
"""
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
EVENT_TIME_CHANGED, MATCH_ALL)
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
|
||||
DOMAIN = "recorder"
|
||||
|
||||
REQUIREMENTS = ['sqlalchemy==1.0.14']
|
||||
|
||||
DEFAULT_URL = "sqlite:///{hass_config_path}"
|
||||
DEFAULT_DB_FILE = "home-assistant_v2.db"
|
||||
|
||||
CONF_DB_URL = "db_url"
|
||||
CONF_PURGE_DAYS = "purge_days"
|
||||
|
||||
RETRIES = 3
|
||||
CONNECT_RETRY_WAIT = 10
|
||||
QUERY_RETRY_WAIT = 0.1
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1)),
|
||||
vol.Optional(CONF_DB_URL): vol.Url(''),
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_INSTANCE = None
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# These classes will be populated during setup()
|
||||
# pylint: disable=invalid-name
|
||||
Session = None
|
||||
|
||||
|
||||
def execute(q):
|
||||
"""Query the database and convert the objects to HA native form.
|
||||
|
||||
This method also retries a few times in the case of stale connections.
|
||||
"""
|
||||
import sqlalchemy.exc
|
||||
for _ in range(0, RETRIES):
|
||||
try:
|
||||
return [
|
||||
row for row in
|
||||
(row.to_native() for row in q)
|
||||
if row is not None]
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True)
|
||||
return []
|
||||
|
||||
|
||||
def run_information(point_in_time=None):
|
||||
"""Return information about current run.
|
||||
|
||||
There is also the run that covers point_in_time.
|
||||
"""
|
||||
_verify_instance()
|
||||
|
||||
recorder_runs = get_model('RecorderRuns')
|
||||
if point_in_time is None or point_in_time > _INSTANCE.recording_start:
|
||||
return recorder_runs(
|
||||
end=None,
|
||||
start=_INSTANCE.recording_start,
|
||||
closed_incorrect=False)
|
||||
|
||||
return query('RecorderRuns').filter(
|
||||
(recorder_runs.start < point_in_time) &
|
||||
(recorder_runs.end > point_in_time)).first()
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the recorder."""
|
||||
# pylint: disable=global-statement
|
||||
# pylint: disable=too-many-locals
|
||||
global _INSTANCE
|
||||
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
|
||||
|
||||
db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None)
|
||||
if not db_url:
|
||||
db_url = DEFAULT_URL.format(
|
||||
hass_config_path=hass.config.path(DEFAULT_DB_FILE))
|
||||
|
||||
_INSTANCE = Recorder(hass, purge_days=purge_days, uri=db_url)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def query(model_name, *args):
|
||||
"""Helper to return a query handle."""
|
||||
if isinstance(model_name, str):
|
||||
return Session().query(get_model(model_name), *args)
|
||||
return Session().query(model_name, *args)
|
||||
|
||||
|
||||
def get_model(model_name):
|
||||
"""Get a model class."""
|
||||
from homeassistant.components.recorder import models
|
||||
|
||||
return getattr(models, model_name)
|
||||
|
||||
|
||||
def log_error(e, retry_wait=0, rollback=True,
|
||||
message="Error during query: %s"):
|
||||
"""Log about SQLAlchemy errors in a sane manner."""
|
||||
import sqlalchemy.exc
|
||||
if not isinstance(e, sqlalchemy.exc.OperationalError):
|
||||
_LOGGER.exception(e)
|
||||
else:
|
||||
_LOGGER.error(message, str(e))
|
||||
if rollback:
|
||||
Session().rollback()
|
||||
if retry_wait:
|
||||
_LOGGER.info("Retrying failed query in %s seconds", QUERY_RETRY_WAIT)
|
||||
time.sleep(retry_wait)
|
||||
|
||||
|
||||
class Recorder(threading.Thread):
|
||||
"""A threaded recorder class."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, hass, purge_days, uri):
|
||||
"""Initialize the recorder."""
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.hass = hass
|
||||
self.purge_days = purge_days
|
||||
self.queue = queue.Queue()
|
||||
self.quit_object = object()
|
||||
self.recording_start = dt_util.utcnow()
|
||||
self.db_url = uri
|
||||
self.db_ready = threading.Event()
|
||||
self.engine = None
|
||||
self._run = None
|
||||
|
||||
def start_recording(event):
|
||||
"""Start recording."""
|
||||
self.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_recording)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
|
||||
hass.bus.listen(MATCH_ALL, self.event_listener)
|
||||
|
||||
def run(self):
|
||||
"""Start processing events to save."""
|
||||
from homeassistant.components.recorder.models import Events, States
|
||||
import sqlalchemy.exc
|
||||
|
||||
global _INSTANCE
|
||||
|
||||
while True:
|
||||
try:
|
||||
self._setup_connection()
|
||||
self._setup_run()
|
||||
break
|
||||
except sqlalchemy.exc.SQLAlchemyError as e:
|
||||
log_error(e, retry_wait=CONNECT_RETRY_WAIT, rollback=False,
|
||||
message="Error during connection setup: %s")
|
||||
|
||||
if self.purge_days is not None:
|
||||
track_point_in_utc_time(self.hass,
|
||||
lambda now: self._purge_old_data(),
|
||||
dt_util.utcnow() + timedelta(minutes=5))
|
||||
|
||||
while True:
|
||||
event = self.queue.get()
|
||||
|
||||
if event == self.quit_object:
|
||||
self._close_run()
|
||||
self._close_connection()
|
||||
_INSTANCE = None
|
||||
self.queue.task_done()
|
||||
return
|
||||
|
||||
elif event.event_type == EVENT_TIME_CHANGED:
|
||||
self.queue.task_done()
|
||||
continue
|
||||
|
||||
session = Session()
|
||||
dbevent = Events.from_event(event)
|
||||
session.add(dbevent)
|
||||
|
||||
for _ in range(0, RETRIES):
|
||||
try:
|
||||
session.commit()
|
||||
break
|
||||
except sqlalchemy.exc.OperationalError as e:
|
||||
log_error(e, retry_wait=QUERY_RETRY_WAIT,
|
||||
rollback=True)
|
||||
|
||||
if event.event_type != EVENT_STATE_CHANGED:
|
||||
self.queue.task_done()
|
||||
continue
|
||||
|
||||
session = Session()
|
||||
dbstate = States.from_event(event)
|
||||
|
||||
for _ in range(0, RETRIES):
|
||||
try:
|
||||
dbstate.event_id = dbevent.event_id
|
||||
session.add(dbstate)
|
||||
session.commit()
|
||||
break
|
||||
except sqlalchemy.exc.OperationalError as e:
|
||||
log_error(e, retry_wait=QUERY_RETRY_WAIT,
|
||||
rollback=True)
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
def event_listener(self, event):
|
||||
"""Listen for new events and put them in the process queue."""
|
||||
self.queue.put(event)
|
||||
|
||||
def shutdown(self, event):
|
||||
"""Tell the recorder to shut down."""
|
||||
self.queue.put(self.quit_object)
|
||||
self.queue.join()
|
||||
|
||||
def block_till_done(self):
|
||||
"""Block till all events processed."""
|
||||
self.queue.join()
|
||||
|
||||
def block_till_db_ready(self):
|
||||
"""Block until the database session is ready."""
|
||||
self.db_ready.wait()
|
||||
|
||||
def _setup_connection(self):
|
||||
"""Ensure database is ready to fly."""
|
||||
# pylint: disable=global-statement
|
||||
global Session
|
||||
|
||||
import homeassistant.components.recorder.models as models
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
if self.db_url == 'sqlite://' or ':memory:' in self.db_url:
|
||||
from sqlalchemy.pool import StaticPool
|
||||
self.engine = create_engine(
|
||||
'sqlite://',
|
||||
connect_args={'check_same_thread': False},
|
||||
poolclass=StaticPool)
|
||||
else:
|
||||
self.engine = create_engine(self.db_url, echo=False)
|
||||
|
||||
models.Base.metadata.create_all(self.engine)
|
||||
session_factory = sessionmaker(bind=self.engine)
|
||||
Session = scoped_session(session_factory)
|
||||
self.db_ready.set()
|
||||
|
||||
def _close_connection(self):
|
||||
"""Close the connection."""
|
||||
global Session
|
||||
self.engine.dispose()
|
||||
self.engine = None
|
||||
Session = None
|
||||
|
||||
def _setup_run(self):
|
||||
"""Log the start of the current run."""
|
||||
recorder_runs = get_model('RecorderRuns')
|
||||
for run in query('RecorderRuns').filter_by(end=None):
|
||||
run.closed_incorrect = True
|
||||
run.end = self.recording_start
|
||||
_LOGGER.warning("Ended unfinished session (id=%s from %s)",
|
||||
run.run_id, run.start)
|
||||
Session().add(run)
|
||||
|
||||
_LOGGER.warning("Found unfinished sessions")
|
||||
|
||||
self._run = recorder_runs(
|
||||
start=self.recording_start,
|
||||
created=dt_util.utcnow()
|
||||
)
|
||||
session = Session()
|
||||
session.add(self._run)
|
||||
session.commit()
|
||||
|
||||
def _close_run(self):
|
||||
"""Save end time for current run."""
|
||||
self._run.end = dt_util.utcnow()
|
||||
session = Session()
|
||||
session.add(self._run)
|
||||
session.commit()
|
||||
self._run = None
|
||||
|
||||
def _purge_old_data(self):
|
||||
"""Purge events and states older than purge_days ago."""
|
||||
from homeassistant.components.recorder.models import Events, States
|
||||
|
||||
if not self.purge_days or self.purge_days < 1:
|
||||
_LOGGER.debug("purge_days set to %s, will not purge any old data.",
|
||||
self.purge_days)
|
||||
return
|
||||
|
||||
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
|
||||
|
||||
_LOGGER.info("Purging events created before %s", purge_before)
|
||||
deleted_rows = Session().query(Events).filter(
|
||||
(Events.created < purge_before)).delete(synchronize_session=False)
|
||||
_LOGGER.debug("Deleted %s events", deleted_rows)
|
||||
|
||||
_LOGGER.info("Purging states created before %s", purge_before)
|
||||
deleted_rows = Session().query(States).filter(
|
||||
(States.created < purge_before)).delete(synchronize_session=False)
|
||||
_LOGGER.debug("Deleted %s states", deleted_rows)
|
||||
|
||||
Session().commit()
|
||||
Session().expire_all()
|
||||
|
||||
# Execute sqlite vacuum command to free up space on disk
|
||||
if self.engine.driver == 'sqlite':
|
||||
_LOGGER.info("Vacuuming SQLite to free space")
|
||||
self.engine.execute("VACUUM")
|
||||
|
||||
|
||||
def _verify_instance():
|
||||
"""Throw error if recorder not initialized."""
|
||||
if _INSTANCE is None:
|
||||
raise RuntimeError("Recorder not initialized.")
|
162
homeassistant/components/recorder/models.py
Normal file
162
homeassistant/components/recorder/models.py
Normal file
@ -0,0 +1,162 @@
|
||||
"""Models for SQLAlchemy."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer,
|
||||
String, Text, distinct)
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.core import Event, EventOrigin, State
|
||||
from homeassistant.remote import JSONEncoder
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
|
||||
# SQLAlchemy Schema
|
||||
# pylint: disable=invalid-name
|
||||
Base = declarative_base()
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Events(Base):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Event history data."""
|
||||
|
||||
__tablename__ = 'events'
|
||||
event_id = Column(Integer, primary_key=True)
|
||||
event_type = Column(String(32), index=True)
|
||||
event_data = Column(Text)
|
||||
origin = Column(String(32))
|
||||
time_fired = Column(DateTime(timezone=True))
|
||||
created = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
|
||||
@staticmethod
|
||||
def from_event(event):
|
||||
"""Create an event database object from a native event."""
|
||||
return Events(event_type=event.event_type,
|
||||
event_data=json.dumps(event.data, cls=JSONEncoder),
|
||||
origin=str(event.origin),
|
||||
time_fired=event.time_fired)
|
||||
|
||||
def to_native(self):
|
||||
"""Convert to a natve HA Event."""
|
||||
try:
|
||||
return Event(
|
||||
self.event_type,
|
||||
json.loads(self.event_data),
|
||||
EventOrigin(self.origin),
|
||||
_process_timestamp(self.time_fired)
|
||||
)
|
||||
except ValueError:
|
||||
# When json.loads fails
|
||||
_LOGGER.exception("Error converting to event: %s", self)
|
||||
return None
|
||||
|
||||
|
||||
class States(Base):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""State change history."""
|
||||
|
||||
__tablename__ = 'states'
|
||||
state_id = Column(Integer, primary_key=True)
|
||||
domain = Column(String(64))
|
||||
entity_id = Column(String(64))
|
||||
state = Column(String(255))
|
||||
attributes = Column(Text)
|
||||
event_id = Column(Integer, ForeignKey('events.event_id'))
|
||||
last_changed = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
last_updated = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
created = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (Index('states__state_changes',
|
||||
'last_changed', 'last_updated', 'entity_id'),
|
||||
Index('states__significant_changes',
|
||||
'domain', 'last_updated', 'entity_id'), )
|
||||
|
||||
@staticmethod
|
||||
def from_event(event):
|
||||
"""Create object from a state_changed event."""
|
||||
entity_id = event.data['entity_id']
|
||||
state = event.data.get('new_state')
|
||||
|
||||
dbstate = States(entity_id=entity_id)
|
||||
|
||||
# State got deleted
|
||||
if state is None:
|
||||
dbstate.state = ''
|
||||
dbstate.domain = split_entity_id(entity_id)[0]
|
||||
dbstate.attributes = '{}'
|
||||
dbstate.last_changed = event.time_fired
|
||||
dbstate.last_updated = event.time_fired
|
||||
else:
|
||||
dbstate.domain = state.domain
|
||||
dbstate.state = state.state
|
||||
dbstate.attributes = json.dumps(dict(state.attributes))
|
||||
dbstate.last_changed = state.last_changed
|
||||
dbstate.last_updated = state.last_updated
|
||||
|
||||
return dbstate
|
||||
|
||||
def to_native(self):
|
||||
"""Convert to an HA state object."""
|
||||
try:
|
||||
return State(
|
||||
self.entity_id, self.state,
|
||||
json.loads(self.attributes),
|
||||
_process_timestamp(self.last_changed),
|
||||
_process_timestamp(self.last_updated)
|
||||
)
|
||||
except ValueError:
|
||||
# When json.loads fails
|
||||
_LOGGER.exception("Error converting row to state: %s", self)
|
||||
return None
|
||||
|
||||
|
||||
class RecorderRuns(Base):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Representation of recorder run."""
|
||||
|
||||
__tablename__ = 'recorder_runs'
|
||||
run_id = Column(Integer, primary_key=True)
|
||||
start = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
end = Column(DateTime(timezone=True))
|
||||
closed_incorrect = Column(Boolean, default=False)
|
||||
created = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
|
||||
def entity_ids(self, point_in_time=None):
|
||||
"""Return the entity ids that existed in this run.
|
||||
|
||||
Specify point_in_time if you want to know which existed at that point
|
||||
in time inside the run.
|
||||
"""
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
session = Session.object_session(self)
|
||||
|
||||
assert session is not None, 'RecorderRuns need to be persisted'
|
||||
|
||||
query = session.query(distinct(States.entity_id)).filter(
|
||||
States.last_updated >= self.start)
|
||||
|
||||
if point_in_time is not None:
|
||||
query = query.filter(States.last_updated < point_in_time)
|
||||
elif self.end is not None:
|
||||
query = query.filter(States.last_updated < self.end)
|
||||
|
||||
return [row[0] for row in query]
|
||||
|
||||
def to_native(self):
|
||||
"""Return self, native format is this model."""
|
||||
return self
|
||||
|
||||
|
||||
def _process_timestamp(ts):
|
||||
"""Process a timestamp into datetime object."""
|
||||
if ts is None:
|
||||
return None
|
||||
elif ts.tzinfo is None:
|
||||
return dt_util.UTC.localize(ts)
|
||||
else:
|
||||
return dt_util.as_utc(ts)
|
@ -14,7 +14,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
|
||||
|
||||
REQUIREMENTS = ['pyRFXtrx==0.8.0']
|
||||
REQUIREMENTS = ['pyRFXtrx==0.9.0']
|
||||
|
||||
DOMAIN = "rfxtrx"
|
||||
|
||||
@ -40,6 +40,7 @@ DATA_TYPES = OrderedDict([
|
||||
('Rain rate', ''),
|
||||
('Energy usage', 'W'),
|
||||
('Total usage', 'W'),
|
||||
('Sound', ''),
|
||||
('Sensor Status', ''),
|
||||
('Unknown', '')])
|
||||
|
||||
@ -65,6 +66,9 @@ def _valid_device(value, device_type):
|
||||
key = device.get('packetid')
|
||||
device.pop('packetid')
|
||||
|
||||
if not len(key) % 2 == 0:
|
||||
key = '0' + key
|
||||
|
||||
if get_rfx_object(key) is None:
|
||||
raise vol.Invalid('Rfxtrx device {} is invalid: '
|
||||
'Invalid device id for {}'.format(key, value))
|
||||
@ -159,7 +163,11 @@ def get_rfx_object(packetid):
|
||||
"""Return the RFXObject with the packetid."""
|
||||
import RFXtrx as rfxtrxmod
|
||||
|
||||
binarypacket = bytearray.fromhex(packetid)
|
||||
try:
|
||||
binarypacket = bytearray.fromhex(packetid)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
pkt = rfxtrxmod.lowlevel.parse(binarypacket)
|
||||
if pkt is None:
|
||||
return None
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.rollershutter import RollershutterDevice
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -11,33 +11,109 @@ from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DEPENDENCIES = [apcupsd.DOMAIN]
|
||||
DEFAULT_NAME = "UPS Status"
|
||||
|
||||
SENSOR_PREFIX = 'UPS '
|
||||
SENSOR_TYPES = {
|
||||
'alarmdel': ['Alarm Delay', '', 'mdi:alarm'],
|
||||
'ambtemp': ['Ambient Temperature', '', 'mdi:thermometer'],
|
||||
'apc': ['Status Data', '', 'mdi:information-outline'],
|
||||
'apcmodel': ['Model', '', 'mdi:information-outline'],
|
||||
'badbatts': ['Bad Batteries', '', 'mdi:information-outline'],
|
||||
'battdate': ['Battery Replaced', '', 'mdi:calendar-clock'],
|
||||
'battstat': ['Battery Status', '', 'mdi:information-outline'],
|
||||
'battv': ['Battery Voltage', 'V', 'mdi:flash'],
|
||||
'bcharge': ['Battery', '%', 'mdi:battery'],
|
||||
'cable': ['Cable Type', '', 'mdi:ethernet-cable'],
|
||||
'cumonbatt': ['Total Time on Battery', '', 'mdi:timer'],
|
||||
'date': ['Status Date', '', 'mdi:calendar-clock'],
|
||||
'dipsw': ['Dip Switch Settings', '', 'mdi:information-outline'],
|
||||
'dlowbatt': ['Low Battery Signal', '', 'mdi:clock-alert'],
|
||||
'driver': ['Driver', '', 'mdi:information-outline'],
|
||||
'dshutd': ['Shutdown Delay', '', 'mdi:timer'],
|
||||
'dwake': ['Wake Delay', '', 'mdi:timer'],
|
||||
'endapc': ['Date and Time', '', 'mdi:calendar-clock'],
|
||||
'extbatts': ['External Batteries', '', 'mdi:information-outline'],
|
||||
'firmware': ['Firmware Version', '', 'mdi:information-outline'],
|
||||
'hitrans': ['Transfer High', 'V', 'mdi:flash'],
|
||||
'hostname': ['Hostname', '', 'mdi:information-outline'],
|
||||
'humidity': ['Ambient Humidity', '%', 'mdi:water-percent'],
|
||||
'itemp': ['Internal Temperature', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||
'lastxfer': ['Last Transfer', '', 'mdi:transfer'],
|
||||
'linefail': ['Input Voltage Status', '', 'mdi:information-outline'],
|
||||
'linefreq': ['Line Frequency', 'Hz', 'mdi:information-outline'],
|
||||
'linev': ['Input Voltage', 'V', 'mdi:flash'],
|
||||
'loadpct': ['Load', '%', 'mdi:gauge'],
|
||||
'lotrans': ['Transfer Low', 'V', 'mdi:flash'],
|
||||
'mandate': ['Manufacture Date', '', 'mdi:calendar'],
|
||||
'masterupd': ['Master Update', '', 'mdi:information-outline'],
|
||||
'maxlinev': ['Input Voltage High', 'V', 'mdi:flash'],
|
||||
'maxtime': ['Battery Timeout', '', 'mdi:timer-off'],
|
||||
'mbattchg': ['Battery Shutdown', '%', 'mdi:battery-alert'],
|
||||
'minlinev': ['Input Voltage Low', 'V', 'mdi:flash'],
|
||||
'mintimel': ['Shutdown Time', '', 'mdi:timer'],
|
||||
'model': ['Model', '', 'mdi:information-outline'],
|
||||
'nombattv': ['Battery Nominal Voltage', 'V', 'mdi:flash'],
|
||||
'nominv': ['Nominal Input Voltage', 'V', 'mdi:flash'],
|
||||
'nomoutv': ['Nominal Output Voltage', 'V', 'mdi:flash'],
|
||||
'nompower': ['Nominal Output Power', 'W', 'mdi:flash'],
|
||||
'numxfers': ['Transfer Count', '', 'mdi:counter'],
|
||||
'outputv': ['Output Voltage', 'V', 'mdi:flash'],
|
||||
'reg1': ['Register 1 Fault', '', 'mdi:information-outline'],
|
||||
'reg2': ['Register 2 Fault', '', 'mdi:information-outline'],
|
||||
'reg3': ['Register 3 Fault', '', 'mdi:information-outline'],
|
||||
'retpct': ['Restore Requirement', '%', 'mdi:battery-alert'],
|
||||
'selftest': ['Last Self Test', '', 'mdi:calendar-clock'],
|
||||
'sense': ['Sensitivity', '', 'mdi:information-outline'],
|
||||
'serialno': ['Serial Number', '', 'mdi:information-outline'],
|
||||
'starttime': ['Startup Time', '', 'mdi:calendar-clock'],
|
||||
'statflag': ['Status Flag', '', 'mdi:information-outline'],
|
||||
'status': ['Status', '', 'mdi:information-outline'],
|
||||
'stesti': ['Self Test Interval', '', 'mdi:information-outline'],
|
||||
'timeleft': ['Time Left', '', 'mdi:clock-alert'],
|
||||
'tonbatt': ['Time on Battery', '', 'mdi:timer'],
|
||||
'upsmode': ['Mode', '', 'mdi:information-outline'],
|
||||
'upsname': ['Name', '', 'mdi:information-outline'],
|
||||
'version': ['Daemon Info', '', 'mdi:information-outline'],
|
||||
'xoffbat': ['Transfer from Battery', '', 'mdi:transfer'],
|
||||
'xoffbatt': ['Transfer from Battery', '', 'mdi:transfer'],
|
||||
'xonbatt': ['Transfer to Battery', '', 'mdi:transfer'],
|
||||
}
|
||||
|
||||
SPECIFIC_UNITS = {
|
||||
"ITEMP": TEMP_CELSIUS
|
||||
'ITEMP': TEMP_CELSIUS
|
||||
}
|
||||
INFERRED_UNITS = {
|
||||
' Minutes': 'min',
|
||||
' Seconds': 'sec',
|
||||
' Percent': '%',
|
||||
' Volts': 'V',
|
||||
' Watts': 'W',
|
||||
' Hz': 'Hz',
|
||||
' C': TEMP_CELSIUS,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup the APCUPSd sensor."""
|
||||
typ = config.get(apcupsd.CONF_TYPE)
|
||||
if typ is None:
|
||||
_LOGGER.error(
|
||||
"You must include a '%s' when configuring an APCUPSd sensor.",
|
||||
apcupsd.CONF_TYPE)
|
||||
return False
|
||||
typ = typ.upper()
|
||||
"""Setup the APCUPSd sensors."""
|
||||
entities = []
|
||||
|
||||
if typ not in apcupsd.DATA.status:
|
||||
_LOGGER.error(
|
||||
"Specified '%s' of '%s' does not appear in the APCUPSd status "
|
||||
"output.", apcupsd.CONF_TYPE, typ)
|
||||
return False
|
||||
for resource in config['resources']:
|
||||
sensor_type = resource.lower()
|
||||
|
||||
add_entities((
|
||||
Sensor(config, apcupsd.DATA, unit=SPECIFIC_UNITS.get(typ)),
|
||||
))
|
||||
if sensor_type not in SENSOR_TYPES:
|
||||
SENSOR_TYPES[sensor_type] = [
|
||||
sensor_type.title(), '', 'mdi:information-outline']
|
||||
|
||||
if sensor_type.upper() not in apcupsd.DATA.status:
|
||||
_LOGGER.warning(
|
||||
'Sensor type: "%s" does not appear in the APCUPSd status '
|
||||
'output.', sensor_type)
|
||||
|
||||
entities.append(APCUPSdSensor(apcupsd.DATA, sensor_type))
|
||||
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
def infer_unit(value):
|
||||
@ -49,25 +125,31 @@ def infer_unit(value):
|
||||
from apcaccess.status import ALL_UNITS
|
||||
for unit in ALL_UNITS:
|
||||
if value.endswith(unit):
|
||||
return value[:-len(unit)], unit
|
||||
return value[:-len(unit)], INFERRED_UNITS.get(unit, unit.strip())
|
||||
return value, None
|
||||
|
||||
|
||||
class Sensor(Entity):
|
||||
class APCUPSdSensor(Entity):
|
||||
"""Representation of a sensor entity for APCUPSd status values."""
|
||||
|
||||
def __init__(self, config, data, unit=None):
|
||||
def __init__(self, data, sensor_type):
|
||||
"""Initialize the sensor."""
|
||||
self._config = config
|
||||
self._unit = unit
|
||||
self._data = data
|
||||
self.type = sensor_type
|
||||
self._name = SENSOR_PREFIX + SENSOR_TYPES[sensor_type][0]
|
||||
self._unit = SENSOR_TYPES[sensor_type][1]
|
||||
self._inferred_unit = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the UPS sensor."""
|
||||
return self._config.get("name", DEFAULT_NAME)
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return SENSOR_TYPES[self.type][2]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@ -77,11 +159,15 @@ class Sensor(Entity):
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
if self._unit is None:
|
||||
if not self._unit:
|
||||
return self._inferred_unit
|
||||
return self._unit
|
||||
|
||||
def update(self):
|
||||
"""Get the latest status and use it to update our sensor state."""
|
||||
key = self._config[apcupsd.CONF_TYPE].upper()
|
||||
self._state, self._inferred_unit = infer_unit(self._data.status[key])
|
||||
if self.type.upper() not in self._data.status:
|
||||
self._state = None
|
||||
self._inferred_unit = None
|
||||
else:
|
||||
self._state, self._inferred_unit = infer_unit(
|
||||
self._data.status[self.type.upper()])
|
||||
|
@ -13,6 +13,7 @@ from homeassistant.const import (CONF_PLATFORM)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['schiene==0.17']
|
||||
|
||||
@ -88,6 +89,7 @@ class SchieneData(object):
|
||||
def __init__(self, start, goal):
|
||||
"""Initialize the sensor."""
|
||||
import schiene
|
||||
|
||||
self.start = start
|
||||
self.goal = goal
|
||||
self.schiene = schiene.Schiene()
|
||||
@ -96,7 +98,8 @@ class SchieneData(object):
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update the connection data."""
|
||||
self.connections = self.schiene.connections(self.start, self.goal)
|
||||
self.connections = self.schiene.connections(
|
||||
self.start, self.goal, dt_util.as_local(dt_util.utcnow()))
|
||||
|
||||
for con in self.connections:
|
||||
# Detail info is not useful. Having a more consistent interface
|
||||
|
@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
|
||||
class EnvisalinkSensor(EnvisalinkDevice):
|
||||
"""Representation of an envisalink keypad."""
|
||||
"""Representation of an Envisalink keypad."""
|
||||
|
||||
def __init__(self, partition_name, partition_number, info, controller):
|
||||
"""Initialize the sensor."""
|
||||
|
@ -10,14 +10,18 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE)
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.location as location
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['googlemaps==2.4.3']
|
||||
REQUIREMENTS = ['googlemaps==2.4.4']
|
||||
|
||||
# Return cached results if last update was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
@ -65,6 +69,8 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
}))
|
||||
})
|
||||
|
||||
TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone"]
|
||||
|
||||
|
||||
def convert_time_to_utc(timestr):
|
||||
"""Take a string like 08:00:00 and convert it to a unix timestamp."""
|
||||
@ -78,36 +84,44 @@ def convert_time_to_utc(timestr):
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the travel time platform."""
|
||||
# pylint: disable=too-many-locals
|
||||
options = config.get(CONF_OPTIONS)
|
||||
def run_setup(event):
|
||||
"""Delay the setup until home assistant is fully initialized.
|
||||
|
||||
if options.get('units') is None:
|
||||
if hass.config.temperature_unit is TEMP_CELSIUS:
|
||||
options['units'] = 'metric'
|
||||
elif hass.config.temperature_unit is TEMP_FAHRENHEIT:
|
||||
options['units'] = 'imperial'
|
||||
This allows any entities to be created already
|
||||
"""
|
||||
options = config.get(CONF_OPTIONS)
|
||||
|
||||
travel_mode = config.get(CONF_TRAVEL_MODE)
|
||||
mode = options.get(CONF_MODE)
|
||||
if options.get('units') is None:
|
||||
if hass.config.temperature_unit is TEMP_CELSIUS:
|
||||
options['units'] = 'metric'
|
||||
elif hass.config.temperature_unit is TEMP_FAHRENHEIT:
|
||||
options['units'] = 'imperial'
|
||||
|
||||
if travel_mode is not None:
|
||||
wstr = ("Google Travel Time: travel_mode is deprecated, please add "
|
||||
"mode to the options dictionary instead!")
|
||||
_LOGGER.warning(wstr)
|
||||
if mode is None:
|
||||
options[CONF_MODE] = travel_mode
|
||||
travel_mode = config.get(CONF_TRAVEL_MODE)
|
||||
mode = options.get(CONF_MODE)
|
||||
|
||||
titled_mode = options.get(CONF_MODE).title()
|
||||
formatted_name = "Google Travel Time - {}".format(titled_mode)
|
||||
name = config.get(CONF_NAME, formatted_name)
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
origin = config.get(CONF_ORIGIN)
|
||||
destination = config.get(CONF_DESTINATION)
|
||||
if travel_mode is not None:
|
||||
wstr = ("Google Travel Time: travel_mode is deprecated, please "
|
||||
"add mode to the options dictionary instead!")
|
||||
_LOGGER.warning(wstr)
|
||||
if mode is None:
|
||||
options[CONF_MODE] = travel_mode
|
||||
|
||||
sensor = GoogleTravelTimeSensor(name, api_key, origin, destination,
|
||||
options)
|
||||
titled_mode = options.get(CONF_MODE).title()
|
||||
formatted_name = "Google Travel Time - {}".format(titled_mode)
|
||||
name = config.get(CONF_NAME, formatted_name)
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
origin = config.get(CONF_ORIGIN)
|
||||
destination = config.get(CONF_DESTINATION)
|
||||
|
||||
if sensor.valid_api_connection:
|
||||
add_devices_callback([sensor])
|
||||
sensor = GoogleTravelTimeSensor(hass, name, api_key, origin,
|
||||
destination, options)
|
||||
|
||||
if sensor.valid_api_connection:
|
||||
add_devices_callback([sensor])
|
||||
|
||||
# Wait until start event is sent to load this component.
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
@ -115,15 +129,25 @@ class GoogleTravelTimeSensor(Entity):
|
||||
"""Representation of a tavel time sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, api_key, origin, destination, options):
|
||||
def __init__(self, hass, name, api_key, origin, destination, options):
|
||||
"""Initialize the sensor."""
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._options = options
|
||||
self._origin = origin
|
||||
self._destination = destination
|
||||
self._matrix = None
|
||||
self.valid_api_connection = True
|
||||
|
||||
# Check if location is a trackable entity
|
||||
if origin.split('.', 1)[0] in TRACKABLE_DOMAINS:
|
||||
self._origin_entity_id = origin
|
||||
else:
|
||||
self._origin = origin
|
||||
|
||||
if destination.split('.', 1)[0] in TRACKABLE_DOMAINS:
|
||||
self._destination_entity_id = destination
|
||||
else:
|
||||
self._destination = destination
|
||||
|
||||
import googlemaps
|
||||
self._client = googlemaps.Client(api_key, timeout=10)
|
||||
try:
|
||||
@ -136,6 +160,9 @@ class GoogleTravelTimeSensor(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._matrix is None:
|
||||
return None
|
||||
|
||||
_data = self._matrix['rows'][0]['elements'][0]
|
||||
if 'duration_in_traffic' in _data:
|
||||
return round(_data['duration_in_traffic']['value']/60)
|
||||
@ -151,6 +178,9 @@ class GoogleTravelTimeSensor(Entity):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._matrix is None:
|
||||
return None
|
||||
|
||||
res = self._matrix.copy()
|
||||
res.update(self._options)
|
||||
del res['rows']
|
||||
@ -186,6 +216,64 @@ class GoogleTravelTimeSensor(Entity):
|
||||
elif atime is not None:
|
||||
options_copy['arrival_time'] = atime
|
||||
|
||||
self._matrix = self._client.distance_matrix(self._origin,
|
||||
self._destination,
|
||||
**options_copy)
|
||||
# Convert device_trackers to google friendly location
|
||||
if hasattr(self, '_origin_entity_id'):
|
||||
self._origin = self._get_location_from_entity(
|
||||
self._origin_entity_id
|
||||
)
|
||||
|
||||
if hasattr(self, '_destination_entity_id'):
|
||||
self._destination = self._get_location_from_entity(
|
||||
self._destination_entity_id
|
||||
)
|
||||
|
||||
self._destination = self._resolve_zone(self._destination)
|
||||
self._origin = self._resolve_zone(self._origin)
|
||||
|
||||
if self._destination is not None and self._origin is not None:
|
||||
self._matrix = self._client.distance_matrix(self._origin,
|
||||
self._destination,
|
||||
**options_copy)
|
||||
|
||||
def _get_location_from_entity(self, entity_id):
|
||||
"""Get the location from the entity state or attributes."""
|
||||
entity = self._hass.states.get(entity_id)
|
||||
|
||||
if entity is None:
|
||||
_LOGGER.error("Unable to find entity %s", entity_id)
|
||||
self.valid_api_connection = False
|
||||
return None
|
||||
|
||||
# Check if device is in a zone
|
||||
zone_entity = self._hass.states.get("zone.%s" % entity.state)
|
||||
if location.has_location(zone_entity):
|
||||
_LOGGER.debug(
|
||||
"%s is in %s, getting zone location.",
|
||||
entity_id, zone_entity.entity_id
|
||||
)
|
||||
return self._get_location_from_attributes(zone_entity)
|
||||
|
||||
# If zone was not found in state then use the state as the location
|
||||
if entity_id.startswith("sensor."):
|
||||
return entity.state
|
||||
|
||||
# For everything else look for location attributes
|
||||
if location.has_location(entity):
|
||||
return self._get_location_from_attributes(entity)
|
||||
|
||||
# When everything fails just return nothing
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_location_from_attributes(entity):
|
||||
"""Get the lat/long string from an entities attributes."""
|
||||
attr = entity.attributes
|
||||
return "%s,%s" % (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE))
|
||||
|
||||
def _resolve_zone(self, friendly_name):
|
||||
entities = self._hass.states.all()
|
||||
for entity in entities:
|
||||
if entity.domain == 'zone' and entity.name == friendly_name:
|
||||
return self._get_location_from_attributes(entity)
|
||||
|
||||
return friendly_name
|
||||
|
@ -28,7 +28,10 @@ HM_UNIT_HA_CAST = {
|
||||
"POWER": "W",
|
||||
"CURRENT": "mA",
|
||||
"VOLTAGE": "V",
|
||||
"ENERGY_COUNTER": "Wh"
|
||||
"ENERGY_COUNTER": "Wh",
|
||||
"GAS_POWER": "m3",
|
||||
"GAS_ENERGY_COUNTER": "m3",
|
||||
"LUX": "lux"
|
||||
}
|
||||
|
||||
|
||||
|
105
homeassistant/components/sensor/imap.py
Normal file
105
homeassistant/components/sensor/imap.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""
|
||||
IMAP sensor support.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.imap/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ICON = 'mdi:email-outline'
|
||||
|
||||
CONF_USER = "user"
|
||||
CONF_PASSWORD = "password"
|
||||
CONF_SERVER = "server"
|
||||
CONF_PORT = "port"
|
||||
CONF_NAME = "name"
|
||||
|
||||
DEFAULT_PORT = 993
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required('platform'): 'imap',
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_USER): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_SERVER): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the IMAP platform."""
|
||||
sensor = ImapSensor(config.get(CONF_NAME, None),
|
||||
config.get(CONF_USER),
|
||||
config.get(CONF_PASSWORD),
|
||||
config.get(CONF_SERVER),
|
||||
config.get(CONF_PORT, DEFAULT_PORT))
|
||||
|
||||
if sensor.connection:
|
||||
add_devices([sensor])
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class ImapSensor(Entity):
|
||||
"""Representation of an IMAP sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, user, password, server, port):
|
||||
"""Initialize the sensor."""
|
||||
self._name = name or user
|
||||
self._user = user
|
||||
self._password = password
|
||||
self._server = server
|
||||
self._port = port
|
||||
self._unread_count = 0
|
||||
self.connection = self._login()
|
||||
self.update()
|
||||
|
||||
def _login(self):
|
||||
"""Login and return an IMAP connection."""
|
||||
import imaplib
|
||||
try:
|
||||
connection = imaplib.IMAP4_SSL(self._server, self._port)
|
||||
connection.login(self._user, self._password)
|
||||
return connection
|
||||
except imaplib.IMAP4.error:
|
||||
_LOGGER.error("Failed to login to %s.", self._server)
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the number of unread emails."""
|
||||
return self._unread_count
|
||||
|
||||
def update(self):
|
||||
"""Check the number of unread emails."""
|
||||
import imaplib
|
||||
try:
|
||||
self.connection.select()
|
||||
self._unread_count = len(self.connection.search(
|
||||
None, 'UnSeen')[1][0].split())
|
||||
except imaplib.IMAP4.abort:
|
||||
_LOGGER.info("Connection to %s lost, attempting to reconnect",
|
||||
self._server)
|
||||
try:
|
||||
self._login()
|
||||
self.update()
|
||||
except imaplib.IMAP4.error:
|
||||
_LOGGER.error("Failed to reconnect.")
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return ICON
|
@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "loopenergy"
|
||||
|
||||
REQUIREMENTS = ['pyloopenergy==0.0.13']
|
||||
REQUIREMENTS = ['pyloopenergy==0.0.14']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -15,7 +15,7 @@ DEPENDENCIES = []
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for sensors."""
|
||||
"""Setup the MySensors platform for sensors."""
|
||||
# Only act if loaded via mysensors by discovery event.
|
||||
# Otherwise gateway is not setup.
|
||||
if discovery_info is None:
|
||||
@ -72,7 +72,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
|
||||
"""Represent the value of a MySensors Sensor child node."""
|
||||
"""Representation of a MySensors Sensor child node."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['pyowm==2.3.1']
|
||||
REQUIREMENTS = ['pyowm==2.3.2']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {
|
||||
'weather': ['Condition', None],
|
||||
@ -127,9 +127,9 @@ class OpenWeatherMapSensor(Entity):
|
||||
else:
|
||||
self._state = round(data.get_temperature()['temp'], 1)
|
||||
elif self.type == 'wind_speed':
|
||||
self._state = data.get_wind()['speed']
|
||||
self._state = round(data.get_wind()['speed'], 1)
|
||||
elif self.type == 'humidity':
|
||||
self._state = data.get_humidity()
|
||||
self._state = round(data.get_humidity(), 1)
|
||||
elif self.type == 'pressure':
|
||||
self._state = round(data.get_pressure()['press'], 0)
|
||||
elif self.type == 'clouds':
|
||||
|
@ -41,12 +41,13 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
sub_sensors = {}
|
||||
data_types = entity_info[ATTR_DATA_TYPE]
|
||||
if len(data_types) == 0:
|
||||
data_type = "Unknown"
|
||||
for data_type in DATA_TYPES:
|
||||
if data_type in event.values:
|
||||
data_types = [data_type]
|
||||
break
|
||||
for _data_type in data_types:
|
||||
new_sensor = RfxtrxSensor(event, entity_info[ATTR_NAME],
|
||||
new_sensor = RfxtrxSensor(None, entity_info[ATTR_NAME],
|
||||
_data_type)
|
||||
sensors.append(new_sensor)
|
||||
sub_sensors[_data_type] = new_sensor
|
||||
@ -109,7 +110,7 @@ class RfxtrxSensor(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self.data_type:
|
||||
if self.event:
|
||||
return self.event.values[self.data_type]
|
||||
return None
|
||||
|
||||
@ -121,7 +122,8 @@ class RfxtrxSensor(Entity):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self.event.values
|
||||
if self.event:
|
||||
return self.event.values
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
|
@ -5,26 +5,28 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.swiss_hydrological_data/
|
||||
"""
|
||||
import logging
|
||||
import collections
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
from homeassistant.const import (TEMP_CELSIUS, CONF_PLATFORM, CONF_NAME)
|
||||
from homeassistant.const import (TEMP_CELSIUS, CONF_PLATFORM, CONF_NAME,
|
||||
STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['beautifulsoup4==4.4.1']
|
||||
REQUIREMENTS = ['xmltodict==0.10.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_RESOURCE = 'http://www.hydrodaten.admin.ch/en/'
|
||||
_RESOURCE = 'http://www.hydrodata.ch/xml/SMS.xml'
|
||||
|
||||
DEFAULT_NAME = 'Water temperature'
|
||||
CONF_STATION = 'station'
|
||||
ICON = 'mdi:cup-water'
|
||||
|
||||
ATTR_LOCATION = 'Location'
|
||||
ATTR_UPDATE = 'Update'
|
||||
ATTR_DISCHARGE = 'Discharge'
|
||||
ATTR_WATERLEVEL = 'Level'
|
||||
ATTR_DISCHARGE_MEAN = 'Discharge mean'
|
||||
@ -37,30 +39,25 @@ ATTR_TEMPERATURE_MAX = 'Temperature max'
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'swiss_hydrological_data',
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_STATION): cv.string,
|
||||
vol.Required(CONF_STATION): vol.Coerce(int),
|
||||
})
|
||||
|
||||
HydroData = collections.namedtuple(
|
||||
"HydrologicalData",
|
||||
['discharge', 'waterlevel', 'temperature', 'discharge_mean',
|
||||
'waterlevel_mean', 'temperature_mean', 'discharge_max', 'waterlevel_max',
|
||||
'temperature_max'])
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Swiss hydrological sensor."""
|
||||
import xmltodict
|
||||
|
||||
station = config.get(CONF_STATION)
|
||||
name = config.get(CONF_NAME, DEFAULT_NAME)
|
||||
|
||||
try:
|
||||
response = requests.get('{}/{}.html'.format(_RESOURCE, station),
|
||||
timeout=5)
|
||||
if not response.ok:
|
||||
_LOGGER.error('The given station does not seem to exist: %s',
|
||||
station)
|
||||
response = requests.get(_RESOURCE, timeout=5)
|
||||
if any(str(station) == location.get('@StrNr') for location in
|
||||
xmltodict.parse(response.text)['AKT_Data']['MesPar']) is False:
|
||||
_LOGGER.error('The given station does not exist: %s', station)
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('The URL is not accessible')
|
||||
@ -89,27 +86,47 @@ class SwissHydrologicalDataSensor(Entity):
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
if self._state is not STATE_UNKNOWN:
|
||||
return self._unit_of_measurement
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
try:
|
||||
return round(float(self._state), 1)
|
||||
except ValueError:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attributes = {}
|
||||
if self.data.measurings is not None:
|
||||
return {
|
||||
ATTR_DISCHARGE: self.data.measurings.discharge,
|
||||
ATTR_WATERLEVEL: self.data.measurings.waterlevel,
|
||||
ATTR_DISCHARGE_MEAN: self.data.measurings.discharge_mean,
|
||||
ATTR_WATERLEVEL_MEAN: self.data.measurings.waterlevel_mean,
|
||||
ATTR_TEMPERATURE_MEAN: self.data.measurings.temperature_mean,
|
||||
ATTR_DISCHARGE_MAX: self.data.measurings.discharge_max,
|
||||
ATTR_WATERLEVEL_MAX: self.data.measurings.waterlevel_max,
|
||||
ATTR_TEMPERATURE_MAX: self.data.measurings.temperature_max,
|
||||
}
|
||||
if '02' in self.data.measurings:
|
||||
attributes[ATTR_WATERLEVEL] = self.data.measurings['02'][
|
||||
'current']
|
||||
attributes[ATTR_WATERLEVEL_MEAN] = self.data.measurings['02'][
|
||||
'mean']
|
||||
attributes[ATTR_WATERLEVEL_MAX] = self.data.measurings['02'][
|
||||
'max']
|
||||
if '03' in self.data.measurings:
|
||||
attributes[ATTR_TEMPERATURE_MEAN] = self.data.measurings['03'][
|
||||
'mean']
|
||||
attributes[ATTR_TEMPERATURE_MAX] = self.data.measurings['03'][
|
||||
'max']
|
||||
if '10' in self.data.measurings:
|
||||
attributes[ATTR_DISCHARGE] = self.data.measurings['10'][
|
||||
'current']
|
||||
attributes[ATTR_DISCHARGE_MEAN] = self.data.measurings['10'][
|
||||
'current']
|
||||
attributes[ATTR_DISCHARGE_MAX] = self.data.measurings['10'][
|
||||
'max']
|
||||
|
||||
attributes[ATTR_LOCATION] = self.data.measurings['location']
|
||||
attributes[ATTR_UPDATE] = self.data.measurings['update_time']
|
||||
return attributes
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
@ -121,7 +138,10 @@ class SwissHydrologicalDataSensor(Entity):
|
||||
"""Get the latest data and update the states."""
|
||||
self.data.update()
|
||||
if self.data.measurings is not None:
|
||||
self._state = self.data.measurings.temperature
|
||||
if '03' not in self.data.measurings:
|
||||
self._state = STATE_UNKNOWN
|
||||
else:
|
||||
self._state = self.data.measurings['03']['current']
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
@ -135,29 +155,34 @@ class HydrologicalData(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from hydrodaten.admin.ch."""
|
||||
from bs4 import BeautifulSoup
|
||||
"""Get the latest data from hydrodata.ch."""
|
||||
import xmltodict
|
||||
|
||||
details = {}
|
||||
try:
|
||||
response = requests.get('{}/{}.html'.format(_RESOURCE,
|
||||
self.station),
|
||||
timeout=5)
|
||||
response = requests.get(_RESOURCE, timeout=5)
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error('Unable to retrieve data')
|
||||
response = None
|
||||
_LOGGER.error('Unable to retrieve data from %s', _RESOURCE)
|
||||
|
||||
try:
|
||||
tables = BeautifulSoup(response.content,
|
||||
'html.parser').findChildren('table')
|
||||
rows = tables[0].findChildren(['th', 'tr'])
|
||||
stations = xmltodict.parse(response.text)['AKT_Data']['MesPar']
|
||||
# Water level: Typ="02", temperature: Typ="03", discharge: Typ="10"
|
||||
for station in stations:
|
||||
if str(self.station) != station.get('@StrNr'):
|
||||
continue
|
||||
for data in ['02', '03', '10']:
|
||||
if data != station.get('@Typ'):
|
||||
continue
|
||||
values = station.get('Wert')
|
||||
if values is not None:
|
||||
details[data] = {
|
||||
'current': values[0],
|
||||
'max': list(values[4].items())[1][1],
|
||||
'mean': list(values[3].items())[1][1]}
|
||||
|
||||
details = []
|
||||
details['location'] = station.get('Name')
|
||||
details['update_time'] = station.get('Zeit')
|
||||
|
||||
for row in rows:
|
||||
cells = row.findChildren('td')
|
||||
for cell in cells:
|
||||
details.append(cell.string)
|
||||
|
||||
self.measurings = HydroData._make(details)
|
||||
self.measurings = details
|
||||
except AttributeError:
|
||||
self.measurings = None
|
||||
|
@ -64,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
continue
|
||||
sensor_name = str(ts_sensor.id)
|
||||
|
||||
for datatype in sensor_value_descriptions.keys():
|
||||
for datatype in sensor_value_descriptions:
|
||||
if datatype & datatype_mask and ts_sensor.has_value(datatype):
|
||||
|
||||
sensor_info = sensor_value_descriptions[datatype]
|
||||
|
@ -68,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
dev = []
|
||||
for device in devices:
|
||||
for type_name in SENSOR_TYPES.keys():
|
||||
for type_name in SENSOR_TYPES:
|
||||
dev.append(ThinkingCleanerSensor(device, type_name,
|
||||
update_devices))
|
||||
|
||||
|
@ -11,9 +11,9 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['uber_rides==0.2.1']
|
||||
REQUIREMENTS = ["uber_rides==0.2.4"]
|
||||
|
||||
ICON = 'mdi:taxi'
|
||||
ICON = "mdi:taxi"
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
@ -21,35 +21,35 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Uber sensor."""
|
||||
if None in (config.get('start_latitude'), config.get('start_longitude')):
|
||||
if None in (config.get("start_latitude"), config.get("start_longitude")):
|
||||
_LOGGER.error(
|
||||
"You must set start latitude and longitude to use the Uber sensor!"
|
||||
)
|
||||
return False
|
||||
|
||||
if config.get('server_token') is None:
|
||||
if config.get("server_token") is None:
|
||||
_LOGGER.error("You must set a server_token to use the Uber sensor!")
|
||||
return False
|
||||
|
||||
from uber_rides.session import Session
|
||||
|
||||
session = Session(server_token=config.get('server_token'))
|
||||
session = Session(server_token=config.get("server_token"))
|
||||
|
||||
wanted_product_ids = config.get('product_ids')
|
||||
wanted_product_ids = config.get("product_ids")
|
||||
|
||||
dev = []
|
||||
timeandpriceest = UberEstimate(session, config['start_latitude'],
|
||||
config['start_longitude'],
|
||||
config.get('end_latitude'),
|
||||
config.get('end_longitude'))
|
||||
timeandpriceest = UberEstimate(session, config["start_latitude"],
|
||||
config["start_longitude"],
|
||||
config.get("end_latitude"),
|
||||
config.get("end_longitude"))
|
||||
for product_id, product in timeandpriceest.products.items():
|
||||
if (wanted_product_ids is not None) and \
|
||||
(product_id not in wanted_product_ids):
|
||||
continue
|
||||
dev.append(UberSensor('time', timeandpriceest, product_id, product))
|
||||
is_metered = (product['price_details']['estimate'] == "Metered")
|
||||
if 'price_details' in product and is_metered is False:
|
||||
dev.append(UberSensor('price', timeandpriceest,
|
||||
dev.append(UberSensor("time", timeandpriceest, product_id, product))
|
||||
if (product.get("price_details") is not None) and \
|
||||
product["price_details"]["estimate"] is not "Metered":
|
||||
dev.append(UberSensor("price", timeandpriceest,
|
||||
product_id, product))
|
||||
add_devices(dev)
|
||||
|
||||
@ -64,30 +64,30 @@ class UberSensor(Entity):
|
||||
self._product_id = product_id
|
||||
self._product = product
|
||||
self._sensortype = sensorType
|
||||
self._name = "{} {}".format(self._product['display_name'],
|
||||
self._name = "{} {}".format(self._product["display_name"],
|
||||
self._sensortype)
|
||||
if self._sensortype == "time":
|
||||
self._unit_of_measurement = "min"
|
||||
time_estimate = self._product.get('time_estimate_seconds', 0)
|
||||
time_estimate = self._product.get("time_estimate_seconds", 0)
|
||||
self._state = int(time_estimate / 60)
|
||||
elif self._sensortype == "price":
|
||||
if 'price_details' in self._product:
|
||||
price_details = self._product['price_details']
|
||||
self._unit_of_measurement = price_details.get('currency_code',
|
||||
'N/A')
|
||||
if 'low_estimate' in price_details:
|
||||
statekey = 'minimum'
|
||||
if self._product.get("price_details") is not None:
|
||||
price_details = self._product["price_details"]
|
||||
self._unit_of_measurement = price_details.get("currency_code")
|
||||
if price_details.get("low_estimate") is not None:
|
||||
statekey = "minimum"
|
||||
else:
|
||||
statekey = 'low_estimate'
|
||||
statekey = "low_estimate"
|
||||
self._state = int(price_details.get(statekey, 0))
|
||||
else:
|
||||
self._unit_of_measurement = 'N/A'
|
||||
self._state = 0
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
if "uber" not in self._name.lower():
|
||||
self._name = "Uber{}".format(self._name)
|
||||
return self._name
|
||||
|
||||
@property
|
||||
@ -103,43 +103,41 @@ class UberSensor(Entity):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
time_estimate = self._product.get('time_estimate_seconds', 'N/A')
|
||||
time_estimate = self._product.get("time_estimate_seconds")
|
||||
params = {
|
||||
'Product ID': self._product['product_id'],
|
||||
'Product short description': self._product['short_description'],
|
||||
'Product display name': self._product['display_name'],
|
||||
'Product description': self._product['description'],
|
||||
'Pickup time estimate (in seconds)': time_estimate,
|
||||
'Trip duration (in seconds)': self._product.get('duration', 'N/A'),
|
||||
'Vehicle Capacity': self._product['capacity']
|
||||
"Product ID": self._product["product_id"],
|
||||
"Product short description": self._product["short_description"],
|
||||
"Product display name": self._product["display_name"],
|
||||
"Product description": self._product["description"],
|
||||
"Pickup time estimate (in seconds)": time_estimate,
|
||||
"Trip duration (in seconds)": self._product.get("duration"),
|
||||
"Vehicle Capacity": self._product["capacity"]
|
||||
}
|
||||
|
||||
if 'price_details' in self._product:
|
||||
price_details = self._product['price_details']
|
||||
distance_key = 'Trip distance (in {}s)'.format(price_details[
|
||||
'distance_unit'])
|
||||
distance_val = self._product.get('distance')
|
||||
params['Minimum price'] = price_details['minimum'],
|
||||
params['Cost per minute'] = price_details['cost_per_minute'],
|
||||
params['Distance units'] = price_details['distance_unit'],
|
||||
params['Cancellation fee'] = price_details['cancellation_fee'],
|
||||
params['Cost per distance'] = price_details['cost_per_distance'],
|
||||
params['Base price'] = price_details['base'],
|
||||
params['Price estimate'] = price_details.get('estimate', 'N/A'),
|
||||
params['Price currency code'] = price_details.get('currency_code'),
|
||||
params['High price estimate'] = price_details.get('high_estimate',
|
||||
'N/A'),
|
||||
params['Low price estimate'] = price_details.get('low_estimate',
|
||||
'N/A'),
|
||||
params['Surge multiplier'] = price_details.get('surge_multiplier',
|
||||
'N/A')
|
||||
if self._product.get("price_details") is not None:
|
||||
price_details = self._product["price_details"]
|
||||
dunit = price_details.get("distance_unit")
|
||||
distance_key = "Trip distance (in {}s)".format(dunit)
|
||||
distance_val = self._product.get("distance")
|
||||
params["Cost per minute"] = price_details.get("cost_per_minute")
|
||||
params["Distance units"] = price_details.get("distance_unit")
|
||||
params["Cancellation fee"] = price_details.get("cancellation_fee")
|
||||
cpd = price_details.get("cost_per_distance")
|
||||
params["Cost per distance"] = cpd
|
||||
params["Base price"] = price_details.get("base")
|
||||
params["Minimum price"] = price_details.get("minimum")
|
||||
params["Price estimate"] = price_details.get("estimate")
|
||||
params["Price currency code"] = price_details.get("currency_code")
|
||||
params["High price estimate"] = price_details.get("high_estimate")
|
||||
params["Low price estimate"] = price_details.get("low_estimate")
|
||||
params["Surge multiplier"] = price_details.get("surge_multiplier")
|
||||
else:
|
||||
distance_key = 'Trip distance (in miles)'
|
||||
distance_val = self._product.get('distance', 'N/A')
|
||||
distance_key = "Trip distance (in miles)"
|
||||
distance_val = self._product.get("distance")
|
||||
|
||||
params[distance_key] = distance_val
|
||||
|
||||
return params
|
||||
return {k: v for k, v in params.items() if v is not None}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
@ -152,13 +150,13 @@ class UberSensor(Entity):
|
||||
self.data.update()
|
||||
self._product = self.data.products[self._product_id]
|
||||
if self._sensortype == "time":
|
||||
time_estimate = self._product.get('time_estimate_seconds', 0)
|
||||
time_estimate = self._product.get("time_estimate_seconds", 0)
|
||||
self._state = int(time_estimate / 60)
|
||||
elif self._sensortype == "price":
|
||||
price_details = self._product.get('price_details')
|
||||
price_details = self._product.get("price_details")
|
||||
if price_details is not None:
|
||||
min_price = price_details.get('minimum')
|
||||
self._state = int(price_details.get('low_estimate', min_price))
|
||||
min_price = price_details.get("minimum")
|
||||
self._state = int(price_details.get("low_estimate", min_price))
|
||||
else:
|
||||
self._state = 0
|
||||
|
||||
@ -190,40 +188,39 @@ class UberEstimate(object):
|
||||
products_response = client.get_products(
|
||||
self.start_latitude, self.start_longitude)
|
||||
|
||||
products = products_response.json.get('products')
|
||||
products = products_response.json.get("products")
|
||||
|
||||
for product in products:
|
||||
self.products[product['product_id']] = product
|
||||
self.products[product["product_id"]] = product
|
||||
|
||||
if self.end_latitude is not None and self.end_longitude is not None:
|
||||
price_response = client.get_price_estimates(
|
||||
self.start_latitude,
|
||||
self.start_longitude,
|
||||
self.end_latitude,
|
||||
self.end_longitude)
|
||||
self.start_latitude, self.start_longitude,
|
||||
self.end_latitude, self.end_longitude)
|
||||
|
||||
prices = price_response.json.get('prices', [])
|
||||
prices = price_response.json.get("prices", [])
|
||||
|
||||
for price in prices:
|
||||
product = self.products[price['product_id']]
|
||||
product = self.products[price["product_id"]]
|
||||
product["duration"] = price.get("duration", "0")
|
||||
product["distance"] = price.get("distance", "0")
|
||||
price_details = product.get("price_details")
|
||||
product["duration"] = price.get('duration', '0')
|
||||
product["distance"] = price.get('distance', '0')
|
||||
if price_details is not None:
|
||||
price_details["estimate"] = price.get('estimate',
|
||||
'0')
|
||||
price_details["high_estimate"] = price.get('high_estimate',
|
||||
'0')
|
||||
price_details["low_estimate"] = price.get('low_estimate',
|
||||
'0')
|
||||
surge_multiplier = price.get('surge_multiplier', '0')
|
||||
price_details["surge_multiplier"] = surge_multiplier
|
||||
if product.get("price_details") is None:
|
||||
price_details = {}
|
||||
price_details["estimate"] = price.get("estimate", "0")
|
||||
price_details["high_estimate"] = price.get("high_estimate",
|
||||
"0")
|
||||
price_details["low_estimate"] = price.get("low_estimate", "0")
|
||||
price_details["currency_code"] = price.get("currency_code")
|
||||
surge_multiplier = price.get("surge_multiplier", "0")
|
||||
price_details["surge_multiplier"] = surge_multiplier
|
||||
product["price_details"] = price_details
|
||||
|
||||
estimate_response = client.get_pickup_time_estimates(
|
||||
self.start_latitude, self.start_longitude)
|
||||
|
||||
estimates = estimate_response.json.get('times')
|
||||
estimates = estimate_response.json.get("times")
|
||||
|
||||
for estimate in estimates:
|
||||
self.products[estimate['product_id']][
|
||||
"time_estimate_seconds"] = estimate.get('estimate', '0')
|
||||
self.products[estimate["product_id"]][
|
||||
"time_estimate_seconds"] = estimate.get("estimate", "0")
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
SENSOR_TYPES = ['temperature', 'humidity']
|
||||
|
||||
|
@ -18,8 +18,7 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REQUIREMENTS = ['xmltodict']
|
||||
REQUIREMENTS = ['xmltodict==0.10.2']
|
||||
|
||||
# Sensor types are defined like so:
|
||||
SENSOR_TYPES = {
|
||||
|
189
homeassistant/components/sensor/yweather.py
Normal file
189
homeassistant/components/sensor/yweather.py
Normal file
@ -0,0 +1,189 @@
|
||||
"""
|
||||
Support for the Yahoo! Weather service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.yweather/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_PLATFORM, TEMP_CELSIUS,
|
||||
CONF_MONITORED_CONDITIONS, STATE_UNKNOWN)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ["yahooweather==0.4"]
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'weather_current': ['Current', None],
|
||||
'weather': ['Condition', None],
|
||||
'temperature': ['Temperature', "temperature"],
|
||||
'temp_min': ['Temperature', "temperature"],
|
||||
'temp_max': ['Temperature', "temperature"],
|
||||
'wind_speed': ['Wind speed', "speed"],
|
||||
'humidity': ['Humidity', "%"],
|
||||
'pressure': ['Pressure', "pressure"],
|
||||
'visibility': ['Visibility', "distance"],
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "yweather",
|
||||
vol.Optional("woeid"): vol.Coerce(str),
|
||||
vol.Optional("forecast"): vol.Coerce(int),
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
[vol.In(SENSOR_TYPES.keys())],
|
||||
})
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Yahoo! weather sensor."""
|
||||
from yahooweather import get_woeid, UNIT_C, UNIT_F
|
||||
|
||||
unit = hass.config.temperature_unit
|
||||
woeid = config.get("woeid", None)
|
||||
forecast = config.get("forecast", 0)
|
||||
|
||||
# convert unit
|
||||
yunit = UNIT_C if unit == TEMP_CELSIUS else UNIT_F
|
||||
|
||||
# for print HA style temp
|
||||
SENSOR_TYPES["temperature"][1] = unit
|
||||
SENSOR_TYPES["temp_min"][1] = unit
|
||||
SENSOR_TYPES["temp_max"][1] = unit
|
||||
|
||||
# if not exists a customer woeid / calc from HA
|
||||
if woeid is None:
|
||||
woeid = get_woeid(hass.config.latitude, hass.config.longitude)
|
||||
# receive a error?
|
||||
if woeid is None:
|
||||
_LOGGER.critical("Can't retrieve WOEID from yahoo!")
|
||||
return False
|
||||
|
||||
# create api object
|
||||
yahoo_api = YahooWeatherData(woeid, yunit)
|
||||
|
||||
# if update is false, it will never work...
|
||||
if not yahoo_api.update():
|
||||
_LOGGER.critical("Can't retrieve weather data from yahoo!")
|
||||
return False
|
||||
|
||||
# check if forecast support by API
|
||||
if forecast >= len(yahoo_api.yahoo.Forecast):
|
||||
_LOGGER.error("Yahoo! only support %d days forcast!",
|
||||
len(yahoo_api.yahoo.Forecast))
|
||||
return False
|
||||
|
||||
dev = []
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||
dev.append(YahooWeatherSensor(yahoo_api, forecast, variable))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class YahooWeatherSensor(Entity):
|
||||
"""Implementation of an Yahoo! weather sensor."""
|
||||
|
||||
def __init__(self, weather_data, forecast, sensor_type):
|
||||
"""Initialize the sensor."""
|
||||
self._client = 'Weather'
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self._type = sensor_type
|
||||
self._state = STATE_UNKNOWN
|
||||
self._unit = SENSOR_TYPES[sensor_type][1]
|
||||
self._data = weather_data
|
||||
self._forecast = forecast
|
||||
self._code = None
|
||||
|
||||
# update data
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return '{} {}'.format(self._client, self._name)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._data.yahoo.Units.get(self._unit, self._unit)
|
||||
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Return the entity picture to use in the frontend, if any."""
|
||||
if self._code is None or "weather" not in self._type:
|
||||
return None
|
||||
|
||||
return self._data.yahoo.getWeatherImage(self._code)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'about': "Weather forecast delivered by Yahoo! Inc. are provided"
|
||||
" free of charge for use by individuals and non-profit"
|
||||
" organizations for personal, non-commercial uses."
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from Yahoo! and updates the states."""
|
||||
self._data.update()
|
||||
|
||||
# default code for weather image
|
||||
self._code = self._data.yahoo.Now["code"]
|
||||
|
||||
# read data
|
||||
if self._type == "weather_current":
|
||||
self._state = self._data.yahoo.Now["text"]
|
||||
elif self._type == "weather":
|
||||
self._code = self._data.yahoo.Forecast[self._forecast]["code"]
|
||||
self._state = self._data.yahoo.Forecast[self._forecast]["text"]
|
||||
elif self._type == "temperature":
|
||||
self._state = self._data.yahoo.Now["temp"]
|
||||
elif self._type == "temp_min":
|
||||
self._code = self._data.yahoo.Forecast[self._forecast]["code"]
|
||||
self._state = self._data.yahoo.Forecast[self._forecast]["low"]
|
||||
elif self._type == "temp_max":
|
||||
self._code = self._data.yahoo.Forecast[self._forecast]["code"]
|
||||
self._state = self._data.yahoo.Forecast[self._forecast]["high"]
|
||||
elif self._type == "wind_speed":
|
||||
self._state = self._data.yahoo.Wind["speed"]
|
||||
elif self._type == "humidity":
|
||||
self._state = self._data.yahoo.Atmosphere["humidity"]
|
||||
elif self._type == "pressure":
|
||||
self._state = self._data.yahoo.Atmosphere["pressure"]
|
||||
elif self._type == "visibility":
|
||||
self._state = self._data.yahoo.Atmosphere["visibility"]
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class YahooWeatherData(object):
|
||||
"""Handle yahoo api object and limit updates."""
|
||||
|
||||
def __init__(self, woeid, temp_unit):
|
||||
"""Initialize the data object."""
|
||||
from yahooweather import YahooWeather
|
||||
|
||||
# init yahoo api object
|
||||
self._yahoo = YahooWeather(woeid, temp_unit)
|
||||
|
||||
@property
|
||||
def yahoo(self):
|
||||
"""Return yahoo api object."""
|
||||
return self._yahoo
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from yahoo. True is success."""
|
||||
return self._yahoo.updateWeather()
|
@ -66,7 +66,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
value.type == zwave.TYPE_DECIMAL):
|
||||
add_devices([ZWaveMultilevelSensor(value)])
|
||||
|
||||
elif value.command_class == zwave.COMMAND_CLASS_ALARM:
|
||||
elif (value.command_class == zwave.COMMAND_CLASS_ALARM or
|
||||
value.command_class == zwave.COMMAND_CLASS_SENSOR_ALARM):
|
||||
add_devices([ZWaveAlarmSensor(value)])
|
||||
|
||||
|
||||
|
33
homeassistant/components/services.yaml
Normal file
33
homeassistant/components/services.yaml
Normal file
@ -0,0 +1,33 @@
|
||||
persistent_notification:
|
||||
create:
|
||||
description: Show a notification in the frontend
|
||||
|
||||
fields:
|
||||
message:
|
||||
description: Message body of the notification. [Templates accepted]
|
||||
example: Please check your configuration.yaml.
|
||||
|
||||
title:
|
||||
description: Optional title for your notification. [Optional, Templates accepted]
|
||||
example: Test notification
|
||||
|
||||
notification_id:
|
||||
description: Target ID of the notification, will replace a notification with the same Id. [Optional]
|
||||
example: 1234
|
||||
|
||||
homematic:
|
||||
virtualkey:
|
||||
description: Press a virtual key from CCU/Homegear or simulate keypress
|
||||
|
||||
fields:
|
||||
address:
|
||||
description: Address of homematic device or BidCoS-RF for virtual remote
|
||||
example: BidCoS-RF
|
||||
|
||||
channel:
|
||||
description: Channel for calling a keypress
|
||||
example: 1
|
||||
|
||||
param:
|
||||
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
|
||||
example: PRESS_LONG
|
@ -20,17 +20,17 @@ DEFAULT_PORT = 8125
|
||||
DEFAULT_PREFIX = 'hass'
|
||||
DEFAULT_RATE = 1
|
||||
|
||||
REQUIREMENTS = ['python-statsd==1.7.2']
|
||||
REQUIREMENTS = ['statsd==3.2.1']
|
||||
|
||||
CONF_HOST = 'host'
|
||||
CONF_PORT = 'port'
|
||||
CONF_PREFIX = 'prefix'
|
||||
CONF_RATE = 'rate'
|
||||
CONF_ATTR = 'log_attributes'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the StatsD component."""
|
||||
from statsd.compat import NUM_TYPES
|
||||
import statsd
|
||||
|
||||
conf = config[DOMAIN]
|
||||
@ -39,16 +39,14 @@ def setup(hass, config):
|
||||
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT)
|
||||
sample_rate = util.convert(conf.get(CONF_RATE), int, DEFAULT_RATE)
|
||||
prefix = util.convert(conf.get(CONF_PREFIX), str, DEFAULT_PREFIX)
|
||||
show_attribute_flag = conf.get(CONF_ATTR, False)
|
||||
|
||||
statsd_connection = statsd.Connection(
|
||||
statsd_client = statsd.StatsClient(
|
||||
host=host,
|
||||
port=port,
|
||||
sample_rate=sample_rate,
|
||||
disabled=False
|
||||
prefix=prefix
|
||||
)
|
||||
|
||||
meter = statsd.Gauge(prefix, statsd_connection)
|
||||
|
||||
def statsd_event_listener(event):
|
||||
"""Listen for new messages on the bus and sends them to StatsD."""
|
||||
state = event.data.get('new_state')
|
||||
@ -61,11 +59,28 @@ def setup(hass, config):
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if not isinstance(_state, NUM_TYPES):
|
||||
return
|
||||
states = dict(state.attributes)
|
||||
|
||||
_LOGGER.debug('Sending %s.%s', state.entity_id, _state)
|
||||
meter.send(state.entity_id, _state)
|
||||
|
||||
if show_attribute_flag is True:
|
||||
statsd_client.gauge(
|
||||
"%s.state" % state.entity_id,
|
||||
_state,
|
||||
sample_rate
|
||||
)
|
||||
|
||||
# Send attribute values
|
||||
for key, value in states.items():
|
||||
if isinstance(value, (float, int)):
|
||||
stat = "%s.%s" % (state.entity_id, key.replace(' ', '_'))
|
||||
statsd_client.gauge(stat, value, sample_rate)
|
||||
|
||||
else:
|
||||
statsd_client.gauge(state.entity_id, _state, sample_rate)
|
||||
|
||||
# Increment the count
|
||||
statsd_client.incr(state.entity_id, rate=sample_rate)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, statsd_event_listener)
|
||||
|
||||
|
@ -133,7 +133,7 @@ class AcerSwitch(SwitchDevice):
|
||||
else:
|
||||
self._available = False
|
||||
|
||||
for key in self._attributes.keys():
|
||||
for key in self._attributes:
|
||||
msg = CMD_DICT.get(key, None)
|
||||
if msg:
|
||||
awns = self._write_read_format(msg)
|
||||
|
@ -18,10 +18,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
|
||||
class DemoSwitch(SwitchDevice):
|
||||
"""represenation of a demo switch."""
|
||||
"""Representation of a demo switch."""
|
||||
|
||||
def __init__(self, name, state, icon, assumed):
|
||||
"""Initialize the Deom switch."""
|
||||
"""Initialize the Demo switch."""
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._state = state
|
||||
self._icon = icon
|
||||
|
@ -4,7 +4,7 @@ Flux for Home-Assistant.
|
||||
The idea was taken from https://github.com/KpaBap/hue-flux/
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch/flux/
|
||||
https://home-assistant.io/components/switch.flux/
|
||||
"""
|
||||
from datetime import time
|
||||
import logging
|
||||
@ -62,7 +62,7 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness):
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the demo switches."""
|
||||
"""Setup the Flux switches."""
|
||||
name = config.get(CONF_NAME)
|
||||
lights = config.get(CONF_LIGHTS)
|
||||
start_time = config.get(CONF_START_TIME)
|
||||
@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class FluxSwitch(SwitchDevice):
|
||||
"""Flux switch."""
|
||||
"""Representation of a Flux switch."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, hass, state, lights, start_time, stop_time,
|
||||
|
42
homeassistant/components/switch/knx.py
Normal file
42
homeassistant/components/switch/knx.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""
|
||||
Support KNX switching actuators.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.knx/
|
||||
"""
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.components.knx import (
|
||||
KNXConfig, KNXGroupAddress)
|
||||
|
||||
DEPENDENCIES = ["knx"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Setup the KNX switch platform."""
|
||||
add_entities([
|
||||
KNXSwitch(hass, KNXConfig(config))
|
||||
])
|
||||
|
||||
|
||||
class KNXSwitch(KNXGroupAddress, SwitchDevice):
|
||||
"""Representation of a KNX switch device."""
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on.
|
||||
|
||||
This sends a value 0 to the group address of the device
|
||||
"""
|
||||
self.group_write(1)
|
||||
self._state = [1]
|
||||
if not self.should_poll:
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the switch off.
|
||||
|
||||
This sends a value 1 to the group address of the device
|
||||
"""
|
||||
self.group_write(0)
|
||||
self._state = [0]
|
||||
if not self.should_poll:
|
||||
self.update_ha_state()
|
@ -6,36 +6,38 @@ https://home-assistant.io/components/switch.mystrom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_PLATFORM, CONF_NAME, CONF_HOST)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
|
||||
REQUIREMENTS = ['python-mystrom==0.3.6']
|
||||
|
||||
DEFAULT_NAME = 'myStrom Switch'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'mystrom',
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return myStrom switch."""
|
||||
host = config.get('host')
|
||||
from pymystrom import MyStromPlug, exceptions
|
||||
|
||||
if host is None:
|
||||
_LOGGER.error('Missing required variable: host')
|
||||
return False
|
||||
|
||||
resource = 'http://{}'.format(host)
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
try:
|
||||
requests.get(resource, timeout=10)
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device %s. "
|
||||
"Please check the IP address in the configuration file",
|
||||
host)
|
||||
MyStromPlug(host).get_status()
|
||||
except exceptions.MyStromConnectionError:
|
||||
_LOGGER.error("No route to device '%s'", host)
|
||||
return False
|
||||
|
||||
add_devices([MyStromSwitch(
|
||||
config.get('name', DEFAULT_NAME),
|
||||
resource)])
|
||||
add_devices([MyStromSwitch(config.get('name', DEFAULT_NAME), host)])
|
||||
|
||||
|
||||
class MyStromSwitch(SwitchDevice):
|
||||
@ -43,10 +45,13 @@ class MyStromSwitch(SwitchDevice):
|
||||
|
||||
def __init__(self, name, resource):
|
||||
"""Initialize the myStrom switch."""
|
||||
self._state = False
|
||||
from pymystrom import MyStromPlug
|
||||
|
||||
self._name = name
|
||||
self._resource = resource
|
||||
self.consumption = 0
|
||||
self.data = {}
|
||||
self.plug = MyStromPlug(self._resource)
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -56,45 +61,37 @@ class MyStromSwitch(SwitchDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._state
|
||||
return bool(self.data['relay'])
|
||||
|
||||
@property
|
||||
def current_power_mwh(self):
|
||||
"""Return the urrent power consumption in mWh."""
|
||||
return self.consumption
|
||||
"""Return the current power consumption in mWh."""
|
||||
return round(self.data['power'], 2)
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on."""
|
||||
from pymystrom import exceptions
|
||||
try:
|
||||
request = requests.get('{}/relay'.format(self._resource),
|
||||
params={'state': '1'},
|
||||
timeout=10)
|
||||
if request.status_code == 200:
|
||||
self._state = True
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("Can't turn on %s. Is device offline?",
|
||||
self.plug.set_relay_on()
|
||||
except exceptions.MyStromConnectionError:
|
||||
_LOGGER.error("No route to device '%s'. Is device offline?",
|
||||
self._resource)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the switch off."""
|
||||
from pymystrom import exceptions
|
||||
try:
|
||||
request = requests.get('{}/relay'.format(self._resource),
|
||||
params={'state': '0'},
|
||||
timeout=10)
|
||||
if request.status_code == 200:
|
||||
self._state = False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("Can't turn on %s. Is device offline?",
|
||||
self.plug.set_relay_off()
|
||||
except exceptions.MyStromConnectionError:
|
||||
_LOGGER.error("No route to device '%s'. Is device offline?",
|
||||
self._resource)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from REST API and update the state."""
|
||||
"""Get the latest data from the device and update the data."""
|
||||
from pymystrom import exceptions
|
||||
try:
|
||||
request = requests.get('{}/report'.format(self._resource),
|
||||
timeout=10)
|
||||
data = request.json()
|
||||
self._state = bool(data['relay'])
|
||||
self.consumption = data['power']
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.data = self.plug.get_status()
|
||||
except exceptions.MyStromConnectionError:
|
||||
self.data = {'power': 0, 'relay': False}
|
||||
_LOGGER.error("No route to device '%s'. Is device offline?",
|
||||
self._resource)
|
||||
|
@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
dev = []
|
||||
for device in devices:
|
||||
for type_name in SWITCH_TYPES.keys():
|
||||
for type_name in SWITCH_TYPES:
|
||||
dev.append(ThinkingCleanerSwitch(device, type_name,
|
||||
update_devices))
|
||||
|
||||
|
52
homeassistant/components/switch/tplink.py
Normal file
52
homeassistant/components/switch/tplink.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""
|
||||
Support for TPLink HS100/HS110 smart switch.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.tplink/
|
||||
"""
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME)
|
||||
|
||||
# constants
|
||||
DEVICE_DEFAULT_NAME = 'HS100'
|
||||
REQUIREMENTS = ['https://github.com/gadgetreactor/pyHS100/archive/'
|
||||
'master.zip#pyHS100==0.1.2']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the TPLink switch platform."""
|
||||
from pyHS100.pyHS100 import SmartPlug
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME)
|
||||
|
||||
add_devices_callback([SmartPlugSwitch(SmartPlug(host),
|
||||
name)])
|
||||
|
||||
|
||||
class SmartPlugSwitch(SwitchDevice):
|
||||
"""Representation of a TPLink Smart Plug switch."""
|
||||
|
||||
def __init__(self, smartplug, name):
|
||||
"""Initialize the switch."""
|
||||
self.smartplug = smartplug
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Smart Plug, if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self.smartplug.state == 'ON'
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on."""
|
||||
self.smartplug.state = 'ON'
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the switch off."""
|
||||
self.smartplug.state = 'OFF'
|
@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Verisure platform."""
|
||||
"""Setup the Verisure switch platform."""
|
||||
if not int(hub.config.get('smartplugs', '1')):
|
||||
return False
|
||||
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -12,7 +12,7 @@ DEPENDENCIES = ["zigbee"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
"""Setup the ZigBee switch platform."""
|
||||
add_entities([
|
||||
ZigBeeSwitch(hass, ZigBeeDigitalOutConfig(config))
|
||||
])
|
||||
|
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