├── .tool-versions ├── requirements.txt ├── smart_hass ├── smart_hass.py ├── __init__.py ├── cli_utils.py ├── templates │ ├── home_assistant_configuration.j2 │ └── bruh_mqtt_multisensor.j2 ├── multisensor.py ├── cli.py └── bayes.py ├── tests ├── __init__.py ├── conftest.py ├── fixtures.py ├── test_cli.py ├── test_cli_utils.py └── test_bayes.py ├── assets └── wiring_diagram_v2.png ├── HISTORY.md ├── dev_requirements.txt ├── AUTHORS.md ├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── MANIFEST.in ├── setup.cfg ├── .gitignore ├── .travis.yml ├── LICENSE ├── setup.py ├── README.md ├── CONTRIBUTING.md ├── travis_pypi_setup.py └── examples └── Smass Examples.ipynb /.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.7.2 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML>=4.2b1 2 | click==6.7 3 | Jinja2>=2.10.1 4 | -------------------------------------------------------------------------------- /smart_hass/smart_hass.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Main module.""" 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for smart_hass.""" 4 | -------------------------------------------------------------------------------- /assets/wiring_diagram_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlmcgehee21/smart_hass/HEAD/assets/wiring_diagram_v2.png -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | 0.1.0 (2017-10-12) 5 | ------------------ 6 | 7 | - First release on PyPI. 8 | 9 | -------------------------------------------------------------------------------- /smart_hass/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for smart_hass.""" 4 | 5 | __author__ = """Jeff McGehee""" 6 | __email__ = 'jlmcgehee21@gmail.com' 7 | __version__ = '0.1.0' 8 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | pip==9.0.1 4 | wheel==0.29.0 5 | watchdog==0.8.3 6 | flake8==2.6.0 7 | coverage==4.1 8 | cryptography==1.7 9 | pytest 10 | pytest-runner 11 | pytest-watch 12 | -------------------------------------------------------------------------------- /smart_hass/cli_utils.py: -------------------------------------------------------------------------------- 1 | 2 | def load_number_arg(number_arg): 3 | if number_arg is not None: 4 | if number_arg.is_integer(): 5 | number_arg = int(number_arg) 6 | 7 | return number_arg 8 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pytest 4 | 5 | TEST_DIR = os.path.dirname(os.path.realpath(__file__)) 6 | code_dir = os.path.join(TEST_DIR, '../smart_hass/') 7 | 8 | sys.path.append(code_dir) 9 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | Development Lead 5 | ---------------- 6 | 7 | - Jeff McGehee <> 8 | 9 | Contributors 10 | ------------ 11 | 12 | None yet. Why not be the first? 13 | -------------------------------------------------------------------------------- /tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import pytest 4 | 5 | from facial_recognition import helpers 6 | from tests.conftest import TEST_DIR 7 | 8 | @pytest.fixture() 9 | def image_directory(): 10 | return os.path.join(TEST_DIR, 'test_images') 11 | 12 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | import pytest 3 | 4 | from smart_hass import cli 5 | 6 | def test_bayes_help(): 7 | """Test the CLI.""" 8 | runner = CliRunner() 9 | 10 | help_result = runner.invoke(cli.bayes, ['--help']) 11 | assert help_result.exit_code == 0 12 | assert 'Show this message and exit.' in help_result.output 13 | 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * smart_hass version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.md 2 | include CONTRIBUTING.md 3 | include HISTORY.md 4 | include LICENSE 5 | include README.md 6 | include requirements.txt 7 | include dev_requirements.txt 8 | 9 | recursive-include tests * 10 | recursive-include smart_hass/templates * 11 | recursive-exclude * __pycache__ 12 | recursive-exclude * *.py[co] 13 | 14 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 15 | -------------------------------------------------------------------------------- /tests/test_cli_utils.py: -------------------------------------------------------------------------------- 1 | from smart_hass import cli_utils 2 | 3 | def test_load_number_arg_none(): 4 | result = cli_utils.load_number_arg(None) 5 | assert result == None 6 | 7 | def test_load_number_arg_int(): 8 | result = cli_utils.load_number_arg(1.0) 9 | assert result == 1 10 | assert type(result) == int 11 | 12 | def test_load_number_arg_float(): 13 | result = cli_utils.load_number_arg(1.1) 14 | assert result == 1.1 15 | assert type(result) == float 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:smart_hass/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bdist_wheel] 15 | universal = 1 16 | 17 | [flake8] 18 | exclude = docs 19 | 20 | [aliases] 21 | test = pytest 22 | # Define setup.py command aliases here 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # pyenv python configuration file 62 | .python-version 63 | multisensor/ 64 | -------------------------------------------------------------------------------- /smart_hass/templates/home_assistant_configuration.j2: -------------------------------------------------------------------------------- 1 | light: 2 | - platform: mqtt_json 3 | name: "{{ sensor_name }} LED" 4 | state_topic: "bruh/{{ sensor_name }}" 5 | command_topic: "bruh/{{ sensor_name}}/set" 6 | brightness: true 7 | flash: true 8 | rgb: true 9 | optimistic: false 10 | qos: 0 11 | 12 | 13 | sensor: 14 | - platform: mqtt 15 | state_topic: "bruh/{{ sensor_name}}" 16 | name: "{{ sensor_name }} Humidity" 17 | unit_of_measurement: "%" 18 | {% raw %} value_template: '{{ value_json.humidity | round(1) }}' {% endraw %} 19 | 20 | - platform: mqtt 21 | state_topic: "bruh/{{ sensor_name}}" 22 | name: "{{ sensor_name }} Light Level" 23 | ##This sensor is not calibrated to actual LUX. Rather, this a map of the input voltage ranging from 0 - 1023. 24 | unit_of_measurement: "LUX" 25 | {% raw %} value_template: '{{ value_json.ldr }}' {% endraw %} 26 | 27 | - platform: mqtt 28 | state_topic: "bruh/{{ sensor_name}}" 29 | name: "{{ sensor_name }} Motion" 30 | {% raw %} value_template: '{{ value_json.motion }}' {% endraw %} 31 | 32 | - platform: mqtt 33 | state_topic: "bruh/{{ sensor_name}}" 34 | name: "{{ sensor_name }} Temperature" 35 | unit_of_measurement: "°F" 36 | {% raw %} value_template: '{{ value_json.temperature | round(1) }}' {% endraw %} 37 | -------------------------------------------------------------------------------- /smart_hass/multisensor.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import jinja2 4 | 5 | THIS_DIR = os.path.dirname(os.path.abspath(__file__)) 6 | TEMPLATE_DIR = os.path.join(os.path.abspath(THIS_DIR), 'templates') 7 | 8 | def render_sketch_file(sensor_name): 9 | wifi_ssid = os.environ.get('WIFI_SSID') 10 | wifi_pwd = os.environ.get('WIFI_PWD') 11 | 12 | mqtt_server = os.environ.get('MQTT_SERVER') 13 | mqtt_user = os.environ.get('MQTT_USER') 14 | mqtt_pwd = os.environ.get('MQTT_PWD') 15 | mqtt_port = os.environ.get('MQTT_PORT') 16 | 17 | ota_pwd = os.environ.get('OTA_PWD') 18 | 19 | j2_env = jinja2.Environment( 20 | loader=jinja2.FileSystemLoader(TEMPLATE_DIR), trim_blocks=True) 21 | template = j2_env.get_template('bruh_mqtt_multisensor.j2') 22 | 23 | output_str = template.render( 24 | wifi_ssid=wifi_ssid, 25 | wifi_pwd=wifi_pwd, 26 | mqtt_server=mqtt_server, 27 | mqtt_user=mqtt_user, 28 | mqtt_pwd=mqtt_pwd, 29 | mqtt_port=mqtt_port, 30 | sensor_name=sensor_name, 31 | ota_pwd=ota_pwd) 32 | 33 | return output_str 34 | 35 | 36 | def render_hass_config(sensor_name): 37 | j2_env = jinja2.Environment( 38 | loader=jinja2.FileSystemLoader(TEMPLATE_DIR), trim_blocks=True) 39 | template = j2_env.get_template('home_assistant_configuration.j2') 40 | 41 | output_str = template.render(sensor_name=sensor_name) 42 | 43 | return output_str 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated and will overwrite each time you run travis_pypi_setup.py 2 | deploy: 3 | provider: pypi 4 | distributions: sdist bdist_wheel 5 | user: jlmcgehee21 6 | password: 7 | secure: !!binary | 8 | cjZDV3Z4Wjg2bVkwRW1FQ2JON1oyV3F0Ry9Gd3VIVXJvWTMrVGUrSlR6Z3dJeENCQUtBRkluMms0 9 | K25xY3h0RFJEM0pkSjExTVRibXR1c21aVlRZMGtUb01mdXByZ2NnNnZUVmFCbXpycy9yckdDY09F 10 | OUN6U29icVovVE5HUjlJYzl4UXdxWjR3bmt4SmZtODN6SUZGMm5NVkRubDJZSFJsalRHcG9CYS9R 11 | NlZLck00aGJRTXRZTU90T09oTW9tdk9lWDFScGNiKzlKbk9vU3FBbTFMWVpzUXpFalpJbHVHbG9s 12 | MUdkR0xDVTdCWm9RZU5sNHBkK0RZaXpFeG1rMEhWc25qcWVVcDFWeTBORVNpeWNtYU9Ra3V4ck9T 13 | cmR4SzVqV0swWmZIUWRnaldwSFRoR05jeExPRW5mNmgremYyQ1o2WnNxcExrdjdGN0N6djJYNVVF 14 | SHhncXhDb0xGSzNRTUxnY3hVM1AwdWF0Zi85Z3MwNnFjeDJBdWJkR0FUM2lkNmx4bnZIVVZsRTF1 15 | NXYxbEViVnR2SG1nMjVZSnlOVVN5Zy9Eb2xtVFp2K1Mwanorc3M2dDlzbWh2MGtxMGtvSm10Ykx5 16 | ek5YYnlwUnlQUEFtdzJDcEh3RDVxOE9TeDh0YnVTZmUrblRvZnAyMzlyUUJoMy9qb3VvMmJMdE9H 17 | M21HaUppbHMzSkxxT3l4N2ZicEx0VmFETXlaTCt1dzRCcjZGSUs1TWpLdk41U0ZHR0RUOEY1SmVY 18 | aHFQQk1ueW5lVnVHcWlhb0V0aWJlb3dEbC93NFpxNHA4YW9Ddk1YUEhuVEpxTDh0ZkNGYWNOUlJD 19 | NFIzWmU0NWtROGNxKzE3R2ltOUdjSVdpTDB0NTBTYU9GLzc3bDhjeVJPNlRaN0R3SlVjWTdtcjg9 20 | true: 21 | branch: master 22 | 23 | dist: xenial 24 | install: pip install -r dev_requirements.txt 25 | language: python 26 | python: 27 | - "3.7" 28 | - "3.6" 29 | - "3.5" 30 | script: pytest 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | GNU GENERAL PUBLIC LICENSE 3 | Version 3, 29 June 2007 4 | 5 | Tools I find useful in my interactions with Home Assistant. 6 | Copyright (C) 2017 Jeff McGehee 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | Also add information on how to contact you by electronic and paper mail. 22 | 23 | You should also get your employer (if you work as a programmer) or school, 24 | if any, to sign a "copyright disclaimer" for the program, if necessary. 25 | For more information on this, and how to apply and follow the GNU GPL, see 26 | . 27 | 28 | The GNU General Public License does not permit incorporating your program 29 | into proprietary programs. If your program is a subroutine library, you 30 | may consider it more useful to permit linking proprietary applications with 31 | the library. If this is what you want to do, use the GNU Lesser General 32 | Public License instead of this License. But first, please read 33 | . 34 | 35 | 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | from setuptools import setup, find_packages 7 | 8 | with open('README.md') as readme_file: 9 | readme = readme_file.read() 10 | 11 | with open('HISTORY.md') as history_file: 12 | history = history_file.read() 13 | 14 | requirements = [ 15 | 'Click>=6.0', 16 | 'PyYAML>=3.0', 17 | 'Jinja2>=2.9.0', 18 | ] 19 | 20 | setup_requirements = [ 21 | 'pytest-runner', 22 | ] 23 | 24 | test_requirements = [ 25 | 'pytest', 26 | ] 27 | 28 | setup( 29 | name='smart_hass', 30 | version='0.2.1', 31 | description="Tools I find useful in my interactions with Home Assistant.", 32 | long_description=readme + '\n\n' + history, 33 | author="Jeff McGehee", 34 | author_email='jlmcgehee21@gmail.com', 35 | url='https://github.com/jlmcgehee21/smart_hass', 36 | packages=find_packages(include=['smart_hass']), 37 | entry_points={ 38 | 'console_scripts': [ 39 | 'smass=smart_hass.cli:cli' 40 | ] 41 | }, 42 | include_package_data=True, 43 | install_requires=requirements, 44 | license="GNU General Public License v3", 45 | zip_safe=False, 46 | keywords='smart_hass', 47 | classifiers=[ 48 | 'Development Status :: 2 - Pre-Alpha', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 51 | 'Natural Language :: English', 52 | 'Programming Language :: Python :: 3', 53 | 'Programming Language :: Python :: 3.5', 54 | 'Programming Language :: Python :: 3.6', 55 | ], 56 | test_suite='tests', 57 | tests_require=test_requirements, 58 | setup_requires=setup_requirements, 59 | ) 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smart\_hass 2 | 3 | [![image](https://img.shields.io/pypi/v/smart_hass.svg)](https://pypi.python.org/pypi/smart_hass) 4 | 5 | [![image](https://img.shields.io/travis/jlmcgehee21/smart_hass.svg)](https://travis-ci.org/jlmcgehee21/smart_hass) 6 | 7 | Tools I find useful in my interactions with Home Assistant. 8 | 9 | - Free software: GNU General Public License v3 10 | 11 | ## Installation 12 | ``` 13 | $ pip install smart_hass 14 | ``` 15 | 16 | ## Usage 17 | 18 | This is a command line tool meant to work in a Unix shell. 19 | 20 | If you don't know what to do, try: 21 | 22 | ``` 23 | $ smass --help 24 | ``` 25 | 26 | ### Binary Bayes introspection 27 | Determine which combinations of observations can cause your Bayesian Binary 28 | sensor to be True/False. 29 | 30 | Latest functionality can be found via: 31 | 32 | ``` 33 | $ smass bayes --help 34 | ``` 35 | 36 | Pipe valid YAML from a Bayesian Binary config: 37 | 38 | ``` 39 | $ pbpaste | smass bayes 40 | ``` 41 | 42 | Identify and analyze Bayesian Binary sensors in a config file: 43 | 44 | ``` 45 | $ smass bayes --conf ~/hass_config/binary_sensors.yaml 46 | ``` 47 | 48 | List all cases where a Bayesian Binary sensor evaluates to True with an 49 | observation of `on` for `binary_sensor.bedroom_motion` 50 | 51 | ``` 52 | $ pbpaste | smass bayes -te binary_sensor.bedroom_modtion -ts on | json_pp 53 | ``` 54 | 55 | ### Multisensor 56 | 57 | Generate an Arduino sketch for an 58 | [ESP-MQTT-JSON-Multisensor](https://github.com/bruhautomation/ESP-MQTT-JSON-Multisensor) 59 | via: 60 | 61 | ``` 62 | $ smass multisensor --name kitchen 63 | ``` 64 | 65 | Yields: `./multisensor/multisensor.ino`, which can then be flashed to a Node MCU 66 | via the Arduino IDE. 67 | 68 | In order for this to function properly, you should set the following environment 69 | variables to use for your multisensor. 70 | * WIFI_SSID 71 | * WIFI_PWD 72 | * MQTT_SERVER 73 | * MQTT_USER 74 | * MQTT_PWD 75 | * MQTT_PORT 76 | * OTA_PWD 77 | 78 | #### Wiring Diagram for Multi Sensor 79 | ![MultiSensor Wiring Diagram](assets/wiring_diagram_v2.png) 80 | 81 | 82 | ## Credits 83 | 84 | This package was created with 85 | [Cookiecutter](https://github.com/audreyr/cookiecutter) and the 86 | [audreyr/cookiecutter-pypackage](https://github.com/audreyr/cookiecutter-pypackage) 87 | project template. 88 | 89 | The multisensor is derived from 90 | [ESP-MQTT-JSON-Multisensor](https://github.com/bruhautomation/ESP-MQTT-JSON-Multisensor). 91 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Contributions are welcome, and they are greatly appreciated! Every 5 | little bit helps, and credit will always be given. 6 | 7 | You can contribute in many ways: 8 | 9 | Types of Contributions 10 | ---------------------- 11 | 12 | ### Report Bugs 13 | 14 | Report bugs at . 15 | 16 | If you are reporting a bug, please include: 17 | 18 | - Your operating system name and version. 19 | - Any details about your local setup that might be helpful in 20 | troubleshooting. 21 | - Detailed steps to reproduce the bug. 22 | 23 | ### Fix Bugs 24 | 25 | Look through the GitHub issues for bugs. Anything tagged with "bug" and 26 | "help wanted" is open to whoever wants to implement it. 27 | 28 | ### Implement Features 29 | 30 | Look through the GitHub issues for features. Anything tagged with 31 | "enhancement" and "help wanted" is open to whoever wants to implement 32 | it. 33 | 34 | ### Write Documentation 35 | 36 | smart\_hass could always use more documentation, whether as part of the 37 | official smart\_hass docs, in docstrings, or even on the web in blog 38 | posts, articles, and such. 39 | 40 | ### Submit Feedback 41 | 42 | The best way to send feedback is to file an issue at 43 | . 44 | 45 | If you are proposing a feature: 46 | 47 | - Explain in detail how it would work. 48 | - Keep the scope as narrow as possible, to make it easier to 49 | implement. 50 | - Remember that this is a volunteer-driven project, and that 51 | contributions are welcome :) 52 | 53 | Get Started! 54 | ------------ 55 | 56 | Ready to contribute? Here's how to set up smart\_hass for local 57 | development. 58 | 59 | 1. Fork the smart\_hass repo on GitHub. 60 | 2. Clone your fork locally: 61 | 62 | $ git clone git@github.com:your_name_here/smart_hass.git 63 | 64 | 3. Install your local copy into a virtualenv. Assuming you have 65 | virtualenvwrapper installed, this is how you set up your fork for 66 | local development: 67 | 68 | $ mkvirtualenv smart_hass 69 | $ cd smart_hass/ 70 | $ python setup.py develop 71 | 72 | 4. Create a branch for local development: 73 | 74 | $ git checkout -b name-of-your-bugfix-or-feature 75 | 76 | Now you can make your changes locally. 77 | 78 | 5. When you're done making changes, check that your changes pass flake8 79 | and the tests, including testing other Python versions with tox: 80 | 81 | $ flake8 smart_hass tests 82 | $ python setup.py test or py.test 83 | $ tox 84 | 85 | To get flake8 and tox, just pip install them into your virtualenv. 86 | 87 | 6. Commit your changes and push your branch to GitHub: 88 | 89 | $ git add . 90 | $ git commit -m "Your detailed description of your changes." 91 | $ git push origin name-of-your-bugfix-or-feature 92 | 93 | 7. Submit a pull request through the GitHub website. 94 | 95 | Pull Request Guidelines 96 | ----------------------- 97 | 98 | Before you submit a pull request, check that it meets these guidelines: 99 | 100 | 1. The pull request should include tests. 101 | 2. If the pull request adds functionality, the docs should be updated. 102 | Put your new functionality into a function with a docstring, and add 103 | the feature to the list in README.rst. 104 | 3. The pull request should work for Python 2.6, 2.7, 3.3, 3.4 and 3.5, 105 | and for PyPy. Check 106 | and 107 | make sure that the tests pass for all supported Python versions. 108 | 109 | Tips 110 | ---- 111 | 112 | To run a subset of tests: 113 | 114 | $ py.test tests.test_smart_hass 115 | -------------------------------------------------------------------------------- /smart_hass/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Console script for smart_hass.""" 3 | 4 | 5 | from .bayes import BayesProcessor 6 | from .multisensor import render_hass_config, render_sketch_file 7 | from . import cli_utils 8 | 9 | import click 10 | import json 11 | import os 12 | import textwrap 13 | import shutil 14 | import sys 15 | import yaml 16 | 17 | 18 | @click.group() 19 | def cli(args=None): 20 | """Console script for smart_hass.""" 21 | pass 22 | 23 | @cli.command() 24 | @click.option('--conf', '-c', default=None, 25 | help='File to parse for Bayesian Binary sensors.') 26 | @click.option('--true/--false', '-t/-f', default=True, 27 | help=''.join(['Output combinations that evaluate to True/False', 28 | ', respectively'])) 29 | @click.option('--sensor-ind', '-si', default=None, type=int, 30 | help=''.join(['Index of Bayesian Binary sensor if multiple', 31 | ' sensors are present'])) 32 | @click.option('--target-entity', '-te', default=None, 33 | help=''.join(['Only return observation combinations which', 34 | ' include this entity'])) 35 | @click.option('--to_state', '-ts', default=None, 36 | help=''.join(['Combine with --target-entity to restrict', 37 | ' observations which include both'])) 38 | @click.option('--above', '-a', default=None, type=float, 39 | help=''.join(['Combine with --target-entity to restrict', 40 | ' observations which include both'])) 41 | @click.option('--below', '-b', default=None, type=float, 42 | help=''.join(['Combine with --target-entity to restrict', 43 | ' observations which include both'])) 44 | @click.option('--summarize', '-s', is_flag=True, 45 | help='Output results as a summary') 46 | def bayes(conf, true, sensor_ind, target_entity, to_state, 47 | above, below, summarize): 48 | 49 | if conf is not None: 50 | with open(conf, 'r') as conf_file: 51 | parsed_yaml = yaml.load(conf_file.read()) 52 | 53 | elif not click.get_text_stream('stdin').isatty: 54 | message = '''You must pipe valid YAML to STDIN or load a YAML file via 55 | the --conf option''' 56 | 57 | click.echo(textwrap.fill(message)) 58 | sys.exit(1) 59 | 60 | else: 61 | stdin_text = click.get_text_stream('stdin').read() 62 | parsed_yaml = yaml.load(stdin_text) 63 | 64 | above = cli_utils.load_number_arg(above) 65 | below = cli_utils.load_number_arg(below) 66 | 67 | target_entity = { 68 | 'entity_id': target_entity, 69 | 'to_state': to_state, 70 | 'above': above, 71 | 'below': below 72 | } 73 | target_entity = dict( 74 | (k, v) for k, v in target_entity.items() if v is not None) 75 | 76 | 77 | processor = BayesProcessor(parsed_yaml, true, sensor_ind, 78 | target_entity) 79 | 80 | processor.proc_sensors(summarize) 81 | 82 | click.echo(json.dumps(processor.summaries)) 83 | 84 | 85 | @cli.command() 86 | @click.option('--name', '-n', default=None, 87 | help='Name of sensor.') 88 | def multisensor(name): 89 | sketch_str = render_sketch_file(name) 90 | 91 | if os.path.exists('multisensor'): 92 | shutil.rmtree('multisensor') 93 | 94 | os.mkdir('multisensor') 95 | 96 | ino_out = os.path.join('multisensor', 'multisensor' + '.ino') 97 | with open(ino_out, 'w') as sketch_file: 98 | sketch_file.write(sketch_str) 99 | 100 | hass_str = render_hass_config(name) 101 | click.echo('#########################################') 102 | click.echo('## Copy Below and Place in Hass Config ##') 103 | click.echo('#########################################') 104 | click.echo(hass_str) 105 | 106 | if __name__ == "__main__": 107 | cli() 108 | -------------------------------------------------------------------------------- /travis_pypi_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """Update encrypted deploy password in Travis config file.""" 4 | 5 | 6 | from __future__ import print_function 7 | import base64 8 | import json 9 | import os 10 | from getpass import getpass 11 | import yaml 12 | from cryptography.hazmat.primitives.serialization import load_pem_public_key 13 | from cryptography.hazmat.backends import default_backend 14 | from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 15 | 16 | 17 | try: 18 | from urllib import urlopen 19 | except ImportError: 20 | from urllib.request import urlopen 21 | 22 | 23 | GITHUB_REPO = 'jlmcgehee21/smart_hass' 24 | TRAVIS_CONFIG_FILE = os.path.join( 25 | os.path.dirname(os.path.abspath(__file__)), '.travis.yml') 26 | 27 | 28 | def load_key(pubkey): 29 | """Load public RSA key. 30 | 31 | Work around keys with incorrect header/footer format. 32 | 33 | Read more about RSA encryption with cryptography: 34 | https://cryptography.io/latest/hazmat/primitives/asymmetric/rsa/ 35 | """ 36 | try: 37 | return load_pem_public_key(pubkey.encode(), default_backend()) 38 | except ValueError: 39 | # workaround for https://github.com/travis-ci/travis-api/issues/196 40 | pubkey = pubkey.replace('BEGIN RSA', 'BEGIN').replace('END RSA', 'END') 41 | return load_pem_public_key(pubkey.encode(), default_backend()) 42 | 43 | 44 | def encrypt(pubkey, password): 45 | """Encrypt password using given RSA public key and encode it with base64. 46 | 47 | The encrypted password can only be decrypted by someone with the 48 | private key (in this case, only Travis). 49 | """ 50 | key = load_key(pubkey) 51 | encrypted_password = key.encrypt(password, PKCS1v15()) 52 | return base64.b64encode(encrypted_password) 53 | 54 | 55 | def fetch_public_key(repo): 56 | """Download RSA public key Travis will use for this repo. 57 | 58 | Travis API docs: http://docs.travis-ci.com/api/#repository-keys 59 | """ 60 | keyurl = 'https://api.travis-ci.org/repos/{0}/key'.format(repo) 61 | data = json.loads(urlopen(keyurl).read().decode()) 62 | if 'key' not in data: 63 | errmsg = "Could not find public key for repo: {}.\n".format(repo) 64 | errmsg += "Have you already added your GitHub repo to Travis?" 65 | raise ValueError(errmsg) 66 | return data['key'] 67 | 68 | 69 | def prepend_line(filepath, line): 70 | """Rewrite a file adding a line to its beginning.""" 71 | with open(filepath) as f: 72 | lines = f.readlines() 73 | 74 | lines.insert(0, line) 75 | 76 | with open(filepath, 'w') as f: 77 | f.writelines(lines) 78 | 79 | 80 | def load_yaml_config(filepath): 81 | """Load yaml config file at the given path.""" 82 | with open(filepath) as f: 83 | return yaml.load(f) 84 | 85 | 86 | def save_yaml_config(filepath, config): 87 | """Save yaml config file at the given path.""" 88 | with open(filepath, 'w') as f: 89 | yaml.dump(config, f, default_flow_style=False) 90 | 91 | 92 | def update_travis_deploy_password(encrypted_password): 93 | """Put `encrypted_password` into the deploy section of .travis.yml.""" 94 | config = load_yaml_config(TRAVIS_CONFIG_FILE) 95 | 96 | config['deploy']['password'] = dict(secure=encrypted_password) 97 | 98 | save_yaml_config(TRAVIS_CONFIG_FILE, config) 99 | 100 | line = ('# This file was autogenerated and will overwrite' 101 | ' each time you run travis_pypi_setup.py\n') 102 | prepend_line(TRAVIS_CONFIG_FILE, line) 103 | 104 | 105 | def main(args): 106 | """Add a PyPI password to .travis.yml so that Travis can deploy to PyPI. 107 | 108 | Fetch the Travis public key for the repo, and encrypt the PyPI password 109 | with it before adding, so that only Travis can decrypt and use the PyPI 110 | password. 111 | """ 112 | public_key = fetch_public_key(args.repo) 113 | password = args.password or getpass('PyPI password: ') 114 | update_travis_deploy_password(encrypt(public_key, password.encode())) 115 | print("Wrote encrypted password to .travis.yml -- you're ready to deploy") 116 | 117 | 118 | if '__main__' == __name__: 119 | import argparse 120 | parser = argparse.ArgumentParser(description=__doc__) 121 | parser.add_argument('--repo', default=GITHUB_REPO, 122 | help='GitHub repo (default: %s)' % GITHUB_REPO) 123 | parser.add_argument('--password', 124 | help='PyPI password (will prompt if not provided)') 125 | 126 | args = parser.parse_args() 127 | main(args) 128 | -------------------------------------------------------------------------------- /smart_hass/bayes.py: -------------------------------------------------------------------------------- 1 | import click 2 | import itertools 3 | import json 4 | from functools import reduce 5 | import operator 6 | import select 7 | import sys 8 | import textwrap 9 | import yaml 10 | 11 | 12 | OPERATORS = {True: operator.gt, False: operator.le} 13 | 14 | class BayesProcessor(): 15 | def __init__(self, parsed_yaml, true_false, sensor_ind, target_entity): 16 | self.sensors = self.sensors_from_parsed_yaml(parsed_yaml, sensor_ind) 17 | self.compare_func = OPERATORS[true_false] 18 | self.summaries = [] 19 | self.target_entity = target_entity 20 | 21 | def sensors_from_parsed_yaml(self, parsed_yaml, sensor_ind): 22 | yaml_parsers = {list: self.process_multiple, 23 | dict: self.process_singular} 24 | 25 | bayesian_sensors = yaml_parsers.get(type(parsed_yaml), 26 | lambda x: [])(parsed_yaml) 27 | 28 | if sensor_ind is not None: 29 | sensors = [bayesian_sensors[sensor_ind]] 30 | else: 31 | sensors = bayesian_sensors 32 | 33 | return sensors 34 | 35 | @staticmethod 36 | def process_multiple(yaml): 37 | return [item for item in yaml if item['platform'] == 'bayesian'] 38 | 39 | @staticmethod 40 | def process_singular(yaml): 41 | return [item for item in [yaml] if item['platform'] == 'bayesian'] 42 | 43 | def proc_sensors(self, summarize): 44 | for sensor_dict in self.sensors: 45 | self.summaries.append({ 46 | 'name': sensor_dict['name'], 47 | 'total_cases': 0, 48 | 'total_matching': 0 49 | }) 50 | 51 | observations = sensor_dict['observations'] 52 | combos = self.generate_sensor_combinations(observations) 53 | target_combos = self.filter_target(combos, self.target_entity) 54 | 55 | true_conds = (comb for comb in target_combos 56 | if self.process_all_combos(sensor_dict, comb)) 57 | 58 | matching_cases = [ 59 | list(self.render_condition(i) for i in list(cond)) 60 | for cond in true_conds 61 | ] 62 | 63 | if not summarize: 64 | self.summaries[-1]['matching_cases'] = matching_cases 65 | 66 | @staticmethod 67 | def generate_sensor_combinations(observation_list): 68 | comb_gen = (itertools.combinations(observation_list, i + 1) 69 | for i in range(len(observation_list) - 1)) 70 | 71 | all_combs = itertools.chain(*comb_gen) 72 | 73 | # Is not possible to observe the same entity in multiple states. 74 | distinct_combs = (sorted(list({comb['entity_id']: comb 75 | for comb in combs}.values()), 76 | key=lambda k: k['entity_id']) 77 | for combs in all_combs) 78 | 79 | distinct_combs = (list(x) 80 | for x in set(tuple(tuple(y.items()) for y in x) 81 | for x in distinct_combs)) 82 | return (list(dict(obs) for obs in comb) for comb in distinct_combs) 83 | 84 | @staticmethod 85 | def filter_target(combos, target): 86 | if target is not None: 87 | combos = (comb for comb in combos 88 | if any(all(item in obs.items() 89 | for item in target.items()) 90 | for obs in comb)) 91 | 92 | return combos 93 | 94 | def process_all_combos(self, sensor, distinct_combs): 95 | prob = reduce(self.__process_combo, distinct_combs, sensor['prior']) 96 | 97 | result = self.compare_func( 98 | round(prob, 2), sensor['probability_threshold']) 99 | 100 | if result: 101 | self.summaries[-1]['total_matching'] += 1 102 | 103 | return result 104 | 105 | def __process_combo(self, prior, comb): 106 | prob_true = comb['prob_given_true'] 107 | prob_false = comb.get('prob_given_false', 1 - prob_true) 108 | 109 | self.summaries[-1]['total_cases'] += 1 110 | return self.update_probability(prior, prob_true, prob_false) 111 | 112 | @staticmethod 113 | def render_condition(cond): 114 | cond = cond.copy() 115 | cond.pop('prob_given_true', None) 116 | cond.pop('prob_given_false', None) 117 | cond.pop('platform', None) 118 | 119 | return cond 120 | 121 | @staticmethod 122 | def update_probability(prior, prob_true, prob_false): 123 | """Update probability using Bayes' rule.""" 124 | numerator = prob_true * prior 125 | denominator = numerator + prob_false * (1 - prior) 126 | 127 | probability = numerator / denominator 128 | return probability 129 | 130 | 131 | if __name__ == '__main__': 132 | main() 133 | -------------------------------------------------------------------------------- /tests/test_bayes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Tests for `bayes` command line tool.""" 5 | 6 | import pytest 7 | 8 | from smart_hass import bayes 9 | 10 | 11 | @pytest.fixture 12 | def static_bayes(): 13 | """Sample pytest fixture. 14 | 15 | See more at: http://doc.pytest.org/en/latest/fixture.html 16 | """ 17 | proc = bayes.BayesProcessor({'platform': 'bayesian'}, 18 | True, None, None) 19 | return proc 20 | 21 | def test_proc_render_condition(static_bayes): 22 | orig_cond = {'foo': 'bar', 'prob_given_true': 0.5} 23 | rendered_cond = static_bayes.render_condition(orig_cond) 24 | 25 | assert orig_cond == {'foo': 'bar', 'prob_given_true': 0.5} 26 | assert rendered_cond == {'foo': 'bar'} 27 | 28 | def test_proc_proc_multiple(static_bayes): 29 | multi_yaml = [{'platform': 'foo'}, 30 | {'platform': 'bayesian'}, 31 | {'platform': 'bar'}] 32 | 33 | result = static_bayes.process_multiple(multi_yaml) 34 | assert result == [multi_yaml[1]] 35 | 36 | def test_proc_proc_singular(static_bayes): 37 | result = static_bayes.process_singular({'platform': 'bayesian'}) 38 | assert result == [{'platform': 'bayesian'}] 39 | 40 | def test_proc_generate_sensor_combinations(static_bayes): 41 | observations = [{'entity_id': 'foo', 'below': 1}, 42 | {'entity_id': 'bar'}, 43 | {'entity_id': 'foo', 'above': 3}, 44 | {'entity_id': 'baz'}] 45 | 46 | result = list(static_bayes.generate_sensor_combinations(observations)) 47 | 48 | target_result = [[{'entity_id': 'foo', 'below': 1}, {'entity_id': 'bar'}, 49 | {'entity_id': 'baz'}], 50 | [{'entity_id': 'bar'}, {'entity_id': 'baz'}], 51 | [{'entity_id': 'foo', 'above': 3}, {'entity_id': 'baz'}], 52 | [{'entity_id': 'bar'}], 53 | [{'entity_id': 'foo', 'above': 3}, {'entity_id': 'bar'}], 54 | [{'entity_id': 'baz'}], 55 | [{'entity_id': 'foo', 'above': 3}], 56 | [{'entity_id': 'foo', 'below': 1}, {'entity_id': 'bar'}], 57 | [{'entity_id': 'bar'}, {'entity_id': 'foo', 'above': 3}, 58 | {'entity_id': 'baz'}], 59 | [{'entity_id': 'foo', 'below': 1}], 60 | [{'entity_id': 'foo', 'below': 1}, {'entity_id': 'baz'}]] 61 | 62 | target_result = [sorted(res, key=lambda k: k['entity_id']) 63 | for res in target_result] 64 | 65 | assert all(sorted(res, key=lambda k: k['entity_id']) in target_result 66 | for res in result) 67 | assert all(sorted(res, key=lambda k: k['entity_id']) in result 68 | for res in target_result) 69 | 70 | def test_proc_filter_target(static_bayes): 71 | observations = [[{'entity_id': 'foo', 'below': 1}, 72 | {'entity_id': 'bar'},], 73 | [{'entity_id': 'foo', 'above': 3},], 74 | [{'entity_id': 'baz'}]] 75 | 76 | result = static_bayes.filter_target(observations, 77 | {'entity_id': 'foo', 'below': 1}) 78 | 79 | assert list(result) == [[{'entity_id': 'foo', 'below': 1}, 80 | {'entity_id': 'bar'},]] 81 | 82 | def test_probability_updates(static_bayes): 83 | """Test probability update function.""" 84 | prob_true = [0.3, 0.6, 0.8] 85 | prob_false = [0.7, 0.4, 0.2] 86 | prior = 0.5 87 | 88 | for pt, pf in zip(prob_true, prob_false): 89 | prior = static_bayes.update_probability(prior, pt, pf) 90 | 91 | assert prior == 0.720000 92 | 93 | prob_true = [0.8, 0.3, 0.9] 94 | prob_false = [0.6, 0.4, 0.2] 95 | prior = 0.7 96 | 97 | for pt, pf in zip(prob_true, prob_false): 98 | prior = static_bayes.update_probability(prior, pt, pf) 99 | 100 | assert prior == 0.9130434782608695 101 | 102 | def test_proc_sensors_from_parsed_yaml(static_bayes): 103 | multi_yaml = [{'platform': 'foo'}, 104 | {'platform': 'bayesian', 105 | 'entity_id': 'sensor1'}, 106 | {'platform': 'bar'}, 107 | {'platform': 'bayesian', 108 | 'entity_id': 'sensor2'}, 109 | ] 110 | 111 | result = static_bayes.sensors_from_parsed_yaml(multi_yaml, None) 112 | assert result == [{'platform': 'bayesian', 'entity_id': 'sensor1'}, 113 | {'platform': 'bayesian', 'entity_id': 'sensor2'}] 114 | 115 | result = static_bayes.sensors_from_parsed_yaml(multi_yaml, 0) 116 | assert result == [{'platform': 'bayesian', 'entity_id': 'sensor1'}] 117 | 118 | single_yaml = {'platform': 'bayesian', 'entity_id': 'sensor2'} 119 | result = static_bayes.sensors_from_parsed_yaml(single_yaml, None) 120 | assert result == [{'platform': 'bayesian', 'entity_id': 'sensor2'}] 121 | 122 | single_yaml = {'platform': 'foo'} 123 | result = static_bayes.sensors_from_parsed_yaml(single_yaml, None) 124 | assert result == [] 125 | 126 | result = static_bayes.sensors_from_parsed_yaml('not a list or dict', None) 127 | assert result == [] 128 | 129 | -------------------------------------------------------------------------------- /examples/Smass Examples.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "import json\n", 11 | "\n", 12 | "%matplotlib inline" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "metadata": {}, 19 | "outputs": [ 20 | { 21 | "data": { 22 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXoAAAFtCAYAAAAJc6GzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4wLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvpW3flQAAIABJREFUeJzt3XmcZmVh5fHfkcVWIawNkm6wUSGM\nElBoiKKCSFSUTCAZN1xYTZsRDBNMIjNqEpwsmEww6kSSjmBajSBBDQQQISyKGpZm36UlkK4OS9si\nLkhYPPnjPhVemuqu962l771Pn+/nU59673NvvXUarVO3nrvJNhERUa9ntB0gIiJmV4o+IqJyKfqI\niMql6CMiKpeij4ioXIo+IqJyKfqIiMql6CMiKpeij4io3IZtBwDYeuutvWDBgrZjRET0yjXXXPM9\n23Mn264TRb9gwQKWLl3adoyIiF6RdM8w22XqJiKicin6iIjKpegjIirXiTn6iIi2PPbYY4yNjfHI\nI4+0HWWN5syZw/z589loo42m9PUp+ohYr42NjbHpppuyYMECJLUd52lss2rVKsbGxthxxx2n9B6Z\nuomI9dojjzzCVltt1cmSB5DEVlttNa2/OFL0EbHe62rJj5tuvhR9RETlMkcfETFgwQnnzej73X3S\nQZNuc9RRR3HuueeyzTbbcPPNN8/o94cUfUzBTP8grG6YH4yImhxxxBEce+yxHHbYYbPy/pm6iYho\n2b777suWW245a++foo+IqFyKPiKickMVvaTNJZ0l6XZJt0l6uaQtJV0k6c7yeYuyrSR9QtIySTdK\n2mN2/wkREbE2w+7Rfxy4wPYuwO7AbcAJwMW2dwIuLssAbwB2Kh+LgFNmNHFERIxk0rNuJG0G7Asc\nAWD7UeBRSQcDry6bLQEuAz4AHAx81raBK8pfA9vZvnfG00dEzLA2zvo69NBDueyyy/je977H/Pnz\nOfHEEzn66KNn7P2HOb1yR2Al8BlJuwPXAMcB2w6U933AtuX1PGD5wNePlbEUfUTEBE4//fRZff9h\npm42BPYATrH9UuAnPDlNA0DZe/co31jSIklLJS1duXLlKF8aEREjGKbox4Ax21eW5bNoiv9+SdsB\nlM8PlPUrgO0Hvn5+GXsK24ttL7S9cO7cSR95GBERUzRp0du+D1gu6RfK0AHArcA5wOFl7HDg7PL6\n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\nBTX0gJsAAAAASUVORK5CYII=\n", 23 | "text/plain": [ 24 | "" 25 | ] 26 | }, 27 | "metadata": {}, 28 | "output_type": "display_data" 29 | } 30 | ], 31 | "source": [ 32 | "conf_file = '~/ownCloud/Github/home_automation/home_assistant/binary_sensors/obscure_events.yaml'\n", 33 | "index = 1\n", 34 | "target = 'sensor.hour_of_day'\n", 35 | "conditions = [\n", 36 | " '--above 20 --below 22',\n", 37 | " '--above 21',\n", 38 | " '--below 6',\n", 39 | " '--below 7',\n", 40 | " '--below 8',\n", 41 | " '--above 7 --below 10',\n", 42 | " '--above 9 --below 21',\n", 43 | "]\n", 44 | "\n", 45 | "match_tuples = []\n", 46 | "for cond in conditions:\n", 47 | " result = !smass bayes -si {index} -c {conf_file} -te {target} {cond} -s\n", 48 | " result_string = ''.join(result.fields()[0])\n", 49 | " match_count = json.loads(result_string)[0]['total_matching']\n", 50 | " \n", 51 | " match_tuples.append((cond, match_count))\n", 52 | " \n", 53 | "match_df = pd.DataFrame(match_tuples).set_index(0)\n", 54 | "match_df.plot(kind='bar');" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [] 70 | } 71 | ], 72 | "metadata": { 73 | "kernelspec": { 74 | "display_name": "Python 3", 75 | "language": "python", 76 | "name": "python3" 77 | }, 78 | "language_info": { 79 | "codemirror_mode": { 80 | "name": "ipython", 81 | "version": 3 82 | }, 83 | "file_extension": ".py", 84 | "mimetype": "text/x-python", 85 | "name": "python", 86 | "nbconvert_exporter": "python", 87 | "pygments_lexer": "ipython3", 88 | "version": "3.5.2" 89 | } 90 | }, 91 | "nbformat": 4, 92 | "nbformat_minor": 2 93 | } 94 | -------------------------------------------------------------------------------- /smart_hass/templates/bruh_mqtt_multisensor.j2: -------------------------------------------------------------------------------- 1 | /* 2 | .______ .______ __ __ __ __ ___ __ __ .___________. ______ .___ ___. ___ .___________. __ ______ .__ __. 3 | | _ \ | _ \ | | | | | | | | / \ | | | | | | / __ \ | \/ | / \ | || | / __ \ | \ | | 4 | | |_) | | |_) | | | | | | |__| | / ^ \ | | | | `---| |----`| | | | | \ / | / ^ \ `---| |----`| | | | | | | \| | 5 | | _ < | / | | | | | __ | / /_\ \ | | | | | | | | | | | |\/| | / /_\ \ | | | | | | | | | . ` | 6 | | |_) | | |\ \-.| `--' | | | | | / _____ \ | `--' | | | | `--' | | | | | / _____ \ | | | | | `--' | | |\ | 7 | |______/ | _| `.__| \______/ |__| |__| /__/ \__\ \______/ |__| \______/ |__| |__| /__/ \__\ |__| |__| \______/ |__| \__| 8 | 9 | Thanks much to @corbanmailloux for providing a great framework for implementing flash/fade with HomeAssistant https://github.com/corbanmailloux/esp-mqtt-rgb-led 10 | 11 | To use this code you will need the following dependancies: 12 | 13 | - Support for the ESP8266 boards. 14 | - You can add it to the board manager by going to File -> Preference and pasting http://arduino.esp8266.com/stable/package_esp8266com_index.json into the Additional Board Managers URL field. 15 | - Next, download the ESP8266 dependancies by going to Tools -> Board -> Board Manager and searching for ESP8266 and installing it. 16 | 17 | - You will also need to download the follow libraries by going to Sketch -> Include Libraries -> Manage Libraries 18 | - DHT sensor library 19 | - Adafruit unified sensor 20 | - PubSubClient 21 | - ArduinoJSON 22 | 23 | UPDATE 16 MAY 2017 by Knutella - Fixed MQTT disconnects when wifi drops by moving around Reconnect and adding a software reset of MCU 24 | 25 | UPDATE 23 MAY 2017 - The MQTT_MAX_PACKET_SIZE parameter may not be setting appropriately do to a bug in the PubSub library. If the MQTT messages are not being transmitted as expected please you may need to change the MQTT_MAX_PACKET_SIZE parameter in "PubSubClient.h" directly. 26 | 27 | */ 28 | 29 | 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | 40 | 41 | 42 | /************ WIFI and MQTT INFORMATION (CHANGE THESE FOR YOUR SETUP) ******************/ 43 | #define wifi_ssid "{{ wifi_ssid }}" //type your WIFI information inside the quotes 44 | #define wifi_password "{{ wifi_pwd }}" 45 | #define mqtt_server "{{ mqtt_server }}" 46 | #define mqtt_user "{{ mqtt_user }}" 47 | #define mqtt_password "{{ mqtt_pwd }}" 48 | #define mqtt_port {{ mqtt_port }} 49 | 50 | 51 | 52 | /************* MQTT TOPICS (change these topics as you wish) **************************/ 53 | #define light_state_topic "bruh/{{ sensor_name }}" 54 | #define light_set_topic "bruh/{{ sensor_name }}/set" 55 | 56 | const char* on_cmd = "ON"; 57 | const char* off_cmd = "OFF"; 58 | 59 | 60 | 61 | /**************************** FOR OTA **************************************************/ 62 | #define SENSORNAME "{{ sensor_name }}" 63 | #define OTApassword "{{ ota_pwd }}" // change this to whatever password you want to use when you upload OTA 64 | int OTAport = 8266; 65 | 66 | 67 | 68 | /**************************** PIN DEFINITIONS ********************************************/ 69 | const int redPin = D1; 70 | const int greenPin = D2; 71 | const int bluePin = D3; 72 | #define PIRPIN D5 73 | #define DHTPIN D7 74 | #define DHTTYPE DHT22 75 | #define LDRPIN A0 76 | 77 | 78 | 79 | /**************************** SENSOR DEFINITIONS *******************************************/ 80 | float ldrValue; 81 | int LDR; 82 | float calcLDR; 83 | float diffLDR = 5; 84 | 85 | float diffTEMP = 0.2; 86 | float tempValue; 87 | 88 | float diffHUM = 1; 89 | float humValue; 90 | 91 | int pirValue; 92 | int pirStatus; 93 | String motionStatus; 94 | 95 | char message_buff[100]; 96 | 97 | int calibrationTime = 0; 98 | 99 | const int BUFFER_SIZE = 300; 100 | 101 | #define MQTT_MAX_PACKET_SIZE 512 102 | 103 | 104 | /******************************** GLOBALS for fade/flash *******************************/ 105 | byte red = 255; 106 | byte green = 255; 107 | byte blue = 255; 108 | byte brightness = 255; 109 | 110 | byte realRed = 0; 111 | byte realGreen = 0; 112 | byte realBlue = 0; 113 | 114 | bool stateOn = false; 115 | 116 | bool startFade = false; 117 | unsigned long lastLoop = 0; 118 | int transitionTime = 0; 119 | bool inFade = false; 120 | int loopCount = 0; 121 | int stepR, stepG, stepB; 122 | int redVal, grnVal, bluVal; 123 | 124 | bool flash = false; 125 | bool startFlash = false; 126 | int flashLength = 0; 127 | unsigned long flashStartTime = 0; 128 | byte flashRed = red; 129 | byte flashGreen = green; 130 | byte flashBlue = blue; 131 | byte flashBrightness = brightness; 132 | 133 | 134 | 135 | WiFiClient espClient; 136 | PubSubClient client(espClient); 137 | DHT dht(DHTPIN, DHTTYPE); 138 | 139 | 140 | 141 | /********************************** START SETUP*****************************************/ 142 | void setup() { 143 | 144 | Serial.begin(115200); 145 | 146 | pinMode(PIRPIN, INPUT); 147 | pinMode(DHTPIN, INPUT); 148 | pinMode(LDRPIN, INPUT); 149 | 150 | Serial.begin(115200); 151 | delay(10); 152 | 153 | ArduinoOTA.setPort(OTAport); 154 | 155 | ArduinoOTA.setHostname(SENSORNAME); 156 | 157 | ArduinoOTA.setPassword((const char *)OTApassword); 158 | 159 | Serial.print("calibrating sensor "); 160 | for (int i = 0; i < calibrationTime; i++) { 161 | Serial.print("."); 162 | delay(1000); 163 | } 164 | 165 | Serial.println("Starting Node named " + String(SENSORNAME)); 166 | 167 | 168 | setup_wifi(); 169 | 170 | client.setServer(mqtt_server, mqtt_port); 171 | client.setCallback(callback); 172 | 173 | 174 | ArduinoOTA.onStart([]() { 175 | Serial.println("Starting"); 176 | }); 177 | ArduinoOTA.onEnd([]() { 178 | Serial.println("\nEnd"); 179 | }); 180 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 181 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 182 | }); 183 | ArduinoOTA.onError([](ota_error_t error) { 184 | Serial.printf("Error[%u]: ", error); 185 | if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); 186 | else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); 187 | else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); 188 | else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); 189 | else if (error == OTA_END_ERROR) Serial.println("End Failed"); 190 | }); 191 | ArduinoOTA.begin(); 192 | Serial.println("Ready"); 193 | Serial.print("IPess: "); 194 | Serial.println(WiFi.localIP()); 195 | reconnect(); 196 | } 197 | 198 | 199 | 200 | 201 | /********************************** START SETUP WIFI*****************************************/ 202 | void setup_wifi() { 203 | 204 | delay(10); 205 | Serial.println(); 206 | Serial.print("Connecting to "); 207 | Serial.println(wifi_ssid); 208 | 209 | WiFi.mode(WIFI_STA); 210 | WiFi.begin(wifi_ssid, wifi_password); 211 | 212 | while (WiFi.status() != WL_CONNECTED) { 213 | delay(500); 214 | Serial.print("."); 215 | } 216 | 217 | Serial.println(""); 218 | Serial.println("WiFi connected"); 219 | Serial.println("IP address: "); 220 | Serial.println(WiFi.localIP()); 221 | } 222 | 223 | 224 | 225 | /********************************** START CALLBACK*****************************************/ 226 | void callback(char* topic, byte* payload, unsigned int length) { 227 | Serial.print("Message arrived ["); 228 | Serial.print(topic); 229 | Serial.print("] "); 230 | 231 | char message[length + 1]; 232 | for (int i = 0; i < length; i++) { 233 | message[i] = (char)payload[i]; 234 | } 235 | message[length] = '\0'; 236 | Serial.println(message); 237 | 238 | if (!processJson(message)) { 239 | return; 240 | } 241 | 242 | if (stateOn) { 243 | // Update lights 244 | realRed = map(red, 0, 255, 0, brightness); 245 | realGreen = map(green, 0, 255, 0, brightness); 246 | realBlue = map(blue, 0, 255, 0, brightness); 247 | } 248 | else { 249 | realRed = 0; 250 | realGreen = 0; 251 | realBlue = 0; 252 | } 253 | 254 | startFade = true; 255 | inFade = false; // Kill the current fade 256 | 257 | sendState(); 258 | } 259 | 260 | 261 | 262 | /********************************** START PROCESS JSON*****************************************/ 263 | bool processJson(char* message) { 264 | StaticJsonBuffer jsonBuffer; 265 | 266 | JsonObject& root = jsonBuffer.parseObject(message); 267 | 268 | if (!root.success()) { 269 | Serial.println("parseObject() failed"); 270 | return false; 271 | } 272 | 273 | if (root.containsKey("state")) { 274 | if (strcmp(root["state"], on_cmd) == 0) { 275 | stateOn = true; 276 | } 277 | else if (strcmp(root["state"], off_cmd) == 0) { 278 | stateOn = false; 279 | } 280 | } 281 | 282 | // If "flash" is included, treat RGB and brightness differently 283 | if (root.containsKey("flash")) { 284 | flashLength = (int)root["flash"] * 1000; 285 | 286 | if (root.containsKey("brightness")) { 287 | flashBrightness = root["brightness"]; 288 | } 289 | else { 290 | flashBrightness = brightness; 291 | } 292 | 293 | if (root.containsKey("color")) { 294 | flashRed = root["color"]["r"]; 295 | flashGreen = root["color"]["g"]; 296 | flashBlue = root["color"]["b"]; 297 | } 298 | else { 299 | flashRed = red; 300 | flashGreen = green; 301 | flashBlue = blue; 302 | } 303 | 304 | flashRed = map(flashRed, 0, 255, 0, flashBrightness); 305 | flashGreen = map(flashGreen, 0, 255, 0, flashBrightness); 306 | flashBlue = map(flashBlue, 0, 255, 0, flashBrightness); 307 | 308 | flash = true; 309 | startFlash = true; 310 | } 311 | else { // Not flashing 312 | flash = false; 313 | 314 | if (root.containsKey("color")) { 315 | red = root["color"]["r"]; 316 | green = root["color"]["g"]; 317 | blue = root["color"]["b"]; 318 | } 319 | 320 | if (root.containsKey("brightness")) { 321 | brightness = root["brightness"]; 322 | } 323 | 324 | if (root.containsKey("transition")) { 325 | transitionTime = root["transition"]; 326 | } 327 | else { 328 | transitionTime = 0; 329 | } 330 | } 331 | 332 | return true; 333 | } 334 | 335 | 336 | 337 | /********************************** START SEND STATE*****************************************/ 338 | void sendState() { 339 | StaticJsonBuffer jsonBuffer; 340 | 341 | JsonObject& root = jsonBuffer.createObject(); 342 | 343 | root["state"] = (stateOn) ? on_cmd : off_cmd; 344 | JsonObject& color = root.createNestedObject("color"); 345 | color["r"] = red; 346 | color["g"] = green; 347 | color["b"] = blue; 348 | 349 | 350 | root["brightness"] = brightness; 351 | root["humidity"] = (String)humValue; 352 | root["motion"] = (String)motionStatus; 353 | root["ldr"] = (String)LDR; 354 | root["temperature"] = (String)tempValue; 355 | 356 | 357 | char buffer[root.measureLength() + 1]; 358 | root.printTo(buffer, sizeof(buffer)); 359 | 360 | Serial.println(buffer); 361 | client.publish(light_state_topic, buffer, true); 362 | } 363 | 364 | 365 | /********************************** START SET COLOR *****************************************/ 366 | void setColor(int inR, int inG, int inB) { 367 | analogWrite(redPin, inR); 368 | analogWrite(greenPin, inG); 369 | analogWrite(bluePin, inB); 370 | 371 | Serial.println("Setting LEDs:"); 372 | Serial.print("r: "); 373 | Serial.print(inR); 374 | Serial.print(", g: "); 375 | Serial.print(inG); 376 | Serial.print(", b: "); 377 | Serial.println(inB); 378 | } 379 | 380 | 381 | 382 | /********************************** START RECONNECT*****************************************/ 383 | void reconnect() { 384 | // Loop until we're reconnected 385 | while (!client.connected()) { 386 | Serial.print("Attempting MQTT connection..."); 387 | // Attempt to connect 388 | if (client.connect(SENSORNAME, mqtt_user, mqtt_password)) { 389 | Serial.println("connected"); 390 | client.subscribe(light_set_topic); 391 | setColor(0, 0, 0); 392 | sendState(); 393 | } else { 394 | Serial.print("failed, rc="); 395 | Serial.print(client.state()); 396 | Serial.println(" try again in 5 seconds"); 397 | // Wait 5 seconds before retrying 398 | delay(5000); 399 | } 400 | } 401 | } 402 | 403 | 404 | 405 | /********************************** START CHECK SENSOR **********************************/ 406 | bool checkBoundSensor(float newValue, float prevValue, float maxDiff) { 407 | return newValue < prevValue - maxDiff || newValue > prevValue + maxDiff; 408 | } 409 | 410 | 411 | /********************************** START MAIN LOOP***************************************/ 412 | void loop() { 413 | 414 | ArduinoOTA.handle(); 415 | 416 | if (!client.connected()) { 417 | // reconnect(); 418 | software_Reset(); 419 | } 420 | client.loop(); 421 | 422 | if (!inFade) { 423 | 424 | float newTempValue = dht.readTemperature(true); //to use celsius remove the true text inside the parentheses 425 | float newHumValue = dht.readHumidity(); 426 | 427 | //PIR CODE 428 | pirValue = digitalRead(PIRPIN); //read state of the 429 | 430 | if (pirValue == LOW && pirStatus != 1) { 431 | motionStatus = "standby"; 432 | sendState(); 433 | pirStatus = 1; 434 | } 435 | 436 | else if (pirValue == HIGH && pirStatus != 2) { 437 | motionStatus = "motion detected"; 438 | sendState(); 439 | pirStatus = 2; 440 | } 441 | 442 | delay(100); 443 | 444 | if (checkBoundSensor(newTempValue, tempValue, diffTEMP)) { 445 | tempValue = newTempValue; 446 | sendState(); 447 | } 448 | 449 | if (checkBoundSensor(newHumValue, humValue, diffHUM)) { 450 | humValue = newHumValue; 451 | sendState(); 452 | } 453 | 454 | 455 | int newLDR = analogRead(LDRPIN); 456 | 457 | if (checkBoundSensor((newLDR + LDR)/2, LDR, diffLDR)) { 458 | LDR = newLDR; 459 | sendState(); 460 | } 461 | 462 | } 463 | 464 | if (flash) { 465 | if (startFlash) { 466 | startFlash = false; 467 | flashStartTime = millis(); 468 | } 469 | 470 | if ((millis() - flashStartTime) <= flashLength) { 471 | if ((millis() - flashStartTime) % 1000 <= 500) { 472 | setColor(flashRed, flashGreen, flashBlue); 473 | } 474 | else { 475 | setColor(0, 0, 0); 476 | // If you'd prefer the flashing to happen "on top of" 477 | // the current color, uncomment the next line. 478 | // setColor(realRed, realGreen, realBlue); 479 | } 480 | } 481 | else { 482 | flash = false; 483 | setColor(realRed, realGreen, realBlue); 484 | } 485 | } 486 | 487 | if (startFade) { 488 | // If we don't want to fade, skip it. 489 | if (transitionTime == 0) { 490 | setColor(realRed, realGreen, realBlue); 491 | 492 | redVal = realRed; 493 | grnVal = realGreen; 494 | bluVal = realBlue; 495 | 496 | startFade = false; 497 | } 498 | else { 499 | loopCount = 0; 500 | stepR = calculateStep(redVal, realRed); 501 | stepG = calculateStep(grnVal, realGreen); 502 | stepB = calculateStep(bluVal, realBlue); 503 | 504 | inFade = true; 505 | } 506 | } 507 | 508 | if (inFade) { 509 | startFade = false; 510 | unsigned long now = millis(); 511 | if (now - lastLoop > transitionTime) { 512 | if (loopCount <= 1020) { 513 | lastLoop = now; 514 | 515 | redVal = calculateVal(stepR, redVal, loopCount); 516 | grnVal = calculateVal(stepG, grnVal, loopCount); 517 | bluVal = calculateVal(stepB, bluVal, loopCount); 518 | 519 | setColor(redVal, grnVal, bluVal); // Write current values to LED pins 520 | 521 | Serial.print("Loop count: "); 522 | Serial.println(loopCount); 523 | loopCount++; 524 | } 525 | else { 526 | inFade = false; 527 | } 528 | } 529 | } 530 | } 531 | 532 | 533 | 534 | 535 | /**************************** START TRANSITION FADER *****************************************/ 536 | // From https://www.arduino.cc/en/Tutorial/ColorCrossfader 537 | /* BELOW THIS LINE IS THE MATH -- YOU SHOULDN'T NEED TO CHANGE THIS FOR THE BASICS 538 | 539 | The program works like this: 540 | Imagine a crossfade that moves the red LED from 0-10, 541 | the green from 0-5, and the blue from 10 to 7, in 542 | ten steps. 543 | We'd want to count the 10 steps and increase or 544 | decrease color values in evenly stepped increments. 545 | Imagine a + indicates raising a value by 1, and a - 546 | equals lowering it. Our 10 step fade would look like: 547 | 548 | 1 2 3 4 5 6 7 8 9 10 549 | R + + + + + + + + + + 550 | G + + + + + 551 | B - - - 552 | 553 | The red rises from 0 to 10 in ten steps, the green from 554 | 0-5 in 5 steps, and the blue falls from 10 to 7 in three steps. 555 | 556 | In the real program, the color percentages are converted to 557 | 0-255 values, and there are 1020 steps (255*4). 558 | 559 | To figure out how big a step there should be between one up- or 560 | down-tick of one of the LED values, we call calculateStep(), 561 | which calculates the absolute gap between the start and end values, 562 | and then divides that gap by 1020 to determine the size of the step 563 | between adjustments in the value. 564 | */ 565 | int calculateStep(int prevValue, int endValue) { 566 | int step = endValue - prevValue; // What's the overall gap? 567 | if (step) { // If its non-zero, 568 | step = 1020 / step; // divide by 1020 569 | } 570 | 571 | return step; 572 | } 573 | 574 | /* The next function is calculateVal. When the loop value, i, 575 | reaches the step size appropriate for one of the 576 | colors, it increases or decreases the value of that color by 1. 577 | (R, G, and B are each calculated separately.) 578 | */ 579 | int calculateVal(int step, int val, int i) { 580 | if ((step) && i % step == 0) { // If step is non-zero and its time to change a value, 581 | if (step > 0) { // increment the value if step is positive... 582 | val += 1; 583 | } 584 | else if (step < 0) { // ...or decrement it if step is negative 585 | val -= 1; 586 | } 587 | } 588 | 589 | // Defensive driving: make sure val stays in the range 0-255 590 | if (val > 255) { 591 | val = 255; 592 | } 593 | else if (val < 0) { 594 | val = 0; 595 | } 596 | 597 | return val; 598 | } 599 | 600 | /****reset***/ 601 | void software_Reset() // Restarts program from beginning but does not reset the peripherals and registers 602 | { 603 | Serial.print("resetting"); 604 | ESP.reset(); 605 | } 606 | --------------------------------------------------------------------------------