Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a764683f3a | |||
| 19cb1a954f | |||
| 7a1e2de49f | |||
| 89639822f1 | |||
| 8c44ecc4ba | |||
| dc0f16c9dd | |||
| 16c71ab207 | |||
| 06d70544bc | |||
| 1877906fdf | |||
| 95d033f1af | |||
| 7cff107c17 | |||
| 89972ed940 | |||
| 6694f29918 | |||
| c1798dbe1f | |||
| 3246b58437 | |||
| 63356fb5eb | |||
| ef64e11b50 | |||
| e38b7d97d2 | |||
| 8984a6b161 | |||
| 49b595e32e | |||
| a60a342864 | |||
| 88b3aa54a8 | |||
| a0c1c918b8 | |||
| 675283c23e | |||
| c023d1d656 | |||
| ce4891fe8e | |||
| 82d98f5b89 | |||
| 2900855061 | |||
| e31d4863c7 | |||
| af736a3e71 | |||
| 16feb1c55e | |||
| 497bc6ac0d | |||
| cae8f8a006 | |||
| 82e992c63c | |||
| 3dcafafc6a | |||
| ebcda4076e | |||
| 011f82f9e3 | |||
| 8ed2c8e6a4 | |||
| b9cadbecaa | |||
| e1db639317 | |||
| beeae17cab | |||
| 8fcfb9136c | |||
| 62c11dde17 | |||
| e58615b2a5 | |||
| bef2f87ddc | |||
| 45a8b74d7f | |||
| 09a4336bc5 | |||
| 6d60287455 | |||
| 6cb91e66c8 | |||
| 2189516966 | |||
| 1738db9ccc | |||
| e0dd5a8558 | |||
| f4f2da5dc7 | |||
| 085d026ab6 | |||
| 3b14189021 | |||
| 6b9e1f3263 | |||
| bde2f0d5a0 | |||
| 50ea3c7744 | |||
| bde9e4e9c0 | |||
| 609458052c | |||
| 344fb9c8b4 | |||
| 03ef74b4ab | |||
| ab63fbff3f | |||
| 2ab2f68318 | |||
| 5d6c13c12c | |||
| ff5c3c9f98 | |||
| 31b8e49ad2 | |||
| 978ebb9c59 | |||
| 85e3dfe6a6 | |||
| cf5aeebba6 | |||
| 3e3d9c881e | |||
| 216a756590 | |||
| db23320659 | |||
| c634cbf866 | |||
| ceb332bc31 | |||
| 86e3fdee1c | |||
| 0f4acb59fe | |||
| c5b2df01d9 | |||
| 83a72ab4dc | |||
| 2cdef7fb2f | |||
| 659d67f362 | |||
| ffccca1f60 | |||
| ef74bd9892 | |||
| 3447fdc76f | |||
| a2e45b8fdd | |||
| a65f196d19 | |||
| a74cdc7b0d | |||
| 449be29022 | |||
| ba8e417390 | |||
| cad995a5f4 | |||
| 06efee7ecf | |||
| bacc14d845 | |||
| 6f8a733434 | |||
| 906e64fdb5 | |||
| 8e406a70f6 | |||
| 8d9f4a1754 | |||
| 0a53b863cd | |||
| 80feb322f9 | |||
| 2b514139eb | |||
| 2b8dfb2a0e | |||
| 6477122b23 | |||
| 1e9db41028 | |||
| 21d3be4027 | |||
| 48b3c98646 | |||
| 15803d1773 | |||
| 3870d2e0cd | |||
| fe0164b137 | |||
| 6bc504bfcc | |||
| c44eefacb4 | |||
| 952b1a3e0c | |||
| a57cd58675 | |||
| d67f79e2eb | |||
| d326d187d1 | |||
| d0b1619946 | |||
| 21be4c1828 | |||
| d1f4901d53 | |||
| 7582eb9f63 | |||
| 419ff18afb | |||
| 8dd7ebb08e | |||
| 5cce02ab62 | |||
| 6a816116ab | |||
| bb0f484caf | |||
| 3c5c018e3e | |||
| 78e7e17484 | |||
| 31d2a5d2d1 | |||
| baa9bdf6fc | |||
| 00179763ef | |||
| 7a73dc7d6a | |||
| d0b9b588a9 | |||
| 592c599488 | |||
| 6714392e9c | |||
| dc75b28b90 | |||
| d2509ce9e3 | |||
| 3afc566be1 | |||
| fb3e388f04 | |||
| 254b1c46ac | |||
| d13cc227cc | |||
| 446f998759 | |||
| 206e7d7a67 | |||
| c3b25f2cd5 | |||
| f3199e7dae | |||
| 4ecd724578 | |||
| e4d3b25f1e | |||
| 7e7f7b64e5 | |||
| e0e9d3c57b | |||
| a687bdb388 | |||
| 199fbc7a15 | |||
| 57754cd2ff | |||
| 21381a95d4 | |||
| be72b04855 | |||
| 86ccf26a1a | |||
| 87c138c559 | |||
| b3acd7d21d | |||
| a19f7bff28 | |||
| 30b7c6b694 | |||
| 43faeff42a | |||
| 5ca26fc13f | |||
| 04748e3ad1 | |||
| 7b02dc434a | |||
| 1c1d18053b | |||
| 2ac752d67a | |||
| a1ef1c996c | |||
| cbb897b2cf | |||
| e4b67c9574 | |||
| 7a8c5a0709 | |||
| aadd730ddd | |||
| 68df3deee0 | |||
| c616115419 | |||
| dfe1b8d934 | |||
| ec8dc25c9c | |||
| 67a04c2a0e | |||
| 600a3e3965 | |||
| 3349bdc2bd | |||
| 12e26d25a5 | |||
| aa3d0e1047 | |||
| d0ee8abcb8 | |||
| 94b47d8bc3 | |||
| 7b942243ab | |||
| a70f922a71 | |||
| 9ce9b8debb | |||
| d7b006600e | |||
| a564fe8286 | |||
| 7fc9fa4b0c | |||
| d87e969671 | |||
| 278514b994 | |||
| 38b0336694 | |||
| caa096ebd5 | |||
| ba417a730b | |||
| 6fa095f4a7 | |||
| 5efa076080 | |||
| cbc0833360 | |||
| 2e62053629 | |||
| 4f09279524 | |||
| 57dfce1583 | |||
| 33bafb8451 | |||
| f59e242c63 | |||
| cb6f50b7ff | |||
| 44177a7fde |
+23
@@ -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
|
||||
@@ -20,6 +21,9 @@ omit =
|
||||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
@@ -81,8 +85,16 @@ omit =
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
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
|
||||
@@ -111,7 +123,10 @@ omit =
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/garage_door/wink.py
|
||||
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
|
||||
@@ -120,7 +135,9 @@ omit =
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
@@ -146,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
|
||||
@@ -170,16 +188,19 @@ omit =
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/forecast.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
@@ -197,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
|
||||
@@ -208,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-75
@@ -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.
|
||||
|
||||
+6
-6
@@ -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,6 +25,9 @@ http:
|
||||
# Set to 1 to enable development mode
|
||||
# development: 1
|
||||
|
||||
# Enable the frontend
|
||||
frontend:
|
||||
|
||||
light:
|
||||
# platform: hue
|
||||
|
||||
@@ -30,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:
|
||||
|
||||
@@ -71,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:
|
||||
|
||||
@@ -102,6 +104,7 @@ browser:
|
||||
|
||||
keyboard:
|
||||
|
||||
# https://home-assistant.io/getting-started/automation/
|
||||
automation:
|
||||
- alias: 'Rule 1 Light on in the evening'
|
||||
trigger:
|
||||
@@ -123,7 +126,6 @@ automation:
|
||||
entity_id: group.living_room
|
||||
|
||||
- alias: 'Rule 2 - Away Mode'
|
||||
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: group.all_devices
|
||||
@@ -136,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
|
||||
@@ -146,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
|
||||
@@ -163,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)
|
||||
|
||||
+16
-117
@@ -3,7 +3,6 @@
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from threading import RLock
|
||||
@@ -11,22 +10,16 @@ from threading import RLock
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components as core_components
|
||||
import homeassistant.components.group as group
|
||||
import homeassistant.config as config_util
|
||||
from homeassistant.components import group, persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.util.location as loc_util
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.const import (
|
||||
CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME,
|
||||
CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__)
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SETUP_LOCK = RLock()
|
||||
@@ -208,11 +201,6 @@ def prepare_setup_platform(hass, config, domain, platform_name):
|
||||
return platform
|
||||
|
||||
|
||||
def mount_local_lib_path(config_dir):
|
||||
"""Add local library to Python Path."""
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
|
||||
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
verbose=False, skip_pip=False,
|
||||
@@ -226,18 +214,17 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
if config_dir is not None:
|
||||
config_dir = os.path.abspath(config_dir)
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
_mount_local_lib_path(config_dir)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
|
||||
try:
|
||||
process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA(
|
||||
core_config))
|
||||
except vol.MultipleInvalid as ex:
|
||||
conf_util.process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
|
||||
return None
|
||||
|
||||
process_ha_config_upgrade(hass)
|
||||
conf_util.process_ha_config_upgrade(hass)
|
||||
|
||||
if enable_log:
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
@@ -262,9 +249,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
|
||||
if not core_components.setup(hass, config):
|
||||
_LOGGER.error('Home Assistant core failed to initialize. '
|
||||
'Further initialization aborted.')
|
||||
|
||||
return hass
|
||||
|
||||
persistent_notification.setup(hass, config)
|
||||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
@@ -291,12 +279,12 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
|
||||
# Set config dir to directory holding config file
|
||||
config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
hass.config.config_dir = config_dir
|
||||
mount_local_lib_path(config_dir)
|
||||
_mount_local_lib_path(config_dir)
|
||||
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
try:
|
||||
config_dict = config_util.load_yaml_config_file(config_path)
|
||||
config_dict = conf_util.load_yaml_config_file(config_path)
|
||||
except HomeAssistantError:
|
||||
return None
|
||||
|
||||
@@ -355,101 +343,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None):
|
||||
'Unable to setup error log %s (access denied)', err_log_path)
|
||||
|
||||
|
||||
def process_ha_config_upgrade(hass):
|
||||
"""Upgrade config if necessary."""
|
||||
version_path = hass.config.path('.HA_VERSION')
|
||||
|
||||
try:
|
||||
with open(version_path, 'rt') as inp:
|
||||
conf_version = inp.readline().strip()
|
||||
except FileNotFoundError:
|
||||
# Last version to not have this file
|
||||
conf_version = '0.7.7'
|
||||
|
||||
if conf_version == __version__:
|
||||
return
|
||||
|
||||
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
|
||||
__version__)
|
||||
|
||||
# This was where dependencies were installed before v0.18
|
||||
# Probably should keep this around until ~v0.20.
|
||||
lib_path = hass.config.path('lib')
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
lib_path = hass.config.path('deps')
|
||||
if os.path.isdir(lib_path):
|
||||
shutil.rmtree(lib_path)
|
||||
|
||||
with open(version_path, 'wt') as outp:
|
||||
outp.write(__version__)
|
||||
|
||||
|
||||
def process_ha_core_config(hass, config):
|
||||
"""Process the [homeassistant] section from the config."""
|
||||
hac = hass.config
|
||||
|
||||
def set_time_zone(time_zone_str):
|
||||
"""Helper method to set time zone."""
|
||||
if time_zone_str is None:
|
||||
return
|
||||
|
||||
time_zone = date_util.get_time_zone(time_zone_str)
|
||||
|
||||
if time_zone:
|
||||
hac.time_zone = time_zone
|
||||
date_util.set_default_time_zone(time_zone)
|
||||
else:
|
||||
_LOGGER.error('Received invalid time zone %s', time_zone_str)
|
||||
|
||||
for key, attr in ((CONF_LATITUDE, 'latitude'),
|
||||
(CONF_LONGITUDE, 'longitude'),
|
||||
(CONF_NAME, 'location_name')):
|
||||
if key in config:
|
||||
setattr(hac, attr, config[key])
|
||||
|
||||
if CONF_TIME_ZONE in config:
|
||||
set_time_zone(config.get(CONF_TIME_ZONE))
|
||||
|
||||
for entity_id, attrs in config.get(CONF_CUSTOMIZE).items():
|
||||
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
|
||||
|
||||
if CONF_TEMPERATURE_UNIT in config:
|
||||
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
|
||||
|
||||
# If we miss some of the needed values, auto detect them
|
||||
if None not in (
|
||||
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
|
||||
return
|
||||
|
||||
_LOGGER.warning('Incomplete core config. Auto detecting location and '
|
||||
'temperature unit')
|
||||
|
||||
info = loc_util.detect_location_info()
|
||||
|
||||
if info is None:
|
||||
_LOGGER.error('Could not detect location information')
|
||||
return
|
||||
|
||||
if hac.latitude is None and hac.longitude is None:
|
||||
hac.latitude = info.latitude
|
||||
hac.longitude = info.longitude
|
||||
|
||||
if hac.temperature_unit is None:
|
||||
if info.use_fahrenheit:
|
||||
hac.temperature_unit = TEMP_FAHRENHEIT
|
||||
else:
|
||||
hac.temperature_unit = TEMP_CELSIUS
|
||||
|
||||
if hac.location_name is None:
|
||||
hac.location_name = info.city
|
||||
|
||||
if hac.time_zone is None:
|
||||
set_time_zone(info.time_zone)
|
||||
|
||||
|
||||
def _ensure_loader_prepared(hass):
|
||||
"""Ensure Home Assistant loader is prepared."""
|
||||
if not loader.PREPARED:
|
||||
loader.prepare(hass)
|
||||
|
||||
|
||||
def _mount_local_lib_path(config_dir):
|
||||
"""Add local library to Python Path."""
|
||||
sys.path.insert(0, os.path.join(config_dir, 'deps'))
|
||||
|
||||
@@ -19,6 +19,8 @@ from homeassistant.const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
"""Load up the module to call the is_on method.
|
||||
@@ -73,6 +75,11 @@ def toggle(hass, entity_id=None, **service_data):
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
|
||||
|
||||
|
||||
def reload_core_config(hass):
|
||||
"""Reload the core config."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup general services related to Home Assistant."""
|
||||
def handle_turn_service(service):
|
||||
@@ -111,4 +118,21 @@ def setup(hass, config):
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
|
||||
def handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import config as conf_util
|
||||
|
||||
try:
|
||||
path = conf_util.find_config_file(hass.config.config_dir)
|
||||
conf = conf_util.load_yaml_config_file(path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
|
||||
handle_reload_config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
EnvisalinkDevice,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_CODE,
|
||||
CONF_PARTITIONNAME,
|
||||
SIGNAL_PARTITION_UPDATE,
|
||||
SIGNAL_KEYPAD_UPDATE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
_code = discovery_info[CONF_CODE]
|
||||
for part_num in _configured_partitions:
|
||||
_device_config_data = PARTITION_SCHEMA(
|
||||
_configured_partitions[part_num])
|
||||
_device = EnvisalinkAlarm(
|
||||
part_num,
|
||||
_device_config_data[CONF_PARTITIONNAME],
|
||||
_code,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Represents the Envisalink-based alarm panel."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, partition_number, alarm_name, code, info, controller):
|
||||
"""Initialize the alarm panel."""
|
||||
from pydispatch import dispatcher
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
_LOGGER.debug('Setting up alarm: ' + alarm_name)
|
||||
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
def _update_callback(self, partition):
|
||||
"""Update HA state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._info['status']['alarm']:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
elif self._info['status']['armed_away']:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif self._info['status']['armed_stay']:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._info['status']['alpha']:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Not possible for us."""
|
||||
raise NotImplementedError()
|
||||
@@ -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
|
||||
@@ -6,7 +6,7 @@ https://home-assistant.io/developers/api/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from time import time
|
||||
import queue
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
@@ -72,19 +72,14 @@ class APIEventStream(HomeAssistantView):
|
||||
|
||||
def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
from eventlet.queue import LightQueue, Empty
|
||||
import eventlet
|
||||
|
||||
cur_hub = eventlet.hubs.get_hub()
|
||||
request.environ['eventlet.minimum_write_chunk_size'] = 0
|
||||
to_write = LightQueue()
|
||||
stop_obj = object()
|
||||
to_write = queue.Queue()
|
||||
|
||||
restrict = request.args.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',')
|
||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||
|
||||
def thread_forward_events(event):
|
||||
def forward_events(event):
|
||||
"""Forward events to the open request."""
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
@@ -99,28 +94,20 @@ class APIEventStream(HomeAssistantView):
|
||||
else:
|
||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||
|
||||
cur_hub.schedule_call_global(0, lambda: to_write.put(data))
|
||||
to_write.put(data)
|
||||
|
||||
def stream():
|
||||
"""Stream events to response."""
|
||||
self.hass.bus.listen(MATCH_ALL, thread_forward_events)
|
||||
self.hass.bus.listen(MATCH_ALL, forward_events)
|
||||
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
|
||||
last_msg = time()
|
||||
# Fire off one message right away to have browsers fire open event
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Somehow our queue.get sometimes takes too long to
|
||||
# be notified of arrival of data. Probably
|
||||
# because of our spawning on hub in other thread
|
||||
# hack. Because current goal is to get this out,
|
||||
# We just timeout every second because it will
|
||||
# return right away if qsize() > 0.
|
||||
# So yes, we're basically polling :(
|
||||
payload = to_write.get(timeout=1)
|
||||
payload = to_write.get(timeout=STREAM_PING_INTERVAL)
|
||||
|
||||
if payload is stop_obj:
|
||||
break
|
||||
@@ -129,15 +116,13 @@ class APIEventStream(HomeAssistantView):
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
yield msg.encode("UTF-8")
|
||||
last_msg = time()
|
||||
except Empty:
|
||||
if time() - last_msg > 50:
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
except queue.Empty:
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
except GeneratorExit:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
break
|
||||
|
||||
self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events)
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
self.hass.bus.remove_listener(MATCH_ALL, forward_events)
|
||||
|
||||
return self.Response(stream(), mimetype='text/event-stream')
|
||||
|
||||
@@ -204,11 +189,12 @@ class APIEntityStateView(HomeAssistantView):
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = request.json.get('attributes')
|
||||
force_update = request.json.get('force_update', False)
|
||||
|
||||
is_new_state = self.hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
self.hass.states.set(entity_id, new_state, attributes)
|
||||
self.hass.states.set(entity_id, new_state, attributes, force_update)
|
||||
|
||||
# Read the state back for our response
|
||||
resp = self.json(self.hass.states.get(entity_id))
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Support for Envisalink zone states- represented as binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.envisalink/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
ZONE_SCHEMA,
|
||||
CONF_ZONENAME,
|
||||
CONF_ZONETYPE,
|
||||
EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""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])
|
||||
_device = EnvisalinkBinarySensor(
|
||||
zone_num,
|
||||
_device_config_data[CONF_ZONENAME],
|
||||
_device_config_data[CONF_ZONETYPE],
|
||||
EVL_CONTROLLER.alarm_state['zone'][zone_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""Representation of an Envisalink binary sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
self._zone_type = zone_type
|
||||
self._zone_number = zone_number
|
||||
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
EnvisalinkDevice.__init__(self, zone_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault']
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._info['status']['open']
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
def _update_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self.update_ha_state()
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Support for Homematic binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematic/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
SENSOR_TYPES_CLASS = {
|
||||
"Remote": None,
|
||||
"ShutterContact": "opening",
|
||||
"Smoke": "smoke",
|
||||
"SmokeV2": "smoke",
|
||||
"Motion": "motion",
|
||||
"MotionV2": "motion",
|
||||
"RemoteMotion": None
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the Homematic binary sensor platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
return homematic.setup_hmdevice_discovery_helper(HMBinarySensor,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
|
||||
"""Representation of a binary Homematic device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
if not self.available:
|
||||
return False
|
||||
return bool(self._hm_get_state())
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
if not self.available:
|
||||
return None
|
||||
|
||||
# If state is MOTION (RemoteMotion works only)
|
||||
if self._state == "MOTION":
|
||||
return "motion"
|
||||
return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None)
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the HM Object as this HA type."""
|
||||
from pyhomematic.devicetypes.sensors import HMBinarySensor\
|
||||
as pyHMBinarySensor
|
||||
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
# check if the Homematic device correct for this HA device
|
||||
if not isinstance(self._hmdevice, pyHMBinarySensor):
|
||||
_LOGGER.critical("This %s can't be use as binary", self._name)
|
||||
return False
|
||||
|
||||
# if exists user value?
|
||||
if self._state and self._state not in self._hmdevice.BINARYNODE:
|
||||
_LOGGER.critical("This %s have no binary with %s", self._name,
|
||||
self._state)
|
||||
return False
|
||||
|
||||
# only check and give a warning to the user
|
||||
if self._state is None and len(self._hmdevice.BINARYNODE) > 1:
|
||||
_LOGGER.critical("%s have multiple binary params. It use all "
|
||||
"binary nodes as one. Possible param values: %s",
|
||||
self._name, str(self._hmdevice.BINARYNODE))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data struct (self._data) from the Homematic metadata."""
|
||||
super()._init_data_struct()
|
||||
|
||||
# object have 1 binary
|
||||
if self._state is None and len(self._hmdevice.BINARYNODE) == 1:
|
||||
for value in self._hmdevice.BINARYNODE:
|
||||
self._state = value
|
||||
|
||||
# add state to data struct
|
||||
if self._state:
|
||||
_LOGGER.debug("%s init datastruct with main node '%s'", self._name,
|
||||
self._state)
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
@@ -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,12 +5,15 @@ 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.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
@@ -22,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:
|
||||
@@ -40,17 +43,28 @@ 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(BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink sensor."""
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
self.wink = wink
|
||||
super().__init__(wink)
|
||||
wink = get_component('wink')
|
||||
self._unit_of_measurement = self.wink.UNIT
|
||||
self._battery = self.wink.battery_level
|
||||
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."""
|
||||
@@ -67,35 +81,3 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this wink sensor."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor if any."""
|
||||
return self.wink.name()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def update(self):
|
||||
"""Update state of the sensor."""
|
||||
self.wink.update_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -81,8 +82,6 @@ class Camera(Entity):
|
||||
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
import eventlet
|
||||
|
||||
def stream():
|
||||
"""Stream images as mjpeg stream."""
|
||||
try:
|
||||
@@ -99,7 +98,7 @@ class Camera(Entity):
|
||||
|
||||
last_image = img_bytes
|
||||
|
||||
eventlet.sleep(0.5)
|
||||
time.sleep(0.5)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Camera platform that has a Raspberry Pi camera."""
|
||||
"""
|
||||
Camera platform that has a Raspberry Pi camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.rpi_camera/
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
@@ -43,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class RaspberryCamera(Camera):
|
||||
"""Raspberry Pi camera."""
|
||||
"""Representation of a Raspberry Pi camera."""
|
||||
|
||||
def __init__(self, device_info):
|
||||
"""Initialize Raspberry Pi camera component."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -37,6 +37,7 @@ def setup(hass, config):
|
||||
"""Setup a demo environment."""
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
|
||||
config.setdefault(ha.DOMAIN, {})
|
||||
config.setdefault(DOMAIN, {})
|
||||
@@ -59,6 +60,11 @@ def setup(hass, config):
|
||||
demo_config[component] = {CONF_PLATFORM: 'demo'}
|
||||
bootstrap.setup_component(hass, component, demo_config)
|
||||
|
||||
# Setup example persistent notification
|
||||
persistent_notification.create(
|
||||
hass, 'This is an example of a persistent notification.',
|
||||
title='Example Notification')
|
||||
|
||||
# Setup room groups
|
||||
lights = sorted(hass.states.entity_ids('light'))
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
Support for Envisalink devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/envisalink/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['pyenvisalink==1.0', 'pydispatcher==2.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'envisalink'
|
||||
|
||||
EVL_CONTROLLER = None
|
||||
|
||||
CONF_EVL_HOST = 'host'
|
||||
CONF_EVL_PORT = 'port'
|
||||
CONF_PANEL_TYPE = 'panel_type'
|
||||
CONF_EVL_VERSION = 'evl_version'
|
||||
CONF_CODE = 'code'
|
||||
CONF_USERNAME = 'user_name'
|
||||
CONF_PASS = 'password'
|
||||
CONF_EVL_KEEPALIVE = 'keepalive_interval'
|
||||
CONF_ZONEDUMP_INTERVAL = 'zonedump_interval'
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_PARTITIONS = 'partitions'
|
||||
|
||||
CONF_ZONENAME = 'name'
|
||||
CONF_ZONETYPE = 'type'
|
||||
CONF_PARTITIONNAME = 'name'
|
||||
|
||||
DEFAULT_PORT = 4025
|
||||
DEFAULT_EVL_VERSION = 3
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_ZONEDUMP_INTERVAL = 30
|
||||
DEFAULT_ZONETYPE = 'opening'
|
||||
|
||||
SIGNAL_ZONE_UPDATE = 'zones_updated'
|
||||
SIGNAL_PARTITION_UPDATE = 'partition_updated'
|
||||
SIGNAL_KEYPAD_UPDATE = 'keypad_updated'
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONENAME): cv.string,
|
||||
vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string})
|
||||
|
||||
PARTITION_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PARTITIONNAME): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_EVL_HOST): cv.string,
|
||||
vol.Required(CONF_PANEL_TYPE):
|
||||
vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASS): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
|
||||
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=3, max=4)),
|
||||
vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||
vol.Optional(CONF_ZONEDUMP_INTERVAL,
|
||||
default=DEFAULT_ZONEDUMP_INTERVAL):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, too-many-function-args, too-many-locals
|
||||
# pylint: disable=too-many-return-statements
|
||||
def setup(hass, base_config):
|
||||
"""Common setup for Envisalink devices."""
|
||||
from pyenvisalink import EnvisalinkAlarmPanel
|
||||
from pydispatch import dispatcher
|
||||
|
||||
global EVL_CONTROLLER
|
||||
|
||||
config = base_config.get(DOMAIN)
|
||||
|
||||
_host = config.get(CONF_EVL_HOST)
|
||||
_port = config.get(CONF_EVL_PORT)
|
||||
_code = config.get(CONF_CODE)
|
||||
_panel_type = config.get(CONF_PANEL_TYPE)
|
||||
_version = config.get(CONF_EVL_VERSION)
|
||||
_user = config.get(CONF_USERNAME)
|
||||
_pass = config.get(CONF_PASS)
|
||||
_keep_alive = config.get(CONF_EVL_KEEPALIVE)
|
||||
_zone_dump = config.get(CONF_ZONEDUMP_INTERVAL)
|
||||
_zones = config.get(CONF_ZONES)
|
||||
_partitions = config.get(CONF_PARTITIONS)
|
||||
_connect_status = {}
|
||||
EVL_CONTROLLER = EnvisalinkAlarmPanel(_host,
|
||||
_port,
|
||||
_panel_type,
|
||||
_version,
|
||||
_user,
|
||||
_pass,
|
||||
_zone_dump,
|
||||
_keep_alive)
|
||||
|
||||
def login_fail_callback(data):
|
||||
"""Callback for when the evl rejects our login."""
|
||||
_LOGGER.error("The envisalink rejected your credentials.")
|
||||
_connect_status['fail'] = 1
|
||||
|
||||
def connection_fail_callback(data):
|
||||
"""Network failure callback."""
|
||||
_LOGGER.error("Could not establish a connection with the envisalink.")
|
||||
_connect_status['fail'] = 1
|
||||
|
||||
def connection_success_callback(data):
|
||||
"""Callback for a successful connection."""
|
||||
_LOGGER.info("Established a connection with the envisalink.")
|
||||
_connect_status['success'] = 1
|
||||
|
||||
def zones_updated_callback(data):
|
||||
"""Handle zone timer updates."""
|
||||
_LOGGER.info("Envisalink sent a zone update event. Updating zones...")
|
||||
dispatcher.send(signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=None,
|
||||
zone=data)
|
||||
|
||||
def alarm_data_updated_callback(data):
|
||||
"""Handle non-alarm based info updates."""
|
||||
_LOGGER.info("Envisalink sent new alarm info. Updating alarms...")
|
||||
dispatcher.send(signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
|
||||
def partition_updated_callback(data):
|
||||
"""Handle partition changes thrown by evl (including alarms)."""
|
||||
_LOGGER.info("The envisalink sent a partition update event.")
|
||||
dispatcher.send(signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
|
||||
def stop_envisalink(event):
|
||||
"""Shutdown envisalink connection and thread on exit."""
|
||||
_LOGGER.info("Shutting down envisalink.")
|
||||
EVL_CONTROLLER.stop()
|
||||
|
||||
def start_envisalink(event):
|
||||
"""Startup process for the Envisalink."""
|
||||
EVL_CONTROLLER.start()
|
||||
for _ in range(10):
|
||||
if 'success' in _connect_status:
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
|
||||
return True
|
||||
elif 'fail' in _connect_status:
|
||||
return False
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
_LOGGER.error("Timeout occurred while establishing evl connection.")
|
||||
return False
|
||||
|
||||
EVL_CONTROLLER.callback_zone_timer_dump = zones_updated_callback
|
||||
EVL_CONTROLLER.callback_zone_state_change = zones_updated_callback
|
||||
EVL_CONTROLLER.callback_partition_state_change = partition_updated_callback
|
||||
EVL_CONTROLLER.callback_keypad_update = alarm_data_updated_callback
|
||||
EVL_CONTROLLER.callback_login_failure = login_fail_callback
|
||||
EVL_CONTROLLER.callback_login_timeout = connection_fail_callback
|
||||
EVL_CONTROLLER.callback_login_success = connection_success_callback
|
||||
|
||||
_result = start_envisalink(None)
|
||||
if not _result:
|
||||
return False
|
||||
|
||||
# Load sub-components for Envisalink
|
||||
if _partitions:
|
||||
load_platform(hass, 'alarm_control_panel', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
load_platform(hass, 'sensor', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
if _zones:
|
||||
load_platform(hass, 'binary_sensor', 'envisalink',
|
||||
{'zones': _zones}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkDevice(Entity):
|
||||
"""Representation of an Envisalink device."""
|
||||
|
||||
def __init__(self, name, info, controller):
|
||||
"""Initialize the device."""
|
||||
self._controller = controller
|
||||
self._info = info
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
@@ -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 = "7962327e4a29e51d4a6f4ee6cca9acc3"
|
||||
UI = "570e1b8744a58024fc4e256f5e024424"
|
||||
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
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -29,7 +29,7 @@
|
||||
|
||||
|
||||
/* eslint-disable quotes, comma-spacing */
|
||||
var PrecacheConfig = [["/","595e12c9755af231fd80191e4cc74d2e"],["/devEvent","595e12c9755af231fd80191e4cc74d2e"],["/devInfo","595e12c9755af231fd80191e4cc74d2e"],["/devService","595e12c9755af231fd80191e4cc74d2e"],["/devState","595e12c9755af231fd80191e4cc74d2e"],["/devTemplate","595e12c9755af231fd80191e4cc74d2e"],["/history","595e12c9755af231fd80191e4cc74d2e"],["/logbook","595e12c9755af231fd80191e4cc74d2e"],["/map","595e12c9755af231fd80191e4cc74d2e"],["/states","595e12c9755af231fd80191e4cc74d2e"],["/static/core-7962327e4a29e51d4a6f4ee6cca9acc3.js","9c07ffb3f81cfb74f8a051b80cc8f9f0"],["/static/frontend-570e1b8744a58024fc4e256f5e024424.html","595e12c9755af231fd80191e4cc74d2e"],["/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.
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Support for building a Raspberry Pi garage controller in HA.
|
||||
|
||||
Instructions for building the controller can be found here
|
||||
https://github.com/andrewshilliday/garage-door-controller
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/garage_door.rpi_gpio/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from time import sleep
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['rpi_gpio']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_DOORS_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.Schema({
|
||||
'name': str,
|
||||
'relay_pin': int,
|
||||
'state_pin': int,
|
||||
})
|
||||
]
|
||||
)
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
'platform': str,
|
||||
vol.Required('doors'): _DOORS_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the garage door platform."""
|
||||
doors = []
|
||||
doors_conf = config.get('doors')
|
||||
|
||||
for door in doors_conf:
|
||||
doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'],
|
||||
door['state_pin']))
|
||||
add_devices(doors)
|
||||
|
||||
|
||||
class RPiGPIOGarageDoor(GarageDoorDevice):
|
||||
"""Representation of a Raspberry garage door."""
|
||||
|
||||
def __init__(self, name, relay_pin, state_pin):
|
||||
"""Initialize the garage door."""
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._relay_pin = relay_pin
|
||||
self._state_pin = state_pin
|
||||
rpi_gpio.setup_output(self._relay_pin)
|
||||
rpi_gpio.setup_input(self._state_pin, 'UP')
|
||||
rpi_gpio.write_output(self._relay_pin, True)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this garage door."""
|
||||
return "{}.{}".format(self.__class__, self._name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door if any."""
|
||||
return self._name
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the garage door."""
|
||||
self._state = rpi_gpio.read_input(self._state_pin) is True
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if door is closed."""
|
||||
return self._state
|
||||
|
||||
def _trigger(self):
|
||||
"""Trigger the door."""
|
||||
rpi_gpio.write_output(self._relay_pin, False)
|
||||
sleep(0.2)
|
||||
rpi_gpio.write_output(self._relay_pin, True)
|
||||
|
||||
def close_door(self):
|
||||
"""Close the door."""
|
||||
if not self.is_closed:
|
||||
self._trigger()
|
||||
|
||||
def open_door(self):
|
||||
"""Open the door."""
|
||||
if self.is_closed:
|
||||
self._trigger()
|
||||
@@ -7,9 +7,10 @@ https://home-assistant.io/components/garage_door.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
pywink.get_garage_doors())
|
||||
|
||||
|
||||
class WinkGarageDoorDevice(GarageDoorDevice):
|
||||
class WinkGarageDoorDevice(WinkDevice, GarageDoorDevice):
|
||||
"""Representation of a Wink garage door."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the garage door."""
|
||||
self.wink = wink
|
||||
self._battery = self.wink.battery_level
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this wink garage door."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the garage door if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the garage door."""
|
||||
self.wink.update_state()
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if door is closed."""
|
||||
return self.wink.state() == 0
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def close_door(self):
|
||||
"""Close the door."""
|
||||
self.wink.set_state(0)
|
||||
@@ -70,16 +51,3 @@ class WinkGarageDoorDevice(GarageDoorDevice):
|
||||
def open_door(self):
|
||||
"""Open the door."""
|
||||
self.wink.set_state(1)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Support for Zwave garage door components.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/garagedoor.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.garage_door import DOMAIN
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.garage_door import GarageDoorDevice
|
||||
|
||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave garage door device."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY:
|
||||
return
|
||||
if value.type != zwave.TYPE_BOOL:
|
||||
return
|
||||
if value.genre != zwave.GENRE_USER:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
|
||||
|
||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
|
||||
"""Representation of an Zwave garage door device."""
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave garage door."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._node = value.node
|
||||
self._state = value.data
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self._state = value.data
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return the current position of Zwave garage door."""
|
||||
return not self._state
|
||||
|
||||
def close_door(self):
|
||||
"""Close the garage door."""
|
||||
self._value.node.set_switch(self._value.value_id, False)
|
||||
|
||||
def open_door(self):
|
||||
"""Open the garage door."""
|
||||
self._value.node.set_switch(self._value.value_id, True)
|
||||
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
CEC component.
|
||||
|
||||
Requires libcec + Python bindings.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_CEC = None
|
||||
DOMAIN = 'hdmi_cec'
|
||||
SERVICE_SELECT_DEVICE = 'select_device'
|
||||
SERVICE_POWER_ON = 'power_on'
|
||||
SERVICE_STANDBY = 'standby'
|
||||
CONF_DEVICES = 'devices'
|
||||
ATTR_DEVICE = 'device'
|
||||
MAX_DEPTH = 4
|
||||
|
||||
|
||||
# pylint: disable=unnecessary-lambda
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.All(cv.positive_int): vol.Any(lambda devices: DEVICE_SCHEMA(devices),
|
||||
cv.string)
|
||||
})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_DEVICES): DEVICE_SCHEMA
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def parse_mapping(mapping, parents=None):
|
||||
"""Parse configuration device mapping."""
|
||||
if parents is None:
|
||||
parents = []
|
||||
for addr, val in mapping.items():
|
||||
cur = parents + [str(addr)]
|
||||
if isinstance(val, dict):
|
||||
yield from parse_mapping(val, cur)
|
||||
elif isinstance(val, str):
|
||||
yield (val, cur)
|
||||
|
||||
|
||||
def pad_physical_address(addr):
|
||||
"""Right-pad a physical address."""
|
||||
return addr + ['0'] * (MAX_DEPTH - len(addr))
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup CEC capability."""
|
||||
global _CEC
|
||||
|
||||
# cec is only available if libcec is properly installed
|
||||
# and the Python bindings are accessible.
|
||||
try:
|
||||
import cec
|
||||
except ImportError:
|
||||
_LOGGER.error("libcec must be installed")
|
||||
return False
|
||||
|
||||
# Parse configuration into a dict of device name
|
||||
# to physical address represented as a list of
|
||||
# four elements.
|
||||
flat = {}
|
||||
for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})):
|
||||
flat[pair[0]] = pad_physical_address(pair[1])
|
||||
|
||||
# Configure libcec.
|
||||
cfg = cec.libcec_configuration()
|
||||
cfg.strDeviceName = 'HASS'
|
||||
cfg.bActivateSource = 0
|
||||
cfg.bMonitorOnly = 1
|
||||
cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT
|
||||
|
||||
# Set up CEC adapter.
|
||||
_CEC = cec.ICECAdapter.Create(cfg)
|
||||
|
||||
def _power_on(call):
|
||||
"""Power on all devices."""
|
||||
_CEC.PowerOnDevices()
|
||||
|
||||
def _standby(call):
|
||||
"""Standby all devices."""
|
||||
_CEC.StandbyDevices()
|
||||
|
||||
def _select_device(call):
|
||||
"""Select the active device."""
|
||||
path = flat.get(call.data[ATTR_DEVICE])
|
||||
if not path:
|
||||
_LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE])
|
||||
cmds = []
|
||||
for i in range(1, MAX_DEPTH - 1):
|
||||
addr = pad_physical_address(path[:i])
|
||||
cmds.append('1f:82:{}{}:{}{}'.format(*addr))
|
||||
cmds.append('1f:86:{}{}:{}{}'.format(*addr))
|
||||
for cmd in cmds:
|
||||
_CEC.Transmit(_CEC.CommandFromString(cmd))
|
||||
_LOGGER.info("Selected %s", call.data[ATTR_DEVICE])
|
||||
|
||||
def _start_cec(event):
|
||||
"""Open CEC adapter."""
|
||||
adapters = _CEC.DetectAdapters()
|
||||
if len(adapters) == 0:
|
||||
_LOGGER.error("No CEC adapter found")
|
||||
return
|
||||
|
||||
if _CEC.Open(adapters[0].strComName):
|
||||
hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on)
|
||||
hass.services.register(DOMAIN, SERVICE_STANDBY, _standby)
|
||||
hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE,
|
||||
_select_device)
|
||||
else:
|
||||
_LOGGER.error("Failed to open adapter")
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec)
|
||||
return True
|
||||
@@ -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):
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
"""
|
||||
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
|
||||
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.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'
|
||||
|
||||
ATTR_DISCOVER_DEVICES = 'devices'
|
||||
ATTR_PARAM = 'param'
|
||||
ATTR_CHANNEL = 'channel'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_ADDRESS = 'address'
|
||||
|
||||
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', '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'
|
||||
]
|
||||
|
||||
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', {}]
|
||||
}
|
||||
|
||||
HM_PRESS_EVENTS = [
|
||||
'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):
|
||||
"""Setup the Homematic component."""
|
||||
global HOMEMATIC, HOMEMATIC_LINK_DELAY
|
||||
|
||||
from pyhomematic import HMConnection
|
||||
|
||||
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")
|
||||
return False
|
||||
|
||||
# Create server thread
|
||||
bound_system_callback = partial(system_callback_handler, hass, config)
|
||||
HOMEMATIC = HMConnection(local=local_ip,
|
||||
localport=local_port,
|
||||
remote=remote_ip,
|
||||
remoteport=remote_port,
|
||||
systemcallback=bound_system_callback,
|
||||
resolvenames=resolvenames,
|
||||
rpcusername=username,
|
||||
rpcpassword=password,
|
||||
interface_id="homeassistant")
|
||||
|
||||
# Start server thread, connect to peer, initialize to receive events
|
||||
HOMEMATIC.start()
|
||||
|
||||
# Stops server when Homeassistant is shutting down
|
||||
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
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def system_callback_handler(hass, config, src, *args):
|
||||
"""Callback handler."""
|
||||
if src == 'newDevices':
|
||||
_LOGGER.debug("newDevices with: %s", str(args))
|
||||
# pylint: disable=unused-variable
|
||||
(interface_id, dev_descriptions) = args
|
||||
key_dict = {}
|
||||
# Get list of all keys of the devices (ignoring channels)
|
||||
for dev in dev_descriptions:
|
||||
key_dict[dev['ADDRESS'].split(':')[0]] = True
|
||||
|
||||
# Register EVENTS
|
||||
# Search all device with a EVENTNODE that include data
|
||||
bound_event_callback = partial(_hm_event_handler, hass)
|
||||
for dev in key_dict:
|
||||
if dev not in HOMEMATIC.devices:
|
||||
continue
|
||||
|
||||
hmdevice = HOMEMATIC.devices.get(dev)
|
||||
# have events?
|
||||
if len(hmdevice.EVENTNODE) > 0:
|
||||
_LOGGER.debug("Register Events from %s", dev)
|
||||
hmdevice.setEventCallback(callback=bound_event_callback,
|
||||
bequeath=True)
|
||||
|
||||
# If configuration allows autodetection of devices,
|
||||
# all devices not configured are added.
|
||||
if key_dict:
|
||||
for component_name, discovery_type in (
|
||||
('switch', DISCOVER_SWITCHES),
|
||||
('light', DISCOVER_LIGHTS),
|
||||
('rollershutter', DISCOVER_ROLLERSHUTTER),
|
||||
('binary_sensor', DISCOVER_BINARY_SENSORS),
|
||||
('sensor', DISCOVER_SENSORS),
|
||||
('thermostat', DISCOVER_THERMOSTATS)):
|
||||
# Get all devices of a specific type
|
||||
found_devices = _get_devices(discovery_type, key_dict)
|
||||
|
||||
# When devices of this type are found
|
||||
# they are setup in HA and an event is fired
|
||||
if found_devices:
|
||||
# Fire discovery event
|
||||
discovery.load_platform(hass, component_name, DOMAIN, {
|
||||
ATTR_DISCOVER_DEVICES: found_devices
|
||||
}, config)
|
||||
|
||||
|
||||
def _get_devices(device_type, keys):
|
||||
"""Get the Homematic devices."""
|
||||
# run
|
||||
device_arr = []
|
||||
for key in keys:
|
||||
device = HOMEMATIC.devices[key]
|
||||
class_name = device.__class__.__name__
|
||||
metadata = {}
|
||||
|
||||
# is class supported by discovery type
|
||||
if class_name not in HM_DEVICE_TYPES[device_type]:
|
||||
continue
|
||||
|
||||
# Load metadata if needed to generate a param list
|
||||
if device_type == DISCOVER_SENSORS:
|
||||
metadata.update(device.SENSORNODE)
|
||||
elif device_type == DISCOVER_BINARY_SENSORS:
|
||||
metadata.update(device.BINARYNODE)
|
||||
|
||||
params = _create_params_list(device, metadata, device_type)
|
||||
if params:
|
||||
# Generate options for 1...n elements with 1...n params
|
||||
for channel in range(1, device.ELEMENT + 1):
|
||||
_LOGGER.debug("Handling %s:%i", key, channel)
|
||||
if channel in params:
|
||||
for param in params[channel]:
|
||||
name = _create_ha_name(name=device.NAME,
|
||||
channel=channel,
|
||||
param=param)
|
||||
device_dict = dict(platform="homematic",
|
||||
address=key,
|
||||
name=name,
|
||||
channel=channel)
|
||||
if param is not None:
|
||||
device_dict[ATTR_PARAM] = param
|
||||
|
||||
# Add new device
|
||||
device_arr.append(device_dict)
|
||||
else:
|
||||
_LOGGER.debug("Channel %i not in params", channel)
|
||||
else:
|
||||
_LOGGER.debug("Got no params for %s", key)
|
||||
_LOGGER.debug("%s autodiscovery: %s",
|
||||
device_type, str(device_arr))
|
||||
return device_arr
|
||||
|
||||
|
||||
def _create_params_list(hmdevice, metadata, device_type):
|
||||
"""Create a list from HMDevice with all possible parameters in config."""
|
||||
params = {}
|
||||
merge = False
|
||||
|
||||
# use merge?
|
||||
if device_type == DISCOVER_SENSORS:
|
||||
merge = True
|
||||
elif device_type == DISCOVER_BINARY_SENSORS:
|
||||
merge = True
|
||||
|
||||
# Search in sensor and binary metadata per elements
|
||||
for channel in range(1, hmdevice.ELEMENT + 1):
|
||||
param_chan = []
|
||||
for node, meta_chan in metadata.items():
|
||||
try:
|
||||
# Is this attribute ignored?
|
||||
if node in HM_IGNORE_DISCOVERY_NODE:
|
||||
continue
|
||||
if meta_chan == 'c' or meta_chan is None:
|
||||
# Only channel linked data
|
||||
param_chan.append(node)
|
||||
elif channel == 1:
|
||||
# First channel can have other data channel
|
||||
param_chan.append(node)
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.error("Exception generating %s (%s)",
|
||||
hmdevice.ADDRESS, str(metadata))
|
||||
|
||||
# default parameter is merge is off
|
||||
if len(param_chan) == 0 and not merge:
|
||||
param_chan.append(None)
|
||||
|
||||
# Add to channel
|
||||
if len(param_chan) > 0:
|
||||
params.update({channel: param_chan})
|
||||
|
||||
_LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS,
|
||||
str(params))
|
||||
return params
|
||||
|
||||
|
||||
def _create_ha_name(name, channel, param):
|
||||
"""Generate a unique object name."""
|
||||
# HMDevice is a simple device
|
||||
if channel == 1 and param is None:
|
||||
return name
|
||||
|
||||
# Has multiple elements/channels
|
||||
if channel > 1 and param is None:
|
||||
return "{} {}".format(name, channel)
|
||||
|
||||
# With multiple param first elements
|
||||
if channel == 1 and param is not None:
|
||||
return "{} {}".format(name, param)
|
||||
|
||||
# Multiple param on object with multiple elements
|
||||
if channel > 1 and param is not None:
|
||||
return "{} {} {}".format(name, channel, param)
|
||||
|
||||
|
||||
def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info,
|
||||
add_callback_devices):
|
||||
"""Helper to setup Homematic devices with discovery info."""
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
_LOGGER.debug("Add device %s from config: %s",
|
||||
str(hmdevicetype), str(config))
|
||||
|
||||
# create object and add to HA
|
||||
new_device = hmdevicetype(config)
|
||||
add_callback_devices([new_device])
|
||||
|
||||
# link to HM
|
||||
new_device.link_homematic()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _hm_event_handler(hass, device, caller, attribute, value):
|
||||
"""Handle all pyhomematic device events."""
|
||||
try:
|
||||
channel = int(device.split(":")[1])
|
||||
address = device.split(":")[0]
|
||||
hmdevice = HOMEMATIC.devices.get(address)
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.error("Event handling channel convert error!")
|
||||
return
|
||||
|
||||
# is not a event?
|
||||
if attribute not in hmdevice.EVENTNODE:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Event %s for %s channel %i", attribute,
|
||||
hmdevice.NAME, channel)
|
||||
|
||||
# keypress event
|
||||
if attribute in HM_PRESS_EVENTS:
|
||||
hass.bus.fire(EVENT_KEYPRESS, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
ATTR_PARAM: attribute,
|
||||
ATTR_CHANNEL: channel
|
||||
})
|
||||
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."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, config):
|
||||
"""Initialize a generic Homematic device."""
|
||||
self._name = config.get(ATTR_NAME, None)
|
||||
self._address = config.get(ATTR_ADDRESS, None)
|
||||
self._channel = config.get(ATTR_CHANNEL, 1)
|
||||
self._state = config.get(ATTR_PARAM, None)
|
||||
self._data = {}
|
||||
self._hmdevice = None
|
||||
self._connected = False
|
||||
self._available = False
|
||||
|
||||
# Set param to uppercase
|
||||
if self._state:
|
||||
self._state = self._state.upper()
|
||||
|
||||
# Generate name
|
||||
if not self._name:
|
||||
self._name = _create_ha_name(name=self._address,
|
||||
channel=self._channel,
|
||||
param=self._state)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. Homematic states are pushed by the XML RPC Server."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def assumed_state(self):
|
||||
"""Return true if unable to access real state of the device."""
|
||||
return not self._available
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true if device is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {}
|
||||
|
||||
# no data available to create
|
||||
if not self.available:
|
||||
return attr
|
||||
|
||||
# Generate an attributes list
|
||||
for node, data in HM_ATTRIBUTE_SUPPORT.items():
|
||||
# Is an attributes and exists for this object
|
||||
if node in self._data:
|
||||
value = data[1].get(self._data[node], self._data[node])
|
||||
attr[data[0]] = value
|
||||
|
||||
# static attributes
|
||||
attr["ID"] = self._hmdevice.ADDRESS
|
||||
|
||||
return attr
|
||||
|
||||
def link_homematic(self):
|
||||
"""Connect to Homematic."""
|
||||
# device is already linked
|
||||
if self._connected:
|
||||
return True
|
||||
|
||||
# Does a HMDevice from pyhomematic exist?
|
||||
if self._address in HOMEMATIC.devices:
|
||||
# Init
|
||||
self._hmdevice = HOMEMATIC.devices[self._address]
|
||||
self._connected = True
|
||||
|
||||
# Check if Homematic class is okay for HA class
|
||||
_LOGGER.info("Start linking %s to %s", self._address, self._name)
|
||||
if self._check_hm_to_ha_object():
|
||||
try:
|
||||
# Init datapoints of this object
|
||||
self._init_data_struct()
|
||||
if HOMEMATIC_LINK_DELAY:
|
||||
# We delay / pause loading of data to avoid overloading
|
||||
# of CCU / Homegear when doing auto detection
|
||||
time.sleep(HOMEMATIC_LINK_DELAY)
|
||||
self._load_init_data_from_hm()
|
||||
_LOGGER.debug("%s datastruct: %s",
|
||||
self._name, str(self._data))
|
||||
|
||||
# Link events from pyhomatic
|
||||
self._subscribe_homematic_events()
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
self._connected = False
|
||||
_LOGGER.error("Exception while linking %s: %s",
|
||||
self._address, str(err))
|
||||
else:
|
||||
_LOGGER.critical("Delink %s object from HM", self._name)
|
||||
self._connected = False
|
||||
|
||||
# Update HA
|
||||
_LOGGER.debug("%s linking done, send update_ha_state", self._name)
|
||||
self.update_ha_state()
|
||||
else:
|
||||
_LOGGER.debug("%s not found in HOMEMATIC.devices", self._address)
|
||||
|
||||
def _hm_event_callback(self, device, caller, attribute, value):
|
||||
"""Handle all pyhomematic device events."""
|
||||
_LOGGER.debug("%s received event '%s' value: %s", self._name,
|
||||
attribute, value)
|
||||
have_change = False
|
||||
|
||||
# Is data needed for this instance?
|
||||
if attribute in self._data:
|
||||
# Did data change?
|
||||
if self._data[attribute] != value:
|
||||
self._data[attribute] = value
|
||||
have_change = True
|
||||
|
||||
# If available it has changed
|
||||
if attribute is "UNREACH":
|
||||
self._available = bool(value)
|
||||
have_change = True
|
||||
|
||||
# If it has changed data point, update HA
|
||||
if have_change:
|
||||
_LOGGER.debug("%s update_ha_state after '%s'", self._name,
|
||||
attribute)
|
||||
self.update_ha_state()
|
||||
|
||||
def _subscribe_homematic_events(self):
|
||||
"""Subscribe all required events to handle job."""
|
||||
channels_to_sub = {}
|
||||
|
||||
# Push data to channels_to_sub from hmdevice metadata
|
||||
for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE,
|
||||
self._hmdevice.ATTRIBUTENODE,
|
||||
self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE,
|
||||
self._hmdevice.ACTIONNODE):
|
||||
for node, channel in metadata.items():
|
||||
# Data is needed for this instance
|
||||
if node in self._data:
|
||||
# chan is current channel
|
||||
if channel == 'c' or channel is None:
|
||||
channel = self._channel
|
||||
# Prepare for subscription
|
||||
try:
|
||||
if int(channel) >= 0:
|
||||
channels_to_sub.update({int(channel): True})
|
||||
except (ValueError, TypeError):
|
||||
_LOGGER("Invalid channel in metadata from %s",
|
||||
self._name)
|
||||
|
||||
# Set callbacks
|
||||
for channel in channels_to_sub:
|
||||
_LOGGER.debug("Subscribe channel %s from %s",
|
||||
str(channel), self._name)
|
||||
self._hmdevice.setEventCallback(callback=self._hm_event_callback,
|
||||
bequeath=False,
|
||||
channel=channel)
|
||||
|
||||
def _load_init_data_from_hm(self):
|
||||
"""Load first value from pyhomematic."""
|
||||
if not self._connected:
|
||||
return False
|
||||
|
||||
# Read data from pyhomematic
|
||||
for metadata, funct in (
|
||||
(self._hmdevice.ATTRIBUTENODE,
|
||||
self._hmdevice.getAttributeData),
|
||||
(self._hmdevice.WRITENODE, self._hmdevice.getWriteData),
|
||||
(self._hmdevice.SENSORNODE, self._hmdevice.getSensorData),
|
||||
(self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)):
|
||||
for node in metadata:
|
||||
if node in self._data:
|
||||
self._data[node] = funct(name=node, channel=self._channel)
|
||||
|
||||
return True
|
||||
|
||||
def _hm_set_state(self, value):
|
||||
"""Set data to main datapoint."""
|
||||
if self._state in self._data:
|
||||
self._data[self._state] = value
|
||||
|
||||
def _hm_get_state(self):
|
||||
"""Get data from main datapoint."""
|
||||
if self._state in self._data:
|
||||
return self._data[self._state]
|
||||
return None
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if it is possible to use the Homematic object as this HA type.
|
||||
|
||||
NEEDS overwrite by inherit!
|
||||
"""
|
||||
if not self._connected or self._hmdevice is None:
|
||||
_LOGGER.error("HA object is not linked to homematic.")
|
||||
return False
|
||||
|
||||
# Check if button option is correctly set for this object
|
||||
if self._channel > self._hmdevice.ELEMENT:
|
||||
_LOGGER.critical("Button option is not correct for this object!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata.
|
||||
|
||||
NEEDS overwrite by inherit!
|
||||
"""
|
||||
# Add all attributes to data dict
|
||||
for data_note in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data.update({data_note: STATE_UNKNOWN})
|
||||
@@ -1,25 +1,31 @@
|
||||
"""This module provides WSGI application to serve the Home Assistant API."""
|
||||
"""
|
||||
This module provides WSGI application to serve the Home Assistant API.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/http/
|
||||
"""
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import threading
|
||||
import re
|
||||
import ssl
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant import util
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS)
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = "http"
|
||||
REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5",)
|
||||
REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10")
|
||||
|
||||
CONF_API_PASSWORD = "api_password"
|
||||
CONF_SERVER_HOST = "server_host"
|
||||
@@ -31,6 +37,27 @@ CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
|
||||
# TLS configuation follows the best-practice guidelines
|
||||
# specified here: https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
# Intermediate guidelines are followed.
|
||||
SSL_VERSION = ssl.PROTOCOL_SSLv23
|
||||
SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
|
||||
if hasattr(ssl, 'OP_NO_COMPRESSION'):
|
||||
SSL_OPTS |= ssl.OP_NO_COMPRESSION
|
||||
CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \
|
||||
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \
|
||||
"DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \
|
||||
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \
|
||||
"ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \
|
||||
"ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \
|
||||
"ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \
|
||||
"DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \
|
||||
"DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \
|
||||
"ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \
|
||||
"AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \
|
||||
"AES256-SHA:DES-CBC3-SHA:!DSS"
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -93,11 +120,17 @@ def setup(hass, config):
|
||||
cors_origins=cors_origins
|
||||
)
|
||||
|
||||
hass.bus.listen_once(
|
||||
ha.EVENT_HOMEASSISTANT_START,
|
||||
lambda event:
|
||||
threading.Thread(target=server.start, daemon=True,
|
||||
name='WSGI-server').start())
|
||||
def start_wsgi_server(event):
|
||||
"""Start the WSGI server."""
|
||||
server.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server)
|
||||
|
||||
def stop_wsgi_server(event):
|
||||
"""Stop the WSGI server."""
|
||||
server.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server)
|
||||
|
||||
hass.wsgi = server
|
||||
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
|
||||
@@ -216,6 +249,7 @@ class HomeAssistantWSGI(object):
|
||||
self.server_port = server_port
|
||||
self.cors_origins = cors_origins
|
||||
self.event_forwarder = None
|
||||
self.server = None
|
||||
|
||||
def register_view(self, view):
|
||||
"""Register a view with the WSGI server.
|
||||
@@ -283,14 +317,34 @@ class HomeAssistantWSGI(object):
|
||||
|
||||
def start(self):
|
||||
"""Start the wsgi server."""
|
||||
from eventlet import wsgi
|
||||
import eventlet
|
||||
from cherrypy import wsgiserver
|
||||
from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter
|
||||
|
||||
# pylint: disable=too-few-public-methods,super-init-not-called
|
||||
class ContextSSLAdapter(BuiltinSSLAdapter):
|
||||
"""SSL Adapter that takes in an SSL context."""
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
# pylint: disable=no-member
|
||||
self.server = wsgiserver.CherryPyWSGIServer(
|
||||
(self.server_host, self.server_port), self,
|
||||
server_name='Home Assistant')
|
||||
|
||||
sock = eventlet.listen((self.server_host, self.server_port))
|
||||
if self.ssl_certificate:
|
||||
sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate,
|
||||
keyfile=self.ssl_key, server_side=True)
|
||||
wsgi.server(sock, self, log=_LOGGER)
|
||||
context = ssl.SSLContext(SSL_VERSION)
|
||||
context.options |= SSL_OPTS
|
||||
context.set_ciphers(CIPHERS)
|
||||
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
|
||||
self.server.ssl_adapter = ContextSSLAdapter(context)
|
||||
|
||||
threading.Thread(target=self.server.start, daemon=True,
|
||||
name='WSGI-server').start()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the wsgi server."""
|
||||
self.server.stop()
|
||||
|
||||
def dispatch_request(self, request):
|
||||
"""Handle incoming request."""
|
||||
@@ -337,6 +391,10 @@ class HomeAssistantWSGI(object):
|
||||
"""Handle a request for base app + extra apps."""
|
||||
from werkzeug.wsgi import DispatcherMiddleware
|
||||
|
||||
if not self.hass.is_running:
|
||||
from werkzeug.exceptions import BadRequest
|
||||
return BadRequest()(environ, start_response)
|
||||
|
||||
app = DispatcherMiddleware(self.base_app, self.extra_apps)
|
||||
# Strip out any cachebusting MD5 fingerprints
|
||||
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
|
||||
@@ -395,7 +453,12 @@ class HomeAssistantView(object):
|
||||
self.hass.wsgi.api_password):
|
||||
authenticated = True
|
||||
|
||||
if self.requires_auth and not authenticated:
|
||||
if authenticated:
|
||||
_LOGGER.info('Successful login/request from %s',
|
||||
request.remote_addr)
|
||||
elif self.requires_auth and not authenticated:
|
||||
_LOGGER.warning('Login attempt or request with an invalid'
|
||||
'password from %s', request.remote_addr)
|
||||
raise Unauthorized()
|
||||
|
||||
request.authenticated = authenticated
|
||||
@@ -437,7 +500,7 @@ class HomeAssistantView(object):
|
||||
mimetype = mimetypes.guess_type(fil)[0]
|
||||
|
||||
try:
|
||||
fil = open(fil)
|
||||
fil = open(fil, mode='br')
|
||||
except IOError:
|
||||
raise NotFound()
|
||||
|
||||
|
||||
@@ -425,39 +425,39 @@ class HvacDevice(Entity):
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
"""Set new target temperature."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set new target humidity."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing operation."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn auxillary heater on."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
||||
@@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-arguments, abstract-method
|
||||
class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
"""Represents a HeatControl hvac."""
|
||||
|
||||
@@ -98,7 +98,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.node == value.node:
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_properties()
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
@@ -211,6 +211,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
value.data = int(round(temperature, 0))
|
||||
else:
|
||||
value.data = int(temperature)
|
||||
break
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
@@ -218,6 +219,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
||||
if value.command_class == 68 and value.index == 0:
|
||||
value.data = bytes(fan, 'utf-8')
|
||||
break
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
@@ -225,6 +227,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
if value.command_class == 64 and value.index == 0:
|
||||
value.data = bytes(operation_mode, 'utf-8')
|
||||
break
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
@@ -233,3 +236,4 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
value.data = int(swing_mode)
|
||||
break
|
||||
|
||||
@@ -23,7 +23,7 @@ DEFAULT_DATABASE = 'home_assistant'
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
|
||||
REQUIREMENTS = ['influxdb==2.12.0']
|
||||
REQUIREMENTS = ['influxdb==3.0.0']
|
||||
|
||||
CONF_HOST = 'host'
|
||||
CONF_PORT = 'port'
|
||||
|
||||
@@ -39,7 +39,6 @@ def setup(hass, config):
|
||||
_LOGGER.error("Could not connect to Insteon service.")
|
||||
return
|
||||
|
||||
for component in 'light':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, 'light', DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -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
|
||||
@@ -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)),
|
||||
@@ -248,7 +249,8 @@ def setup(hass, config):
|
||||
class Light(ToggleEntity):
|
||||
"""Representation of a light."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
# pylint: disable=no-self-use, abstract-method
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for EnOcean light sources.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.enocean/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
@@ -86,7 +85,7 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
|
||||
self._on_state = False
|
||||
|
||||
def value_changed(self, val):
|
||||
"""Update the internal state of this device in HA."""
|
||||
"""Update the internal state of this device."""
|
||||
self._brightness = math.floor(val / 100.0 * 256.0)
|
||||
self._on_state = bool(val != 0)
|
||||
self.update_ha_state()
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Support for Homematic lighs.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.homematic/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.light import (ATTR_BRIGHTNESS, Light)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the Homematic light platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
return homematic.setup_hmdevice_discovery_helper(HMLight,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
class HMLight(homematic.HMDevice, Light):
|
||||
"""Representation of a Homematic light."""
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
if not self.available:
|
||||
return None
|
||||
# Is dimmer?
|
||||
if self._state is "LEVEL":
|
||||
return int(self._hm_get_state() * 255)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
try:
|
||||
return self._hm_get_state() > 0
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if not self.available:
|
||||
return
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs and self._state is "LEVEL":
|
||||
percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255
|
||||
self._hmdevice.set_level(percent_bright, self._channel)
|
||||
else:
|
||||
self._hmdevice.on(self._channel)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
if self.available:
|
||||
self._hmdevice.off(self._channel)
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the Homematic object as this HA type."""
|
||||
from pyhomematic.devicetypes.actors import Dimmer, Switch
|
||||
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
# Check if the Homematic device is correct for this HA device
|
||||
if isinstance(self._hmdevice, Switch):
|
||||
return True
|
||||
if isinstance(self._hmdevice, Dimmer):
|
||||
return True
|
||||
|
||||
_LOGGER.critical("This %s can't be use as light", self._name)
|
||||
return False
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
from pyhomematic.devicetypes.actors import Dimmer, Switch
|
||||
|
||||
super()._init_data_struct()
|
||||
|
||||
# Use STATE
|
||||
if isinstance(self._hmdevice, Switch):
|
||||
self._state = "STATE"
|
||||
|
||||
# Use LEVEL
|
||||
if isinstance(self._hmdevice, Dimmer):
|
||||
self._state = "LEVEL"
|
||||
|
||||
# Add state to data dict
|
||||
if self._state:
|
||||
_LOGGER.debug("%s init datadict with main node '%s'", self._name,
|
||||
self._state)
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
else:
|
||||
_LOGGER.critical("Can't correctly init light %s.", self._name)
|
||||
@@ -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'
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for LimitlessLED bulbs.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.limitlessled/
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for MySensors lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.mysensors/
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
"""
|
||||
Support for Osram Lightify.
|
||||
|
||||
Uses: https://github.com/aneumeier/python-lightify for the Osram light
|
||||
interface.
|
||||
|
||||
In order to use the platform just add the following to the configuration.yaml:
|
||||
|
||||
light:
|
||||
platform: osramlightify
|
||||
host: <hostname_or_ip>
|
||||
|
||||
Todo:
|
||||
Add support for Non RGBW lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.osramlightify/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from datetime import timedelta
|
||||
@@ -40,7 +30,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Find and return lights."""
|
||||
"""Setup Osram Lightify lights."""
|
||||
import lightify
|
||||
host = config.get(CONF_HOST)
|
||||
if host:
|
||||
@@ -85,7 +75,7 @@ def setup_bridge(bridge, add_devices_callback):
|
||||
|
||||
|
||||
class OsramLightifyLight(Light):
|
||||
"""Defines an Osram Lightify Light."""
|
||||
"""Representation of an Osram Lightify Light."""
|
||||
|
||||
def __init__(self, light_id, light, update_lights):
|
||||
"""Initialize the light."""
|
||||
|
||||
@@ -8,12 +8,13 @@ import logging
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||
Light, ATTR_RGB_COLOR
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
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.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
@@ -35,26 +36,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
WinkLight(light) for light in pywink.get_bulbs())
|
||||
|
||||
|
||||
class WinkLight(Light):
|
||||
class WinkLight(WinkDevice, Light):
|
||||
"""Representation of a Wink light."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""
|
||||
Initialize the light.
|
||||
|
||||
:type wink: pywink.devices.standard.bulb.WinkBulb
|
||||
"""
|
||||
self.wink = wink
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this Wink light."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the light if any."""
|
||||
return self.wink.name()
|
||||
"""Initialize the Wink device."""
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
@@ -66,11 +53,6 @@ class WinkLight(Light):
|
||||
"""Return the brightness of the light."""
|
||||
return int(self.wink.brightness() * 255)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
@property
|
||||
def xy_color(self):
|
||||
"""Current bulb color in CIE 1931 (XY) color space."""
|
||||
@@ -112,7 +94,3 @@ class WinkLight(Light):
|
||||
def turn_off(self):
|
||||
"""Turn the switch off."""
|
||||
self.wink.set_state(False)
|
||||
|
||||
def update(self):
|
||||
"""Update state of the light."""
|
||||
self.wink.update_state(require_desired_state_fulfilled=True)
|
||||
|
||||
@@ -4,13 +4,42 @@ Support for Z-Wave lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.zwave/
|
||||
"""
|
||||
import logging
|
||||
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
from threading import Timer
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN, Light
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
|
||||
ATTR_RGB_COLOR, DOMAIN, Light
|
||||
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_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
|
||||
TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN
|
||||
TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -29,7 +58,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveDimmer(value)])
|
||||
|
||||
if node.has_command_class(zwave.COMMAND_CLASS_COLOR):
|
||||
try:
|
||||
add_devices([ZwaveColorLight(value)])
|
||||
except ValueError as exception:
|
||||
_LOGGER.warning(
|
||||
"Error initializing as color bulb: %s "
|
||||
"Initializing as standard dimmer.", exception)
|
||||
add_devices([ZwaveDimmer(value)])
|
||||
else:
|
||||
add_devices([ZwaveDimmer(value)])
|
||||
|
||||
|
||||
def brightness_state(value):
|
||||
@@ -50,8 +89,9 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
from pydispatch import dispatcher
|
||||
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
|
||||
self._brightness, self._state = brightness_state(value)
|
||||
self._brightness = None
|
||||
self._state = None
|
||||
self.update_properties()
|
||||
|
||||
# Used for value change event handling
|
||||
self._refreshing = False
|
||||
@@ -60,6 +100,11 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
dispatcher.connect(
|
||||
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def update_properties(self):
|
||||
"""Update internal properties based on zwave values."""
|
||||
# Brightness
|
||||
self._brightness, self._state = brightness_state(self._value)
|
||||
|
||||
def _value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id != value.value_id:
|
||||
@@ -67,7 +112,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
|
||||
if self._refreshing:
|
||||
self._refreshing = False
|
||||
self._brightness, self._state = brightness_state(value)
|
||||
self.update_properties()
|
||||
else:
|
||||
def _refresh_value():
|
||||
"""Used timer callback for delayed value refresh."""
|
||||
@@ -108,3 +153,164 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
"""Turn the device off."""
|
||||
if self._value.node.set_dimmer(self._value.value_id, 0):
|
||||
self._state = STATE_OFF
|
||||
|
||||
|
||||
def ct_to_rgb(temp):
|
||||
"""Convert color temperature (mireds) to RGB."""
|
||||
colorlist = list(
|
||||
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
|
||||
return [int(val) for val in colorlist]
|
||||
|
||||
|
||||
class ZwaveColorLight(ZwaveDimmer):
|
||||
"""Representation of a Z-Wave color changing light."""
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the light."""
|
||||
self._value_color = None
|
||||
self._value_color_channels = None
|
||||
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
|
||||
# only include one dimmer and one color command, but this will
|
||||
# hopefully provide some forward compatibility for new devices that
|
||||
# have multiple color changing elements.
|
||||
for value_color in value.node.get_rgbbulbs().values():
|
||||
if value.instance == value_color.instance:
|
||||
self._value_color = value_color
|
||||
|
||||
if self._value_color is None:
|
||||
raise ValueError("No matching color command found.")
|
||||
|
||||
for value_color_channels in value.node.get_values(
|
||||
class_id=zwave.COMMAND_CLASS_COLOR, genre='System',
|
||||
type="Int").values():
|
||||
self._value_color_channels = value_color_channels
|
||||
|
||||
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):
|
||||
"""Update internal properties based on zwave values."""
|
||||
super().update_properties()
|
||||
|
||||
# Color Channels
|
||||
self._color_channels = self._value_color_channels.data
|
||||
|
||||
# Color Data String
|
||||
data = self._value_color.data
|
||||
|
||||
# RGB is always present in the openzwave color data string.
|
||||
self._rgb = [
|
||||
int(data[1:3], 16),
|
||||
int(data[3:5], 16),
|
||||
int(data[5:7], 16)]
|
||||
|
||||
# Parse remaining color channels. Openzwave appends white channels
|
||||
# that are present.
|
||||
index = 7
|
||||
|
||||
# Warm white
|
||||
if self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
||||
warm_white = int(data[index:index+2], 16)
|
||||
index += 2
|
||||
else:
|
||||
warm_white = 0
|
||||
|
||||
# Cold white
|
||||
if self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
||||
cold_white = int(data[index:index+2], 16)
|
||||
index += 2
|
||||
else:
|
||||
cold_white = 0
|
||||
|
||||
# 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 warm_white > 0:
|
||||
self._ct = TEMP_WARM_HASS
|
||||
self._rgb = ct_to_rgb(self._ct)
|
||||
elif cold_white > 0:
|
||||
self._ct = TEMP_COLD_HASS
|
||||
self._rgb = ct_to_rgb(self._ct)
|
||||
else:
|
||||
# RGB color is being used. Just report midpoint.
|
||||
self._ct = TEMP_MID_HASS
|
||||
|
||||
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
|
||||
self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=warm_white))
|
||||
|
||||
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
|
||||
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
|
||||
self._color_channels & COLOR_CHANNEL_GREEN or
|
||||
self._color_channels & COLOR_CHANNEL_BLUE):
|
||||
self._rgb = None
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the rgb color."""
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature."""
|
||||
return self._ct
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
rgbw = None
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
# 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'
|
||||
else:
|
||||
self._ct = TEMP_COLD_HASS
|
||||
rgbw = b'#00000000FF'
|
||||
|
||||
elif ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
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")
|
||||
else:
|
||||
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
|
||||
|
||||
super().turn_on(**kwargs)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Support for Vera locks.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/lock.vera/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED)
|
||||
from homeassistant.components.vera import (
|
||||
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
|
||||
|
||||
DEPENDENCIES = ['vera']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Find and return Vera locks."""
|
||||
add_devices_callback(
|
||||
VeraLock(device, VERA_CONTROLLER) for
|
||||
device in VERA_DEVICES['lock'])
|
||||
|
||||
|
||||
class VeraLock(VeraDevice, LockDevice):
|
||||
"""Representation of a Vera lock."""
|
||||
|
||||
def __init__(self, vera_device, controller):
|
||||
"""Initialize the Vera device."""
|
||||
self._state = None
|
||||
VeraDevice.__init__(self, vera_device, controller)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
attr = {}
|
||||
if self.vera_device.has_battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
|
||||
|
||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
||||
return attr
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
self.vera_device.lock()
|
||||
self._state = STATE_LOCKED
|
||||
self.update_ha_state()
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
self.vera_device.unlock()
|
||||
self._state = STATE_UNLOCKED
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if device is on."""
|
||||
return self._state == STATE_LOCKED
|
||||
|
||||
def update(self):
|
||||
"""Called by the Vera device callback to update state."""
|
||||
self._state = (STATE_LOCKED if self.vera_device.is_locked(True)
|
||||
else STATE_UNLOCKED)
|
||||
@@ -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):
|
||||
|
||||
@@ -7,9 +7,10 @@ https://home-assistant.io/components/lock.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -30,38 +31,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(WinkLockDevice(lock) for lock in pywink.get_locks())
|
||||
|
||||
|
||||
class WinkLockDevice(LockDevice):
|
||||
class WinkLockDevice(WinkDevice, LockDevice):
|
||||
"""Representation of a Wink lock."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the lock."""
|
||||
self.wink = wink
|
||||
self._battery = self.wink.battery_level
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of this wink lock."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the lock if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the lock."""
|
||||
self.wink.update_state()
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if device is locked."""
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
self.wink.set_state(True)
|
||||
@@ -69,16 +50,3 @@ class WinkLockDevice(LockDevice):
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
self.wink.set_state(False)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._battery:
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL: self._battery_level,
|
||||
}
|
||||
|
||||
@property
|
||||
def _battery_level(self):
|
||||
"""Return the battery level."""
|
||||
return self.wink.battery_level * 100
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
Support for interface with a Sony Bravia TV.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.braviatv/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/aparraga/braviarc/archive/0.3.3.zip'
|
||||
'#braviarc==0.3.3']
|
||||
|
||||
BRAVIA_CONFIG_FILE = 'bravia.conf'
|
||||
CLIENTID_PREFIX = 'HomeAssistant'
|
||||
NICKNAME = 'Home Assistant'
|
||||
|
||||
# Map ip to request id for configuring
|
||||
_CONFIGURING = {}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
|
||||
|
||||
def _get_mac_address(ip_address):
|
||||
"""Get the MAC address of the device."""
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
pid = Popen(["arp", "-n", ip_address], stdout=PIPE)
|
||||
pid_component = pid.communicate()[0]
|
||||
mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'),
|
||||
pid_component).groups()[0]
|
||||
return mac
|
||||
|
||||
|
||||
def _config_from_file(filename, config=None):
|
||||
"""Create the configuration from a file."""
|
||||
if config:
|
||||
# We're writing configuration
|
||||
bravia_config = _config_from_file(filename)
|
||||
if bravia_config is None:
|
||||
bravia_config = {}
|
||||
new_config = bravia_config.copy()
|
||||
new_config.update(config)
|
||||
try:
|
||||
with open(filename, 'w') as fdesc:
|
||||
fdesc.write(json.dumps(new_config))
|
||||
except IOError as error:
|
||||
_LOGGER.error('Saving config file failed: %s', error)
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
# We're reading config
|
||||
if os.path.isfile(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fdesc:
|
||||
return json.loads(fdesc.read())
|
||||
except ValueError as error:
|
||||
return {}
|
||||
except IOError as error:
|
||||
_LOGGER.error('Reading config file failed: %s', error)
|
||||
# This won't work yet
|
||||
return False
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the Sony Bravia TV platform."""
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
if host is None:
|
||||
return # if no host configured, do not continue
|
||||
|
||||
pin = None
|
||||
bravia_config = _config_from_file(hass.config.path(BRAVIA_CONFIG_FILE))
|
||||
while len(bravia_config):
|
||||
# Setup a configured TV
|
||||
host_ip, host_config = bravia_config.popitem()
|
||||
if host_ip == host:
|
||||
pin = host_config['pin']
|
||||
mac = host_config['mac']
|
||||
name = config.get(CONF_NAME)
|
||||
add_devices_callback([BraviaTVDevice(host, mac, name, pin)])
|
||||
return
|
||||
|
||||
setup_bravia(config, pin, hass, add_devices_callback)
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def setup_bravia(config, pin, hass, add_devices_callback):
|
||||
"""Setup a Sony Bravia TV based on host parameter."""
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
if name is None:
|
||||
name = "Sony Bravia TV"
|
||||
|
||||
if pin is None:
|
||||
request_configuration(config, hass, add_devices_callback)
|
||||
return
|
||||
else:
|
||||
mac = _get_mac_address(host)
|
||||
if mac is not None:
|
||||
mac = mac.decode('utf8')
|
||||
# If we came here and configuring this host, mark as done
|
||||
if host in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(host)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
_LOGGER.info('Discovery configuration done!')
|
||||
|
||||
# Save config
|
||||
if not _config_from_file(
|
||||
hass.config.path(BRAVIA_CONFIG_FILE),
|
||||
{host: {'pin': pin, 'host': host, 'mac': mac}}):
|
||||
_LOGGER.error('failed to save config file')
|
||||
|
||||
add_devices_callback([BraviaTVDevice(host, mac, name, pin)])
|
||||
|
||||
|
||||
def request_configuration(config, hass, add_devices_callback):
|
||||
"""Request configuration steps from the user."""
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
if name is None:
|
||||
name = "Sony Bravia"
|
||||
|
||||
configurator = get_component('configurator')
|
||||
|
||||
# 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.")
|
||||
return
|
||||
|
||||
def bravia_configuration_callback(data):
|
||||
"""Callback after user enter PIN."""
|
||||
from braviarc import braviarc
|
||||
|
||||
pin = data.get('pin')
|
||||
braviarc = braviarc.BraviaRC(host)
|
||||
braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME)
|
||||
if braviarc.is_connected():
|
||||
setup_bravia(config, pin, hass, add_devices_callback)
|
||||
else:
|
||||
request_configuration(config, hass, add_devices_callback)
|
||||
|
||||
_CONFIGURING[host] = configurator.request_config(
|
||||
hass, name, bravia_configuration_callback,
|
||||
description='Enter the Pin shown on your Sony Bravia TV.' +
|
||||
'If no Pin is shown, enter 0000 to let TV show you a Pin.',
|
||||
description_image="/static/images/smart-tv.png",
|
||||
submit_caption="Confirm",
|
||||
fields=[{'id': 'pin', 'name': 'Enter the pin', 'type': ''}]
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method, too-many-public-methods,
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
class BraviaTVDevice(MediaPlayerDevice):
|
||||
"""Representation of a Sony Bravia TV."""
|
||||
|
||||
def __init__(self, host, mac, name, pin):
|
||||
"""Initialize the Sony Bravia device."""
|
||||
from braviarc import braviarc
|
||||
|
||||
self._pin = pin
|
||||
self._braviarc = braviarc.BraviaRC(host, mac)
|
||||
self._name = name
|
||||
self._state = STATE_OFF
|
||||
self._muted = False
|
||||
self._program_name = None
|
||||
self._channel_name = None
|
||||
self._channel_number = None
|
||||
self._source = None
|
||||
self._source_list = []
|
||||
self._original_content_list = []
|
||||
self._content_mapping = {}
|
||||
self._duration = None
|
||||
self._content_uri = None
|
||||
self._id = None
|
||||
self._playing = False
|
||||
self._start_date_time = None
|
||||
self._program_media_type = None
|
||||
self._min_volume = None
|
||||
self._max_volume = None
|
||||
self._volume = None
|
||||
|
||||
self._braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME)
|
||||
if self._braviarc.is_connected():
|
||||
self.update()
|
||||
else:
|
||||
self._state = STATE_OFF
|
||||
|
||||
def update(self):
|
||||
"""Update TV info."""
|
||||
if not self._braviarc.is_connected():
|
||||
self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME)
|
||||
if not self._braviarc.is_connected():
|
||||
return
|
||||
|
||||
# Retrieve the latest data.
|
||||
try:
|
||||
if self._state == STATE_ON:
|
||||
# refresh volume info:
|
||||
self._refresh_volume()
|
||||
self._refresh_channels()
|
||||
|
||||
power_status = self._braviarc.get_power_status()
|
||||
if power_status == 'active':
|
||||
self._state = STATE_ON
|
||||
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)
|
||||
self._state = STATE_OFF
|
||||
|
||||
def _refresh_volume(self):
|
||||
"""Refresh volume information."""
|
||||
volume_info = self._braviarc.get_volume_info()
|
||||
if volume_info is not None:
|
||||
self._volume = volume_info.get('volume')
|
||||
self._min_volume = volume_info.get('minVolume')
|
||||
self._max_volume = volume_info.get('maxVolume')
|
||||
self._muted = volume_info.get('mute')
|
||||
|
||||
def _refresh_channels(self):
|
||||
if len(self._source_list) == 0:
|
||||
self._content_mapping = self._braviarc. \
|
||||
load_source_list()
|
||||
self._source_list = []
|
||||
for key in self._content_mapping:
|
||||
self._source_list.append(key)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
return self._source
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return self._source_list
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
if self._volume is not None:
|
||||
return self._volume / 100
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Boolean if volume is currently muted."""
|
||||
return self._muted
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_BRAVIA
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return_value = None
|
||||
if self._channel_name is not None:
|
||||
return_value = self._channel_name
|
||||
if self._program_name is not None:
|
||||
return_value = return_value + ': ' + self._program_name
|
||||
return return_value
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
return self._channel_name
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
return self._duration
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
self._braviarc.set_volume_level(volume)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
self._braviarc.turn_on()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self._braviarc.turn_off()
|
||||
|
||||
def volume_up(self):
|
||||
"""Volume up the media player."""
|
||||
self._braviarc.volume_up()
|
||||
|
||||
def volume_down(self):
|
||||
"""Volume down media player."""
|
||||
self._braviarc.volume_down()
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Send mute command."""
|
||||
self._braviarc.mute_volume(mute)
|
||||
|
||||
def select_source(self, source):
|
||||
"""Set the input source."""
|
||||
if source in self._content_mapping:
|
||||
uri = self._content_mapping[source]
|
||||
self._braviarc.play_content(uri)
|
||||
|
||||
def media_play_pause(self):
|
||||
"""Simulate play pause media player."""
|
||||
if self._playing:
|
||||
self.media_pause()
|
||||
else:
|
||||
self.media_play()
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
self._playing = True
|
||||
self._braviarc.media_play()
|
||||
|
||||
def media_pause(self):
|
||||
"""Send media pause command to media player."""
|
||||
self._playing = False
|
||||
self._braviarc.media_pause()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self._braviarc.media_next_track()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send the previous track command."""
|
||||
self._braviarc.media_previous_track()
|
||||
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
Support for interacting with and controlling the cmus music player.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.cmus/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD,
|
||||
CONF_PORT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pycmus==0.1.0']
|
||||
|
||||
SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
|
||||
SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discover_info=None):
|
||||
"""Setup the CMUS platform."""
|
||||
from pycmus import exceptions
|
||||
|
||||
host = config.get(CONF_HOST, None)
|
||||
password = config.get(CONF_PASSWORD, None)
|
||||
port = config.get(CONF_PORT, None)
|
||||
name = config.get(CONF_NAME, None)
|
||||
if host and not password:
|
||||
_LOGGER.error("A password must be set if using a remote cmus server")
|
||||
return False
|
||||
try:
|
||||
cmus_remote = CmusDevice(host, password, port, name)
|
||||
except exceptions.InvalidPassword:
|
||||
_LOGGER.error("The provided password was rejected by cmus")
|
||||
return False
|
||||
add_devices([cmus_remote])
|
||||
|
||||
|
||||
class CmusDevice(MediaPlayerDevice):
|
||||
"""Representation of a running CMUS."""
|
||||
|
||||
# pylint: disable=no-member, too-many-public-methods, abstract-method
|
||||
def __init__(self, server, password, port, name):
|
||||
"""Initialize the CMUS device."""
|
||||
from pycmus import remote
|
||||
|
||||
if server:
|
||||
port = port or 3000
|
||||
self.cmus = remote.PyCmus(server=server, password=password,
|
||||
port=port)
|
||||
auto_name = "cmus-%s" % server
|
||||
else:
|
||||
self.cmus = remote.PyCmus()
|
||||
auto_name = "cmus-local"
|
||||
self._name = name or auto_name
|
||||
self.status = {}
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
status = self.cmus.get_status_dict()
|
||||
if not status:
|
||||
_LOGGER.warning("Recieved no status from cmus")
|
||||
else:
|
||||
self.status = status
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the media state."""
|
||||
if 'status' not in self.status:
|
||||
self.update()
|
||||
if self.status['status'] == 'playing':
|
||||
return STATE_PLAYING
|
||||
elif self.status['status'] == 'paused':
|
||||
return STATE_PAUSED
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
return self.status.get('file')
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
"""Content type of the current playing media."""
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
return self.status.get('duration')
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self.status['tag'].get('title')
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
return self.status['tag'].get('artist')
|
||||
|
||||
@property
|
||||
def media_track(self):
|
||||
"""Track number of current playing media, music track only."""
|
||||
return self.status['tag'].get('tracknumber')
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album name of current playing media, music track only."""
|
||||
return self.status['tag'].get('album')
|
||||
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
"""Album artist of current playing media, music track only."""
|
||||
return self.status['tag'].get('albumartist')
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Return the volume level."""
|
||||
left = self.status['set'].get('vol_left')[0]
|
||||
right = self.status['set'].get('vol_right')[0]
|
||||
if left != right:
|
||||
volume = float(left + right) / 2
|
||||
else:
|
||||
volume = left
|
||||
return int(volume)/100
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_CMUS
|
||||
|
||||
def turn_off(self):
|
||||
"""Service to send the CMUS the command to stop playing."""
|
||||
self.cmus.player_stop()
|
||||
|
||||
def turn_on(self):
|
||||
"""Service to send the CMUS the command to start playing."""
|
||||
self.cmus.player_play()
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
self.cmus.set_volume(int(volume * 100))
|
||||
|
||||
def volume_up(self):
|
||||
"""Function to send CMUS the command for volume up."""
|
||||
left = self.status['set'].get('vol_left')
|
||||
right = self.status['set'].get('vol_right')
|
||||
if left != right:
|
||||
current_volume = float(left + right) / 2
|
||||
else:
|
||||
current_volume = left
|
||||
|
||||
if current_volume <= 100:
|
||||
self.cmus.set_volume(int(current_volume) + 5)
|
||||
|
||||
def volume_down(self):
|
||||
"""Function to send CMUS the command for volume down."""
|
||||
left = self.status['set'].get('vol_left')
|
||||
right = self.status['set'].get('vol_right')
|
||||
if left != right:
|
||||
current_volume = float(left + right) / 2
|
||||
else:
|
||||
current_volume = left
|
||||
|
||||
if current_volume <= 100:
|
||||
self.cmus.set_volume(int(current_volume) - 5)
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Send the play command."""
|
||||
if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]:
|
||||
self.cmus.player_play_file(media_id)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Invalid media type %s. Only %s and %s are supported",
|
||||
media_type, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST)
|
||||
|
||||
def media_pause(self):
|
||||
"""Send the pause command."""
|
||||
self.cmus.player_pause()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.cmus.player_next()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send next track command."""
|
||||
self.cmus.player_prev()
|
||||
|
||||
def media_seek(self, position):
|
||||
"""Send seek command."""
|
||||
self.cmus.seek(position)
|
||||
|
||||
def media_play(self):
|
||||
"""Send the play command."""
|
||||
self.cmus.player_play()
|
||||
|
||||
def media_stop(self):
|
||||
"""Send the stop command."""
|
||||
self.cmus.stop()
|
||||
@@ -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."""
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['websocket-client==0.35.0']
|
||||
REQUIREMENTS = ['websocket-client==0.37.0']
|
||||
SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['jsonrpc-requests==0.2']
|
||||
REQUIREMENTS = ['jsonrpc-requests==0.3']
|
||||
|
||||
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
@@ -301,4 +301,4 @@ class KodiDevice(MediaPlayerDevice):
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Send the play_media command to the media player."""
|
||||
self._server.Player.Open({media_type: media_id}, {})
|
||||
self._server.Player.Open({"item": {"file": str(media_id)}})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for the roku media player.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.roku/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -16,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
|
||||
@@ -46,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)
|
||||
|
||||
@@ -62,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):
|
||||
@@ -77,8 +86,9 @@ class RokuDevice(MediaPlayerDevice):
|
||||
self.current_app = self.roku.current_app
|
||||
else:
|
||||
self.current_app = None
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.current_app = None
|
||||
except (requests.exceptions.ConnectionError,
|
||||
requests.exceptions.ReadTimeout):
|
||||
_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).
|
||||
|
||||
@@ -154,12 +162,20 @@ sonos_group_players:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
sonos_unjoin:
|
||||
description: Unjoin the player from a group.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will be unjoined from their group. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
sonos_snapshot:
|
||||
description: Take a snapshot of the media player.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
description: Name(s) of entites that will be snapshot. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
sonos_restore:
|
||||
@@ -167,5 +183,5 @@ sonos_restore:
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
description: Name(s) of entites that will be restored. Platform dependent.
|
||||
example: 'media_player.living_room_sonos'
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for interacting with Snapcast clients.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.snapcast/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
|
||||
|
||||
@@ -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,14 +32,19 @@ _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
|
||||
|
||||
# pylint: disable=unused-argument, too-many-locals
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Sonos platform."""
|
||||
import soco
|
||||
@@ -72,47 +78,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
add_devices(devices)
|
||||
_LOGGER.info('Added %s Sonos speakers', len(players))
|
||||
|
||||
def _apply_service(service, service_func, *service_func_args):
|
||||
"""Internal func for applying a service."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
service_func(device, *service_func_args)
|
||||
device.update_ha_state(True)
|
||||
|
||||
def group_players_service(service):
|
||||
"""Group media players, use player as coordinator."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
_apply_service(service, SonosDevice.group_players)
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
def unjoin_service(service):
|
||||
"""Unjoin the player from a group."""
|
||||
_apply_service(service, SonosDevice.unjoin)
|
||||
|
||||
for device in _devices:
|
||||
device.group_players()
|
||||
device.update_ha_state(True)
|
||||
|
||||
def snapshot(service):
|
||||
def snapshot_service(service):
|
||||
"""Take a snapshot."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
_apply_service(service, SonosDevice.snapshot)
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
device.snapshot(service)
|
||||
device.update_ha_state(True)
|
||||
|
||||
def restore(service):
|
||||
def restore_service(service):
|
||||
"""Restore a snapshot."""
|
||||
entity_id = service.data.get('entity_id')
|
||||
|
||||
if entity_id:
|
||||
_devices = [device for device in devices
|
||||
if device.entity_id == entity_id]
|
||||
else:
|
||||
_devices = devices
|
||||
|
||||
for device in _devices:
|
||||
device.restore(service)
|
||||
device.update_ha_state(True)
|
||||
_apply_service(service, SonosDevice.restore)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
@@ -121,12 +115,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
group_players_service,
|
||||
descriptions.get(SERVICE_GROUP_PLAYERS))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_UNJOIN,
|
||||
unjoin_service,
|
||||
descriptions.get(SERVICE_UNJOIN))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
|
||||
snapshot,
|
||||
snapshot_service,
|
||||
descriptions.get(SERVICE_SNAPSHOT))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_RESTORE,
|
||||
restore,
|
||||
restore_service,
|
||||
descriptions.get(SERVICE_RESTORE))
|
||||
|
||||
return True
|
||||
@@ -169,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
|
||||
@@ -268,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'],
|
||||
@@ -297,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."""
|
||||
@@ -327,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."""
|
||||
@@ -356,12 +393,17 @@ class SonosDevice(MediaPlayerDevice):
|
||||
self._player.partymode()
|
||||
|
||||
@only_if_coordinator
|
||||
def snapshot(self, service):
|
||||
def unjoin(self):
|
||||
"""Unjoin the player from a group."""
|
||||
self._player.unjoin()
|
||||
|
||||
@only_if_coordinator
|
||||
def snapshot(self):
|
||||
"""Snapshot the player."""
|
||||
self.soco_snapshot.snapshot()
|
||||
|
||||
@only_if_coordinator
|
||||
def restore(self, service):
|
||||
def restore(self):
|
||||
"""Restore snapshot for the player."""
|
||||
self.soco_snapshot.restore(True)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Combination of multiple media players into one for a universal controller.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.universal/
|
||||
"""
|
||||
|
||||
import logging
|
||||
# pylint: disable=import-error
|
||||
from copy import copy
|
||||
@@ -18,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,
|
||||
@@ -347,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
|
||||
@@ -425,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:
|
||||
|
||||
@@ -29,7 +29,7 @@ MQTT_CLIENT = None
|
||||
SERVICE_PUBLISH = 'publish'
|
||||
EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received'
|
||||
|
||||
REQUIREMENTS = ['paho-mqtt==1.1']
|
||||
REQUIREMENTS = ['paho-mqtt==1.2']
|
||||
|
||||
CONF_EMBEDDED = 'embedded'
|
||||
CONF_BROKER = 'broker'
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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)
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/notify.pushover/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TITLE, DOMAIN, BaseNotificationService)
|
||||
ATTR_TITLE, ATTR_TARGET, ATTR_DATA, DOMAIN, BaseNotificationService)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
@@ -51,7 +51,18 @@ class PushoverNotificationService(BaseNotificationService):
|
||||
"""Send a message to a user."""
|
||||
from pushover import RequestError
|
||||
|
||||
# Make a copy and use empty dict if necessary
|
||||
data = dict(kwargs.get(ATTR_DATA) or {})
|
||||
|
||||
data['title'] = kwargs.get(ATTR_TITLE)
|
||||
|
||||
target = kwargs.get(ATTR_TARGET)
|
||||
if target is not None:
|
||||
data['device'] = target
|
||||
|
||||
try:
|
||||
self.pushover.send_message(message, title=kwargs.get(ATTR_TITLE))
|
||||
self.pushover.send_message(message, **data)
|
||||
except ValueError as val_err:
|
||||
_LOGGER.error(str(val_err))
|
||||
except RequestError:
|
||||
_LOGGER.exception("Could not send pushover notification")
|
||||
|
||||
@@ -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.16']
|
||||
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
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
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
|
||||
|
||||
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 + '.{}'
|
||||
|
||||
SERVICE_CREATE = 'create'
|
||||
ATTR_TITLE = 'title'
|
||||
ATTR_MESSAGE = 'message'
|
||||
ATTR_NOTIFICATION_ID = 'notification_id'
|
||||
|
||||
SCHEMA_SERVICE_CREATE = vol.Schema({
|
||||
vol.Required(ATTR_MESSAGE): cv.template,
|
||||
vol.Optional(ATTR_TITLE): cv.template,
|
||||
vol.Optional(ATTR_NOTIFICATION_ID): cv.string,
|
||||
})
|
||||
|
||||
|
||||
DEFAULT_OBJECT_ID = 'notification'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create(hass, message, title=None, notification_id=None):
|
||||
"""Generate a notification."""
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
(ATTR_TITLE, title),
|
||||
(ATTR_MESSAGE, message),
|
||||
(ATTR_NOTIFICATION_ID, notification_id),
|
||||
] if value is not None
|
||||
}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_CREATE, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the persistent notification component."""
|
||||
def create_service(call):
|
||||
"""Handle a create notification service call."""
|
||||
title = call.data.get(ATTR_TITLE)
|
||||
message = call.data.get(ATTR_MESSAGE)
|
||||
notification_id = call.data.get(ATTR_NOTIFICATION_ID)
|
||||
|
||||
if notification_id is not None:
|
||||
entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id))
|
||||
else:
|
||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID,
|
||||
hass=hass)
|
||||
attr = {}
|
||||
if title is not None:
|
||||
try:
|
||||
title = template.render(hass, title)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error('Error rendering title %s: %s', title, ex)
|
||||
|
||||
attr[ATTR_TITLE] = title
|
||||
|
||||
try:
|
||||
message = template.render(hass, message)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error('Error rendering message %s: %s', message, ex)
|
||||
|
||||
hass.states.set(entity_id, message, attr)
|
||||
|
||||
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.")
|
||||
@@ -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.")
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
The homematic rollershutter platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/rollershutter.homematic/
|
||||
|
||||
Important: For this platform to work the homematic component has to be
|
||||
properly configured.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN)
|
||||
from homeassistant.components.rollershutter import RollershutterDevice,\
|
||||
ATTR_CURRENT_POSITION
|
||||
import homeassistant.components.homematic as homematic
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
return homematic.setup_hmdevice_discovery_helper(HMRollershutter,
|
||||
discovery_info,
|
||||
add_callback_devices)
|
||||
|
||||
|
||||
class HMRollershutter(homematic.HMDevice, RollershutterDevice):
|
||||
"""Represents a Homematic Rollershutter in Home Assistant."""
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""
|
||||
Return current position of rollershutter.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
if self.available:
|
||||
return int((1 - self._hm_get_state()) * 100)
|
||||
return None
|
||||
|
||||
def position(self, **kwargs):
|
||||
"""Move to a defined position: 0 (closed) and 100 (open)."""
|
||||
if self.available:
|
||||
if ATTR_CURRENT_POSITION in kwargs:
|
||||
position = float(kwargs[ATTR_CURRENT_POSITION])
|
||||
position = min(100, max(0, position))
|
||||
level = (100 - position) / 100.0
|
||||
self._hmdevice.set_level(level, self._channel)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the rollershutter."""
|
||||
current = self.current_position
|
||||
if current is None:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
return STATE_CLOSED if current == 100 else STATE_OPEN
|
||||
|
||||
def move_up(self, **kwargs):
|
||||
"""Move the rollershutter up."""
|
||||
if self.available:
|
||||
self._hmdevice.move_up(self._channel)
|
||||
|
||||
def move_down(self, **kwargs):
|
||||
"""Move the rollershutter down."""
|
||||
if self.available:
|
||||
self._hmdevice.move_down(self._channel)
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the device if in motion."""
|
||||
if self.available:
|
||||
self._hmdevice.stop(self._channel)
|
||||
|
||||
def _check_hm_to_ha_object(self):
|
||||
"""Check if possible to use the HM Object as this HA type."""
|
||||
from pyhomematic.devicetypes.actors import Blind
|
||||
|
||||
# Check compatibility from HMDevice
|
||||
if not super()._check_hm_to_ha_object():
|
||||
return False
|
||||
|
||||
# Check if the homematic device is correct for this HA device
|
||||
if isinstance(self._hmdevice, Blind):
|
||||
return True
|
||||
|
||||
_LOGGER.critical("This %s can't be use as rollershutter!", self._name)
|
||||
return False
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from hm metadata."""
|
||||
super()._init_data_struct()
|
||||
|
||||
# Add state to data dict
|
||||
self._state = "LEVEL"
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
@@ -7,9 +7,10 @@ https://home-assistant.io/components/rollershutter.wink/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.rollershutter import RollershutterDevice
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
REQUIREMENTS = ['python-wink==0.7.7']
|
||||
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
pywink.get_shades())
|
||||
|
||||
|
||||
class WinkRollershutterDevice(RollershutterDevice):
|
||||
class WinkRollershutterDevice(WinkDevice, RollershutterDevice):
|
||||
"""Representation of a Wink rollershutter (shades)."""
|
||||
|
||||
def __init__(self, wink):
|
||||
"""Initialize the rollershutter."""
|
||||
self.wink = wink
|
||||
self._battery = None
|
||||
WinkDevice.__init__(self, wink)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Wink Shades don't track their position."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this wink rollershutter."""
|
||||
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the rollershutter if any."""
|
||||
return self.wink.name()
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the rollershutter."""
|
||||
return self.wink.update_state()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""True if connection == True."""
|
||||
return self.wink.available
|
||||
|
||||
def move_down(self):
|
||||
"""Close the shade."""
|
||||
self.wink.set_state(0)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Support for Zwave roller shutter components.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/rollershutter.zwave/
|
||||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.rollershutter import DOMAIN
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.rollershutter import RollershutterDevice
|
||||
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
|
||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave roller shutters."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
||||
|
||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||
return
|
||||
if value.index != 0:
|
||||
return
|
||||
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
|
||||
|
||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
||||
"""Representation of an Zwave roller shutter."""
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._node = value.node
|
||||
dispatcher.connect(
|
||||
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state(True)
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""Return the current position of Zwave roller shutter."""
|
||||
return self._value.data
|
||||
|
||||
def move_up(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
self._node.set_dimmer(self._value.value_id, 100)
|
||||
|
||||
def move_down(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
self._node.set_dimmer(self._value.value_id, 0)
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
for value in self._node.get_values(
|
||||
class_id=COMMAND_CLASS_SWITCH_BINARY).values():
|
||||
# Rollershutter will toggle between UP (True), DOWN (False).
|
||||
# It also stops the shutter if the same value is sent while moving.
|
||||
value.data = value.data
|
||||
break
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user