diff --git a/.coveragerc b/.coveragerc index 48ea0375587..ff540ca1f2e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -138,6 +138,7 @@ omit = homeassistant/components/device_tracker/ubus.py homeassistant/components/discovery.py homeassistant/components/downloader.py + homeassistant/components/fan/mqtt.py homeassistant/components/feedreader.py homeassistant/components/foursquare.py homeassistant/components/garage_door/rpi_gpio.py @@ -205,6 +206,7 @@ omit = homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/bitcoin.py + homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py @@ -223,6 +225,7 @@ omit = homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/lastfm.py + homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/mhz19.py homeassistant/components/sensor/mqtt_room.py @@ -232,6 +235,7 @@ omit = homeassistant/components/sensor/onewire.py homeassistant/components/sensor/openexchangerates.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/sabnzbd.py @@ -250,6 +254,7 @@ omit = homeassistant/components/sensor/twitch.py homeassistant/components/sensor/uber.py homeassistant/components/sensor/worldclock.py + homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/yweather.py homeassistant/components/switch/acer_projector.py homeassistant/components/switch/arest.py diff --git a/.gitignore b/.gitignore index b73dcef1073..147d68c36d3 100644 --- a/.gitignore +++ b/.gitignore @@ -96,4 +96,7 @@ virtualization/vagrant/.vagrant virtualization/vagrant/config # Visual Studio Code -.vscode \ No newline at end of file +.vscode + +# Built docs +docs/build \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000000..69893c43847 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,230 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " livehtml to make standalone HTML files via sphinx-autobuild" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: livehtml +livehtml: + sphinx-autobuild -z ../homeassistant/ --port 0 -B -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Home-Assistant.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Home-Assistant.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Home-Assistant" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Home-Assistant" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/build/.empty b/docs/build/.empty new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000000..7713f1cadb0 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,281 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +set I18NSPHINXOPTS=%SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. epub3 to make an epub3 + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + echo. dummy to check syntax errors of document sources + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Home-Assistant.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Home-Assistant.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "epub3" ( + %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +if "%1" == "dummy" ( + %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. Dummy builder generates no files. + goto end +) + +:end diff --git a/docs/source/_ext/edit_on_github.py b/docs/source/_ext/edit_on_github.py new file mode 100644 index 00000000000..eef249a3f01 --- /dev/null +++ b/docs/source/_ext/edit_on_github.py @@ -0,0 +1,45 @@ +""" +Sphinx extension to add ReadTheDocs-style "Edit on GitHub" links to the +sidebar. + +Loosely based on https://github.com/astropy/astropy/pull/347 +""" + +import os +import warnings + + +__licence__ = 'BSD (3 clause)' + + +def get_github_url(app, view, path): + github_fmt = 'https://github.com/{}/{}/{}/{}{}' + return ( + github_fmt.format(app.config.edit_on_github_project, view, + app.config.edit_on_github_branch, + app.config.edit_on_github_src_path, path)) + + +def html_page_context(app, pagename, templatename, context, doctree): + if templatename != 'page.html': + return + + if not app.config.edit_on_github_project: + warnings.warn("edit_on_github_project not specified") + return + if not doctree: + warnings.warn("doctree is None") + return + path = os.path.relpath(doctree.get('source'), app.builder.srcdir) + show_url = get_github_url(app, 'blob', path) + edit_url = get_github_url(app, 'edit', path) + + context['show_on_github_url'] = show_url + context['edit_on_github_url'] = edit_url + + +def setup(app): + app.add_config_value('edit_on_github_project', '', True) + app.add_config_value('edit_on_github_branch', 'master', True) + app.add_config_value('edit_on_github_src_path', '', True) # 'eg' "docs/" + app.connect('html-page-context', html_page_context) diff --git a/docs/source/_static/favicon.ico b/docs/source/_static/favicon.ico new file mode 100644 index 00000000000..6d12158c18b Binary files /dev/null and b/docs/source/_static/favicon.ico differ diff --git a/docs/source/_static/logo-apple.png b/docs/source/_static/logo-apple.png new file mode 100644 index 00000000000..20117d00f22 Binary files /dev/null and b/docs/source/_static/logo-apple.png differ diff --git a/docs/source/_static/logo.png b/docs/source/_static/logo.png new file mode 100644 index 00000000000..2959efdf89d Binary files /dev/null and b/docs/source/_static/logo.png differ diff --git a/docs/source/_templates/links.html b/docs/source/_templates/links.html new file mode 100644 index 00000000000..57a2e09f99e --- /dev/null +++ b/docs/source/_templates/links.html @@ -0,0 +1,8 @@ + +
diff --git a/docs/source/_templates/sourcelink.html b/docs/source/_templates/sourcelink.html new file mode 100644 index 00000000000..8cf2c4f92ae --- /dev/null +++ b/docs/source/_templates/sourcelink.html @@ -0,0 +1,13 @@ +{%- if show_source and has_source and sourcename %} +

{{ _('This Page') }}

+ +{%- endif %} diff --git a/docs/source/api/bootstrap.rst b/docs/source/api/bootstrap.rst new file mode 100644 index 00000000000..363f7969961 --- /dev/null +++ b/docs/source/api/bootstrap.rst @@ -0,0 +1,7 @@ +.. _bootstrap_module: + +:mod:`homeassistant.bootstrap` +------------------------- + +.. automodule:: homeassistant.bootstrap + :members: diff --git a/docs/source/api/core.rst b/docs/source/api/core.rst new file mode 100644 index 00000000000..a32bdc24d11 --- /dev/null +++ b/docs/source/api/core.rst @@ -0,0 +1,18 @@ +.. _core_module: + +:mod:`homeassistant.core` +------------------------- + +.. automodule:: homeassistant.core + +.. autoclass:: Config + :members: + +.. autoclass:: EventBus + :members: + +.. autoclass:: StateMachine + :members: + +.. autoclass:: ServiceRegistry + :members: diff --git a/docs/source/api/device_tracker.rst b/docs/source/api/device_tracker.rst new file mode 100644 index 00000000000..e3d65174815 --- /dev/null +++ b/docs/source/api/device_tracker.rst @@ -0,0 +1,10 @@ +.. _components_device_tracker_module: + +:mod:`homeassistant.components.device_tracker` +---------------------------------------------- + +.. automodule:: homeassistant.components.device_tracker + :members: + +.. autoclass:: Device + :members: diff --git a/docs/source/api/entity.rst b/docs/source/api/entity.rst new file mode 100644 index 00000000000..99ae43dc3ae --- /dev/null +++ b/docs/source/api/entity.rst @@ -0,0 +1,12 @@ +.. _helpers_entity_module: + +:mod:`homeassistant.helpers.entity` +----------------------------------- + +.. automodule:: homeassistant.helpers.entity + +.. autoclass:: Entity + :members: + +.. autoclass:: ToggleEntity + :members: diff --git a/docs/source/api/event.rst b/docs/source/api/event.rst new file mode 100644 index 00000000000..b1295b81409 --- /dev/null +++ b/docs/source/api/event.rst @@ -0,0 +1,20 @@ +.. _helpers_event_module: + +:mod:`homeassistant.helpers.event` +---------------------------------- + +.. automodule:: homeassistant.helpers.event + +.. autofunction:: track_state_change + +.. autofunction:: track_point_in_time + +.. autofunction:: track_point_in_utc_time + +.. autofunction:: track_sunrise + +.. autofunction:: track_sunset + +.. autofunction:: track_utc_time_change + +.. autofunction:: track_time_change diff --git a/docs/source/api/helpers.rst b/docs/source/api/helpers.rst new file mode 100644 index 00000000000..af186fb1341 --- /dev/null +++ b/docs/source/api/helpers.rst @@ -0,0 +1,118 @@ +homeassistant.helpers package +============================= + +Submodules +---------- + +homeassistant.helpers.condition module +-------------------------------------- + +.. automodule:: homeassistant.helpers.condition + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.config_validation module +---------------------------------------------- + +.. automodule:: homeassistant.helpers.config_validation + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.discovery module +-------------------------------------- + +.. automodule:: homeassistant.helpers.discovery + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entity module +----------------------------------- + +.. automodule:: homeassistant.helpers.entity + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entity_component module +--------------------------------------------- + +.. automodule:: homeassistant.helpers.entity_component + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.event module +---------------------------------- + +.. automodule:: homeassistant.helpers.event + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.event_decorators module +--------------------------------------------- + +.. automodule:: homeassistant.helpers.event_decorators + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.location module +------------------------------------- + +.. automodule:: homeassistant.helpers.location + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.script module +----------------------------------- + +.. automodule:: homeassistant.helpers.script + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.service module +------------------------------------ + +.. automodule:: homeassistant.helpers.service + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.state module +---------------------------------- + +.. automodule:: homeassistant.helpers.state + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.template module +------------------------------------- + +.. automodule:: homeassistant.helpers.template + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.typing module +----------------------------------- + +.. automodule:: homeassistant.helpers.typing + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: homeassistant.helpers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/homeassistant.rst b/docs/source/api/homeassistant.rst new file mode 100644 index 00000000000..f5ff069451d --- /dev/null +++ b/docs/source/api/homeassistant.rst @@ -0,0 +1,78 @@ +homeassistant package +===================== + +Subpackages +----------- + +.. toctree:: + + helpers + util + +Submodules +---------- + +bootstrap module +------------------------------ + +.. automodule:: homeassistant.bootstrap + :members: + :undoc-members: + :show-inheritance: + +config module +--------------------------- + +.. automodule:: homeassistant.config + :members: + :undoc-members: + :show-inheritance: + +const module +-------------------------- + +.. automodule:: homeassistant.const + :members: + :undoc-members: + :show-inheritance: + +core module +------------------------- + +.. automodule:: homeassistant.core + :members: + :undoc-members: + :show-inheritance: + +exceptions module +------------------------------- + +.. automodule:: homeassistant.exceptions + :members: + :undoc-members: + :show-inheritance: + +loader module +--------------------------- + +.. automodule:: homeassistant.loader + :members: + :undoc-members: + :show-inheritance: + +remote module +--------------------------- + +.. automodule:: homeassistant.remote + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: homeassistant + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/util.rst b/docs/source/api/util.rst new file mode 100644 index 00000000000..7d6a22dbc0b --- /dev/null +++ b/docs/source/api/util.rst @@ -0,0 +1,78 @@ +homeassistant.util package +========================== + +Submodules +---------- + +homeassistant.util.color module +------------------------------- + +.. automodule:: homeassistant.util.color + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.distance module +---------------------------------- + +.. automodule:: homeassistant.util.distance + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.dt module +---------------------------- + +.. automodule:: homeassistant.util.dt + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.location module +---------------------------------- + +.. automodule:: homeassistant.util.location + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.package module +--------------------------------- + +.. automodule:: homeassistant.util.package + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.temperature module +------------------------------------- + +.. automodule:: homeassistant.util.temperature + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.unit_system module +------------------------------------- + +.. automodule:: homeassistant.util.unit_system + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.yaml module +------------------------------ + +.. automodule:: homeassistant.util.yaml + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: homeassistant.util + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000000..18b14795caa --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Home-Assistant documentation build configuration file, created by +# sphinx-quickstart on Sun Aug 28 13:13:10 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import sys +import os +from os.path import relpath +import inspect +from homeassistant.const import (__version__, __short_version__, PROJECT_NAME, + PROJECT_LONG_DESCRIPTION, + PROJECT_COPYRIGHT, PROJECT_AUTHOR, + PROJECT_GITHUB_USERNAME, + PROJECT_GITHUB_REPOSITORY, + GITHUB_PATH, GITHUB_URL) + + +sys.path.insert(0, os.path.abspath('_ext')) +sys.path.insert(0, os.path.abspath('../homeassistant')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.linkcode', + 'sphinx_autodoc_annotation', + 'edit_on_github' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = PROJECT_NAME +copyright = PROJECT_COPYRIGHT +author = PROJECT_AUTHOR + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = __short_version__ +# The full version, including alpha/beta/rc tags. +release = __version__ + +code_branch = 'dev' if 'dev' in __version__ else 'master' + +# Edit on Github config +edit_on_github_project = GITHUB_PATH +edit_on_github_branch = code_branch +edit_on_github_src_path = 'docs/source/' + + +def linkcode_resolve(domain, info): + """ + Determine the URL corresponding to Python object + """ + if domain != 'py': + return None + modname = info['module'] + fullname = info['fullname'] + submod = sys.modules.get(modname) + if submod is None: + return None + obj = submod + for part in fullname.split('.'): + try: + obj = getattr(obj, part) + except: + return None + try: + fn = inspect.getsourcefile(obj) + except: + fn = None + if not fn: + return None + try: + source, lineno = inspect.findsource(obj) + except: + lineno = None + if lineno: + linespec = "#L%d" % (lineno + 1) + else: + linespec = "" + fn = relpath(fn, start='../') + + return '{}/blob/{}/{}{}'.format(GITHUB_URL, code_branch, fn, linespec) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +html_theme_options = { + 'logo': 'logo.png', + 'logo_name': PROJECT_NAME, + 'description': PROJECT_LONG_DESCRIPTION, + 'github_user': PROJECT_GITHUB_USERNAME, + 'github_repo': PROJECT_GITHUB_REPOSITORY, + 'github_type': 'star', + 'github_banner': True, + 'travis_button': True, + 'touch_icon': 'logo-apple.png', + # 'fixed_sidebar': True, # Re-enable when we have more content +} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = 'Home-Assistant v0.27.0' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = '_static/logo.png' + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. +# This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +html_favicon = '_static/favicon.ico' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +html_sidebars = { + '**': [ + 'about.html', + 'links.html', + 'searchbox.html', + 'sourcelink.html', + 'navigation.html', + 'relations.html' + ] +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Home-Assistantdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Home-Assistant.tex', 'Home-Assistant Documentation', + 'Home-Assistant Team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# It false, will not define \strong, \code, itleref, \crossref ... but only +# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added +# packages. +# +# latex_keep_old_macro_names = True + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'home-assistant', 'Home-Assistant Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Home-Assistant', 'Home-Assistant Documentation', + author, 'Home-Assistant', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000000..a6157dc7aac --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,22 @@ +================================ +Home Assistant API Documentation +================================ + +Public API documentation for `Home Assistant developers`_. + +Contents: + +.. toctree:: + :maxdepth: 2 + :glob: + + api/* + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +.. _Home Assistant developers: https://home-assistant.io/developers/ diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 4b526c40b38..5e291e90717 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -14,7 +14,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error import homeassistant.components as core_components -from homeassistant.components import group, persistent_notification +from homeassistant.components import persistent_notification import homeassistant.config as conf_util import homeassistant.core as core import homeassistant.loader as loader @@ -90,67 +90,12 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool: domain, domain) return False + config = prepare_setup_component(hass, config, domain) + + if config is None: + return False + component = loader.get_component(domain) - missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', []) - if dep not in hass.config.components] - - if missing_deps: - _LOGGER.error( - 'Not initializing %s because not all dependencies loaded: %s', - domain, ", ".join(missing_deps)) - return False - - if hasattr(component, 'CONFIG_SCHEMA'): - try: - config = component.CONFIG_SCHEMA(config) - except vol.MultipleInvalid as ex: - log_exception(ex, domain, config) - return False - - elif hasattr(component, 'PLATFORM_SCHEMA'): - platforms = [] - for p_name, p_config in config_per_platform(config, domain): - # Validate component specific platform schema - try: - p_validated = component.PLATFORM_SCHEMA(p_config) - except vol.MultipleInvalid as ex: - log_exception(ex, domain, p_config) - return False - - # Not all platform components follow same pattern for platforms - # So if p_name is None we are not going to validate platform - # (the automation component is one of them) - if p_name is None: - platforms.append(p_validated) - continue - - platform = prepare_setup_platform(hass, config, domain, - p_name) - - if platform is None: - return False - - # Validate platform specific schema - if hasattr(platform, 'PLATFORM_SCHEMA'): - try: - p_validated = platform.PLATFORM_SCHEMA(p_validated) - except vol.MultipleInvalid as ex: - log_exception(ex, '{}.{}'.format(domain, p_name), - p_validated) - return False - - platforms.append(p_validated) - - # Create a copy of the configuration with all config for current - # component removed and add validated config back in. - filter_keys = extract_domain_configs(config, domain) - config = {key: value for key, value in config.items() - if key not in filter_keys} - config[domain] = platforms - - if not _handle_requirements(hass, component, domain): - return False - _CURRENT_SETUP.append(domain) try: @@ -173,7 +118,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool: # Assumption: if a component does not depend on groups # it communicates with devices - if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []): + if 'group' not in getattr(component, 'DEPENDENCIES', []): hass.pool.add_worker() hass.bus.fire( @@ -182,6 +127,74 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool: return True +def prepare_setup_component(hass: core.HomeAssistant, config: dict, + domain: str): + """Prepare setup of a component and return processed config.""" + # pylint: disable=too-many-return-statements + component = loader.get_component(domain) + missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', []) + if dep not in hass.config.components] + + if missing_deps: + _LOGGER.error( + 'Not initializing %s because not all dependencies loaded: %s', + domain, ", ".join(missing_deps)) + return None + + if hasattr(component, 'CONFIG_SCHEMA'): + try: + config = component.CONFIG_SCHEMA(config) + except vol.MultipleInvalid as ex: + log_exception(ex, domain, config) + return None + + elif hasattr(component, 'PLATFORM_SCHEMA'): + platforms = [] + for p_name, p_config in config_per_platform(config, domain): + # Validate component specific platform schema + try: + p_validated = component.PLATFORM_SCHEMA(p_config) + except vol.MultipleInvalid as ex: + log_exception(ex, domain, p_config) + return None + + # Not all platform components follow same pattern for platforms + # So if p_name is None we are not going to validate platform + # (the automation component is one of them) + if p_name is None: + platforms.append(p_validated) + continue + + platform = prepare_setup_platform(hass, config, domain, + p_name) + + if platform is None: + return None + + # Validate platform specific schema + if hasattr(platform, 'PLATFORM_SCHEMA'): + try: + p_validated = platform.PLATFORM_SCHEMA(p_validated) + except vol.MultipleInvalid as ex: + log_exception(ex, '{}.{}'.format(domain, p_name), + p_validated) + return None + + platforms.append(p_validated) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + filter_keys = extract_domain_configs(config, domain) + config = {key: value for key, value in config.items() + if key not in filter_keys} + config[domain] = platforms + + if not _handle_requirements(hass, component, domain): + return None + + return config + + def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, platform_name: str) -> Optional[ModuleType]: """Load a platform and makes sure dependencies are setup.""" diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 542cb5e3d02..a986d911115 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -6,34 +6,40 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/ """ import logging +import voluptuous as vol + import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) - -_LOGGER = logging.getLogger(__name__) - + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, CONF_CODE, + CONF_NAME) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom' '/archive/0.1.1.zip' '#pyalarmdotcom==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + DEFAULT_NAME = 'Alarm.com' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_CODE): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup an Alarm.com control panel.""" + name = config.get(CONF_NAME) + code = config.get(CONF_CODE) 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([AlarmDotCom(hass, - config.get('name', DEFAULT_NAME), - config.get('code'), - username, - password)]) + add_devices([AlarmDotCom(hass, name, code, username, password)]) # pylint: disable=too-many-arguments, too-many-instance-attributes diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 3bc7b860869..b5bdf478add 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -13,33 +13,31 @@ import homeassistant.components.mqtt as mqtt from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, - CONF_NAME) + CONF_NAME, CONF_CODE) from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['mqtt'] - CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' -CONF_CODE = 'code' -DEFAULT_NAME = "MQTT Alarm" -DEFAULT_DISARM = "DISARM" -DEFAULT_ARM_HOME = "ARM_HOME" -DEFAULT_ARM_AWAY = "ARM_AWAY" +DEFAULT_ARM_AWAY = 'ARM_AWAY' +DEFAULT_ARM_HOME = 'ARM_HOME' +DEFAULT_DISARM = 'DISARM' +DEFAULT_NAME = 'MQTT Alarm' +DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, - vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, - vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, + vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, }) @@ -47,20 +45,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the MQTT platform.""" add_devices([MqttAlarm( hass, - config[CONF_NAME], - config[CONF_STATE_TOPIC], - config[CONF_COMMAND_TOPIC], - config[CONF_QOS], - config[CONF_PAYLOAD_DISARM], - config[CONF_PAYLOAD_ARM_HOME], - config[CONF_PAYLOAD_ARM_AWAY], + config.get(CONF_NAME), + config.get(CONF_STATE_TOPIC), + config.get(CONF_COMMAND_TOPIC), + config.get(CONF_QOS), + config.get(CONF_PAYLOAD_DISARM), + config.get(CONF_PAYLOAD_ARM_HOME), + config.get(CONF_PAYLOAD_ARM_AWAY), config.get(CONF_CODE))]) # pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=abstract-method class MqttAlarm(alarm.AlarmControlPanel): - """Represent a MQTT alarm status.""" + """Representation of a MQTT alarm status.""" def __init__(self, hass, name, state_topic, command_topic, qos, payload_disarm, payload_arm_home, payload_arm_away, code): diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index 2b3facbdb0e..45857f3ef29 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -7,22 +7,40 @@ https://home-assistant.io/components/alarm_control_panel.nx584/ import logging import requests +import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_UNKNOWN) + STATE_UNKNOWN, CONF_NAME, CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pynx584==0.2'] + _LOGGER = logging.getLogger(__name__) +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'NX584' +DEFAULT_PORT = 5007 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup nx584 platform.""" - host = config.get('host', 'localhost:5007') + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + + url = 'http://{}:{}'.format(host, port) try: - add_devices([NX584Alarm(hass, host, config.get('name', 'NX584'))]) + add_devices([NX584Alarm(hass, url, name)]) except requests.exceptions.ConnectionError as ex: _LOGGER.error('Unable to connect to NX584: %s', str(ex)) return False @@ -31,13 +49,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class NX584Alarm(alarm.AlarmControlPanel): """Represents the NX584-based alarm panel.""" - def __init__(self, hass, host, name): + def __init__(self, hass, url, name): """Initalize the nx584 alarm panel.""" from nx584 import client self._hass = hass - self._host = host self._name = name - self._alarm = client.Client('http://%s' % host) + self._url = url + self._alarm = client.Client(self._url) # Do an initial list operation so that we will try to actually # talk to the API and trigger a requests exception for setup_platform() # to catch @@ -66,7 +84,7 @@ class NX584Alarm(alarm.AlarmControlPanel): zones = self._alarm.list_zones() except requests.exceptions.ConnectionError as ex: _LOGGER.error('Unable to connect to %(host)s: %(reason)s', - dict(host=self._host, reason=ex)) + dict(host=self._url, reason=ex)) return STATE_UNKNOWN except IndexError: _LOGGER.error('nx584 reports no partitions') diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index a248df5fc21..82927246ec6 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -6,32 +6,39 @@ https://home-assistant.io/components/alarm_control_panel.simplisafe/ """ import logging +import voluptuous as vol + import homeassistant.components.alarm_control_panel as alarm - +from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN, + CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN, CONF_CODE, CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) +import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['https://github.com/w1ll1am23/simplisafe-python/archive/' '586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#' 'simplisafe-python==0.0.1'] +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'SimpliSafe' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_CODE): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the SimpliSafe platform.""" + name = config.get(CONF_NAME) + code = config.get(CONF_CODE) 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'))]) + add_devices([SimpliSafeAlarm(name, username, password, code)]) # pylint: disable=abstract-method diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index ee1ccfc1bd0..248d575baf7 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -8,7 +8,7 @@ import logging import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.verisure import HUB as hub - +from homeassistant.components.verisure import (CONF_ALARM, CONF_CODE_DIGITS) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Verisure platform.""" alarms = [] - if int(hub.config.get('alarm', '1')): + if int(hub.config.get(CONF_ALARM, 1)): hub.update_alarms() alarms.extend([ VerisureAlarm(value.id) @@ -36,7 +36,7 @@ class VerisureAlarm(alarm.AlarmControlPanel): """Initalize the Verisure alarm panel.""" self._id = device_id self._state = STATE_UNKNOWN - self._digits = int(hub.config.get('code_digits', '4')) + self._digits = hub.config.get(CONF_CODE_DIGITS) self._changed_by = None @property diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index f0073bad838..be455995743 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -98,31 +98,32 @@ class APIEventStream(HomeAssistantView): def stream(): """Stream events to response.""" - self.hass.bus.listen(MATCH_ALL, forward_events) + unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events) - _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) + try: + _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) - # Fire off one message right away to have browsers fire open event - to_write.put(STREAM_PING_PAYLOAD) + # Fire off one message so browsers fire open event right away + to_write.put(STREAM_PING_PAYLOAD) - while True: - try: - payload = to_write.get(timeout=STREAM_PING_INTERVAL) + while True: + try: + payload = to_write.get(timeout=STREAM_PING_INTERVAL) - if payload is stop_obj: + if payload is stop_obj: + break + + msg = "data: {}\n\n".format(payload) + _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), + msg.strip()) + yield msg.encode("UTF-8") + except queue.Empty: + to_write.put(STREAM_PING_PAYLOAD) + except GeneratorExit: break - - msg = "data: {}\n\n".format(payload) - _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), - msg.strip()) - yield msg.encode("UTF-8") - except queue.Empty: - to_write.put(STREAM_PING_PAYLOAD) - except GeneratorExit: - break - - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) - self.hass.bus.remove_listener(MATCH_ALL, forward_events) + finally: + _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) + unsub_stream() return self.Response(stream(), mimetype='text/event-stream') diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index d99043f0c75..863d94033a8 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -4,19 +4,28 @@ Allow to setup simple automation rules via the config file. For more details about this component, please refer to the documentation at https://home-assistant.io/components/automation/ """ +from functools import partial import logging +import os import voluptuous as vol from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant import config as conf_util +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, + SERVICE_TOGGLE) from homeassistant.components import logbook from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import extract_domain_configs, script, condition +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import get_platform +from homeassistant.util.dt import utcnow import homeassistant.helpers.config_validation as cv DOMAIN = 'automation' +ENTITY_ID_FORMAT = DOMAIN + '.{}' DEPENDENCIES = ['group'] @@ -36,6 +45,11 @@ DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND METHOD_TRIGGER = 'trigger' METHOD_IF_ACTION = 'if_action' +ATTR_LAST_TRIGGERED = 'last_triggered' +ATTR_VARIABLES = 'variables' +SERVICE_TRIGGER = 'trigger' +SERVICE_RELOAD = 'reload' + _LOGGER = logging.getLogger(__name__) @@ -88,41 +102,206 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE): vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)), - CONF_CONDITION: _CONDITION_SCHEMA, + vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, }) +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +TRIGGER_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_VARIABLES, default={}): dict, +}) + +RELOAD_SERVICE_SCHEMA = vol.Schema({}) + + +def is_on(hass, entity_id=None): + """ + Return true if specified automation entity_id is on. + + Check all automation if no entity_id specified. + """ + entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) + return any(hass.states.is_state(entity_id, STATE_ON) + for entity_id in entity_ids) + + +def turn_on(hass, entity_id=None): + """Turn on specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +def turn_off(hass, entity_id=None): + """Turn off specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +def toggle(hass, entity_id=None): + """Toggle specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + +def trigger(hass, entity_id=None): + """Trigger specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TRIGGER, data) + + +def reload(hass): + """Reload the automation from config.""" + hass.services.call(DOMAIN, SERVICE_RELOAD) + def setup(hass, config): """Setup the automation.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + success = _process_config(hass, config, component) + + if not success: + return False + + descriptions = conf_util.load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + + def trigger_service_handler(service_call): + """Handle automation triggers.""" + for entity in component.extract_from_service(service_call): + entity.trigger(service_call.data.get(ATTR_VARIABLES)) + + def service_handler(service_call): + """Handle automation service calls.""" + for entity in component.extract_from_service(service_call): + getattr(entity, service_call.service)() + + def reload_service_handler(service_call): + """Remove all automations and load new ones from config.""" + conf = component.prepare_reload() + if conf is None: + return + _process_config(hass, conf, component) + + hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler, + descriptions.get(SERVICE_TRIGGER), + schema=TRIGGER_SERVICE_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler, + descriptions.get(SERVICE_RELOAD), + schema=RELOAD_SERVICE_SCHEMA) + + for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE): + hass.services.register(DOMAIN, service, service_handler, + descriptions.get(service), + schema=SERVICE_SCHEMA) + + return True + + +class AutomationEntity(ToggleEntity): + """Entity to show status of entity.""" + + def __init__(self, name, attach_triggers, cond_func, action): + """Initialize an automation entity.""" + self._name = name + self._attach_triggers = attach_triggers + self._detach_triggers = attach_triggers(self.trigger) + self._cond_func = cond_func + self._action = action + self._enabled = True + self._last_triggered = None + + @property + def name(self): + """Name of the automation.""" + return self._name + + @property + def should_poll(self): + """No polling needed for automation entities.""" + return False + + @property + def state_attributes(self): + """Return the entity state attributes.""" + return { + ATTR_LAST_TRIGGERED: self._last_triggered + } + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._enabled + + def turn_on(self, **kwargs) -> None: + """Turn the entity on.""" + if self._enabled: + return + + self._detach_triggers = self._attach_triggers(self.trigger) + self._enabled = True + self.update_ha_state() + + def turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + if not self._enabled: + return + + self._detach_triggers() + self._detach_triggers = None + self._enabled = False + self.update_ha_state() + + def trigger(self, variables): + """Trigger automation.""" + if self._cond_func(variables): + self._action(variables) + self._last_triggered = utcnow() + self.update_ha_state() + + def remove(self): + """Remove automation from HASS.""" + self.turn_off() + super().remove() + + +def _process_config(hass, config, component): + """Process config and add automations.""" success = False + for config_key in extract_domain_configs(config, DOMAIN): conf = config[config_key] for list_no, config_block in enumerate(conf): - name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key, - list_no)) - success = (_setup_automation(hass, config_block, name, config) or - success) + name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, + list_no) + + action = _get_action(hass, config_block.get(CONF_ACTION, {}), name) + + if CONF_CONDITION in config_block: + cond_func = _process_if(hass, config, config_block) + + if cond_func is None: + continue + else: + def cond_func(variables): + """Condition will always pass.""" + return True + + attach_triggers = partial(_process_trigger, hass, config, + config_block.get(CONF_TRIGGER, []), name) + entity = AutomationEntity(name, attach_triggers, cond_func, action) + component.add_entities((entity,)) + success = True return success -def _setup_automation(hass, config_block, name, config): - """Setup one instance of automation.""" - action = _get_action(hass, config_block.get(CONF_ACTION, {}), name) - - if CONF_CONDITION in config_block: - action = _process_if(hass, config, config_block, action) - - if action is None: - return False - - _process_trigger(hass, config, config_block.get(CONF_TRIGGER, []), name, - action) - return True - - def _get_action(hass, config, name): """Return an action based on a configuration.""" script_obj = script.Script(hass, config, name) @@ -136,7 +315,7 @@ def _get_action(hass, config, name): return action -def _process_if(hass, config, p_config, action): +def _process_if(hass, config, p_config): """Process if checks.""" cond_type = p_config.get(CONF_CONDITION_TYPE, DEFAULT_CONDITION_TYPE).lower() @@ -182,29 +361,43 @@ def _process_if(hass, config, p_config, action): if cond_type == CONDITION_TYPE_AND: def if_action(variables=None): """AND all conditions.""" - if all(check(hass, variables) for check in checks): - action(variables) + return all(check(hass, variables) for check in checks) else: def if_action(variables=None): """OR all conditions.""" - if any(check(hass, variables) for check in checks): - action(variables) + return any(check(hass, variables) for check in checks) return if_action def _process_trigger(hass, config, trigger_configs, name, action): """Setup the triggers.""" + removes = [] + for conf in trigger_configs: platform = _resolve_platform(METHOD_TRIGGER, hass, config, conf.get(CONF_PLATFORM)) if platform is None: continue - if platform.trigger(hass, conf, action): - _LOGGER.info("Initialized rule %s", name) - else: + remove = platform.trigger(hass, conf, action) + + if not remove: _LOGGER.error("Error setting up rule %s", name) + continue + + _LOGGER.info("Initialized rule %s", name) + removes.append(remove) + + if not removes: + return None + + def remove_triggers(): + """Remove attached triggers.""" + for remove in removes: + remove() + + return remove_triggers def _resolve_platform(method, hass, config, platform): diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 6b3160996f3..795dd94a71f 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -39,5 +39,4 @@ def trigger(hass, config, action): }, }) - hass.bus.listen(event_type, handle_event) - return True + return hass.bus.listen(event_type, handle_event) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index e4a6b221e04..6824c32bf07 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -7,13 +7,12 @@ at https://home-assistant.io/components/automation/#mqtt-trigger import voluptuous as vol import homeassistant.components.mqtt as mqtt -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['mqtt'] CONF_TOPIC = 'topic' -CONF_PAYLOAD = 'payload' TRIGGER_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): mqtt.DOMAIN, @@ -24,7 +23,7 @@ TRIGGER_SCHEMA = vol.Schema({ def trigger(hass, config, action): """Listen for state changes based on configuration.""" - topic = config[CONF_TOPIC] + topic = config.get(CONF_TOPIC) payload = config.get(CONF_PAYLOAD) def mqtt_automation_listener(msg_topic, msg_payload, qos): @@ -39,6 +38,4 @@ def trigger(hass, config, action): } }) - mqtt.subscribe(hass, topic, mqtt_automation_listener) - - return True + return mqtt.subscribe(hass, topic, mqtt_automation_listener) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 3a148b0880f..608063b4708 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -63,7 +63,4 @@ def trigger(hass, config, action): action(variables) - track_state_change( - hass, entity_id, state_automation_listener) - - return True + return track_state_change(hass, entity_id, state_automation_listener) diff --git a/homeassistant/components/automation/services.yaml b/homeassistant/components/automation/services.yaml new file mode 100644 index 00000000000..ee22b671eca --- /dev/null +++ b/homeassistant/components/automation/services.yaml @@ -0,0 +1,34 @@ +turn_on: + description: Enable an automation. + + fields: + entity_id: + description: Name of the automation to turn on. + example: 'automation.notify_home' + +turn_off: + description: Disable an automation. + + fields: + entity_id: + description: Name of the automation to turn off. + example: 'automation.notify_home' + +toggle: + description: Toggle an automation. + + fields: + entity_id: + description: Name of the automation to toggle on/off. + example: 'automation.notify_home' + +trigger: + description: Trigger the action of an automation. + + fields: + entity_id: + description: Name of the automation to trigger. + example: 'automation.notify_home' + +reload: + description: Reload the automation configuration. diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 03902c1d6e2..8e0eb5231a5 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -7,8 +7,7 @@ at https://home-assistant.io/components/automation/#state-trigger import voluptuous as vol import homeassistant.util.dt as dt_util -from homeassistant.const import ( - EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, CONF_PLATFORM) +from homeassistant.const import MATCH_ALL, CONF_PLATFORM from homeassistant.helpers.event import track_state_change, track_point_in_time import homeassistant.helpers.config_validation as cv @@ -39,9 +38,13 @@ def trigger(hass, config, action): from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL time_delta = config.get(CONF_FOR) + remove_state_for_cancel = None + remove_state_for_listener = None def state_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" + nonlocal remove_state_for_cancel, remove_state_for_listener + def call_action(): """Call action with right context.""" action({ @@ -60,26 +63,33 @@ def trigger(hass, config, action): def state_for_listener(now): """Fire on state changes after a delay and calls action.""" - hass.bus.remove_listener( - EVENT_STATE_CHANGED, attached_state_for_cancel) + remove_state_for_cancel() call_action() def state_for_cancel_listener(entity, inner_from_s, inner_to_s): """Fire on changes and cancel for listener if changed.""" if inner_to_s.state == to_s.state: return - hass.bus.remove_listener(EVENT_TIME_CHANGED, - attached_state_for_listener) - hass.bus.remove_listener(EVENT_STATE_CHANGED, - attached_state_for_cancel) + remove_state_for_listener() + remove_state_for_cancel() - attached_state_for_listener = track_point_in_time( + remove_state_for_listener = track_point_in_time( hass, state_for_listener, dt_util.utcnow() + time_delta) - attached_state_for_cancel = track_state_change( + remove_state_for_cancel = track_state_change( hass, entity, state_for_cancel_listener) - track_state_change( - hass, entity_id, state_automation_listener, from_state, to_state) + unsub = track_state_change(hass, entity_id, state_automation_listener, + from_state, to_state) - return True + def remove(): + """Remove state listeners.""" + unsub() + # pylint: disable=not-callable + if remove_state_for_cancel is not None: + remove_state_for_cancel() + + if remove_state_for_listener is not None: + remove_state_for_listener() + + return remove diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 7666847575e..991f9b3b385 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -42,8 +42,6 @@ def trigger(hass, config, action): # Do something to call action if event == SUN_EVENT_SUNRISE: - track_sunrise(hass, call_action, offset) + return track_sunrise(hass, call_action, offset) else: - track_sunset(hass, call_action, offset) - - return True + return track_sunset(hass, call_action, offset) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 1cfbf45a24d..0891590a539 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -49,5 +49,4 @@ def trigger(hass, config, action): elif not template_result: already_triggered = False - track_state_change(hass, MATCH_ALL, state_changed_listener) - return True + return track_state_change(hass, MATCH_ALL, state_changed_listener) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index ca80536ea96..0732e2b212c 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -47,7 +47,5 @@ def trigger(hass, config, action): }, }) - track_time_change(hass, time_automation_listener, - hour=hours, minute=minutes, second=seconds) - - return True + return track_time_change(hass, time_automation_listener, + hour=hours, minute=minutes, second=seconds) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 5578bf052c4..ec948684805 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -58,7 +58,5 @@ def trigger(hass, config, action): }, }) - track_state_change( - hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL) - - return True + return track_state_change(hass, entity_id, zone_automation_listener, + MATCH_ALL, MATCH_ALL) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 2f751683265..18e33ffe738 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -27,6 +27,7 @@ SENSOR_CLASSES = [ 'moisture', # Specifically a wetness sensor 'motion', # Motion sensor 'moving', # On means moving, Off means stopped + 'occupancy', # On means occupied, Off means not occupied 'opening', # Door, window, etc. 'power', # Power, over-current, etc 'safety', # Generic on=unsafe, off=safe diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py index f9e192c7984..2419d6f766e 100644 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ b/homeassistant/components/binary_sensor/bloomsky.py @@ -6,32 +6,39 @@ https://home-assistant.io/components/binary_sensor.bloomsky/ """ import logging -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.loader import get_component +import voluptuous as vol -DEPENDENCIES = ["bloomsky"] +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.loader import get_component +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['bloomsky'] # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { - "Rain": "moisture", - "Night": None, + 'Rain': 'moisture', + 'Night': None, } +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the available BloomSky weather binary sensors.""" - logger = logging.getLogger(__name__) bloomsky = get_component('bloomsky') - sensors = config.get('monitored_conditions', SENSOR_TYPES) + # Default needed in case of discovery + sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) for device in bloomsky.BLOOMSKY.devices.values(): for variable in sensors: - if variable in SENSOR_TYPES: - add_devices([BloomSkySensor(bloomsky.BLOOMSKY, - device, - variable)]) - else: - logger.error("Cannot find definition for device: %s", variable) + add_devices([BloomSkySensor(bloomsky.BLOOMSKY, device, variable)]) class BloomSkySensor(BinarySensorDevice): @@ -40,10 +47,10 @@ class BloomSkySensor(BinarySensorDevice): def __init__(self, bs, device, sensor_name): """Initialize a BloomSky binary sensor.""" self._bloomsky = bs - self._device_id = device["DeviceID"] + self._device_id = device['DeviceID'] self._sensor_name = sensor_name - self._name = "{} {}".format(device["DeviceName"], sensor_name) - self._unique_id = "bloomsky_binary_sensor {}".format(self._name) + self._name = '{} {}'.format(device['DeviceName'], sensor_name) + self._unique_id = 'bloomsky_binary_sensor {}'.format(self._name) self.update() @property @@ -71,4 +78,4 @@ class BloomSkySensor(BinarySensorDevice): self._bloomsky.refresh_devices() self._state = \ - self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] + self._bloomsky.devices[self._device_id]['Data'][self._sensor_name] diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index e589506eac7..f56f9cb7a39 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -7,46 +7,50 @@ https://home-assistant.io/components/binary_sensor.command_line/ import logging from datetime import timedelta -from homeassistant.components.binary_sensor import (BinarySensorDevice, - SENSOR_CLASSES) +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA) from homeassistant.components.sensor.command_line import CommandSensorData -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import ( + CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE, + CONF_SENSOR_CLASS, CONF_COMMAND) from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Binary Command Sensor" -DEFAULT_SENSOR_CLASS = None +DEFAULT_NAME = 'Binary Command Sensor' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' -# Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Command Sensor.""" - if config.get('command') is None: - _LOGGER.error('Missing required variable: "command"') - return False + """Setup the Command line Binary Sensor.""" + name = config.get(CONF_NAME) + command = config.get(CONF_COMMAND) + payload_off = config.get(CONF_PAYLOAD_OFF) + payload_on = config.get(CONF_PAYLOAD_ON) + sensor_class = config.get(CONF_SENSOR_CLASS) + value_template = config.get(CONF_VALUE_TEMPLATE) - sensor_class = config.get('sensor_class') - if sensor_class not in SENSOR_CLASSES: - _LOGGER.warning('Unknown sensor class: %s', sensor_class) - sensor_class = DEFAULT_SENSOR_CLASS - - data = CommandSensorData(config.get('command')) + data = CommandSensorData(command) add_devices([CommandBinarySensor( - hass, - data, - config.get('name', DEFAULT_NAME), - sensor_class, - config.get('payload_on', DEFAULT_PAYLOAD_ON), - config.get('payload_off', DEFAULT_PAYLOAD_OFF), - config.get(CONF_VALUE_TEMPLATE) - )]) + hass, data, name, sensor_class, payload_on, payload_off, + value_template)]) # pylint: disable=too-many-arguments, too-many-instance-attributes diff --git a/homeassistant/components/binary_sensor/ecobee.py b/homeassistant/components/binary_sensor/ecobee.py index 09cbfd852e3..93583ff08b1 100644 --- a/homeassistant/components/binary_sensor/ecobee.py +++ b/homeassistant/components/binary_sensor/ecobee.py @@ -2,7 +2,7 @@ Support for Ecobee sensors. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ecobee/ +https://home-assistant.io/components/binary_sensor.ecobee/ """ from homeassistant.components import ecobee from homeassistant.components.binary_sensor import BinarySensorDevice @@ -38,7 +38,7 @@ class EcobeeBinarySensor(BinarySensorDevice): self.sensor_name = sensor_name self.index = sensor_index self._state = None - self._sensor_class = 'motion' + self._sensor_class = 'occupancy' self.update() @property diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py index e02a560ec54..9c37ff7744c 100644 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ b/homeassistant/components/binary_sensor/ffmpeg.py @@ -16,7 +16,7 @@ from homeassistant.config import load_yaml_config_file from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME, ATTR_ENTITY_ID) -REQUIREMENTS = ["ha-ffmpeg==0.9"] +REQUIREMENTS = ["ha-ffmpeg==0.10"] SERVICE_RESTART = 'ffmpeg_restart' diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index 117642c65f1..073f5d7eb6d 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -31,9 +31,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return - return homematic.setup_hmdevice_discovery_helper(HMBinarySensor, - discovery_info, - add_callback_devices) + return homematic.setup_hmdevice_discovery_helper( + HMBinarySensor, + discovery_info, + add_callback_devices + ) class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): @@ -57,44 +59,8 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): 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, diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index a381305691a..fd767bb1528 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -9,45 +9,42 @@ import logging import voluptuous as vol import homeassistant.components.mqtt as mqtt -from homeassistant.components.binary_sensor import (BinarySensorDevice, - SENSOR_CLASSES) -from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE -from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, SENSOR_CLASSES) +from homeassistant.const import ( + CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, + CONF_SENSOR_CLASS) +from homeassistant.components.mqtt import (CONF_STATE_TOPIC, CONF_QOS) from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['mqtt'] - -CONF_SENSOR_CLASS = 'sensor_class' -CONF_PAYLOAD_ON = 'payload_on' -CONF_PAYLOAD_OFF = 'payload_off' - DEFAULT_NAME = 'MQTT Binary sensor' -DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' +DEFAULT_PAYLOAD_ON = 'ON' +DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_SENSOR_CLASS, default=None): vol.Any(vol.In(SENSOR_CLASSES), vol.SetTo(None)), - vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, - vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, }) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Add MQTT binary sensor.""" + """Setup the MQTT binary sensor.""" add_devices([MqttBinarySensor( hass, - config[CONF_NAME], - config[CONF_STATE_TOPIC], - config[CONF_SENSOR_CLASS], - config[CONF_QOS], - config[CONF_PAYLOAD_ON], - config[CONF_PAYLOAD_OFF], + config.get(CONF_NAME), + config.get(CONF_STATE_TOPIC), + config.get(CONF_SENSOR_CLASS), + config.get(CONF_QOS), + config.get(CONF_PAYLOAD_ON), + config.get(CONF_PAYLOAD_OFF), config.get(CONF_VALUE_TEMPLATE) )]) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 9f963b730b5..4dfe4d58b99 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -6,12 +6,12 @@ https://home-assistant.io/components/binary_sensor.nest/ """ import voluptuous as vol -import homeassistant.components.nest as nest -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.sensor.nest import NestSensor -from homeassistant.const import ( - CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS -) +from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) +import homeassistant.components.nest as nest +import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['nest'] BINARY_TYPES = ['fan', @@ -25,11 +25,11 @@ BINARY_TYPES = ['fan', 'hvac_emer_heat_state', 'online'] -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): nest.DOMAIN, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Required(CONF_MONITORED_CONDITIONS): [vol.In(BINARY_TYPES)], + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(BINARY_TYPES)]), }) diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py index 25c922ca20c..6763eaafa55 100644 --- a/homeassistant/components/binary_sensor/octoprint.py +++ b/homeassistant/components/binary_sensor/octoprint.py @@ -5,45 +5,56 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.octoprint/ """ import logging + import requests +import voluptuous as vol -from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ( + CONF_NAME, STATE_ON, STATE_OFF, CONF_MONITORED_CONDITIONS) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.loader import get_component +import homeassistant.helpers.config_validation as cv -DEPENDENCIES = ["octoprint"] + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['octoprint'] + +DEFAULT_NAME = 'OctoPrint' SENSOR_TYPES = { # API Endpoint, Group, Key, unit - "Printing": ["printer", "state", "printing", None], - "Printing Error": ["printer", "state", "error", None] + 'Printing': ['printer', 'state', 'printing', None], + 'Printing Error': ['printer', 'state', 'error', None] } -_LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the available OctoPrint binary sensors.""" octoprint = get_component('octoprint') - name = config.get(CONF_NAME, "OctoPrint") - monitored_conditions = config.get("monitored_conditions", + name = config.get(CONF_NAME) + monitored_conditions = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES.keys()) devices = [] for octo_type in monitored_conditions: - if octo_type in SENSOR_TYPES: - new_sensor = OctoPrintBinarySensor(octoprint.OCTOPRINT, - octo_type, - SENSOR_TYPES[octo_type][2], - name, - SENSOR_TYPES[octo_type][3], - SENSOR_TYPES[octo_type][0], - SENSOR_TYPES[octo_type][1], - "flags") - devices.append(new_sensor) - else: - _LOGGER.error("Unknown OctoPrint sensor type: %s", octo_type) + new_sensor = OctoPrintBinarySensor(octoprint.OCTOPRINT, + octo_type, + SENSOR_TYPES[octo_type][2], + name, + SENSOR_TYPES[octo_type][3], + SENSOR_TYPES[octo_type][0], + SENSOR_TYPES[octo_type][1], + 'flags') + devices.append(new_sensor) add_devices(devices) @@ -52,14 +63,14 @@ class OctoPrintBinarySensor(BinarySensorDevice): """Representation an OctoPrint binary sensor.""" # pylint: disable=too-many-arguments - def __init__(self, api, condition, sensor_type, sensor_name, - unit, endpoint, group, tool=None): + def __init__(self, api, condition, sensor_type, sensor_name, unit, + endpoint, group, tool=None): """Initialize a new OctoPrint sensor.""" self.sensor_name = sensor_name if tool is None: - self._name = sensor_name + ' ' + condition + self._name = '{} {}'.format(sensor_name, condition) else: - self._name = sensor_name + ' ' + condition + self._name = '{} {}'.format(sensor_name, condition) self.sensor_type = sensor_type self.api = api self._state = False diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 4a6e48ca5a3..71666b91d06 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -13,12 +13,15 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.sensor.rest import RestData from homeassistant.const import ( CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, - CONF_SENSOR_CLASS) + CONF_SENSOR_CLASS, CONF_VERIFY_SSL) from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +_LOGGER = logging.getLogger(__name__) + DEFAULT_METHOD = 'GET' DEFAULT_NAME = 'REST Binary Sensor' +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, @@ -27,10 +30,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, }) -_LOGGER = logging.getLogger(__name__) - # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): @@ -39,7 +41,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): resource = config.get(CONF_RESOURCE) method = config.get(CONF_METHOD) payload = config.get(CONF_PAYLOAD) - verify_ssl = config.get('verify_ssl', True) + verify_ssl = config.get(CONF_VERIFY_SSL) sensor_class = config.get(CONF_SENSOR_CLASS) value_template = config.get(CONF_VALUE_TEMPLATE) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index e87594e625c..e0b748bbbbe 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -5,22 +5,22 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.template/ """ import logging + import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, + SENSOR_CLASSES_SCHEMA) +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, MATCH_ALL, CONF_VALUE_TEMPLATE, + CONF_SENSOR_CLASS, CONF_SENSORS) +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.event import track_state_change import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import (BinarySensorDevice, - ENTITY_ID_FORMAT, - PLATFORM_SCHEMA, - SENSOR_CLASSES_SCHEMA) - -from homeassistant.const import (ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, MATCH_ALL, - CONF_VALUE_TEMPLATE, CONF_SENSOR_CLASS) -from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity import generate_entity_id -from homeassistant.helpers import template -from homeassistant.helpers.event import track_state_change - -CONF_SENSORS = 'sensors' +_LOGGER = logging.getLogger(__name__) SENSOR_SCHEMA = vol.Schema({ vol.Required(CONF_VALUE_TEMPLATE): cv.template, @@ -33,15 +33,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), }) -_LOGGER = logging.getLogger(__name__) - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup template binary sensors.""" sensors = [] for device, device_config in config[CONF_SENSORS].items(): - value_template = device_config[CONF_VALUE_TEMPLATE] entity_ids = device_config[ATTR_ENTITY_ID] friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) @@ -85,8 +82,7 @@ class BinarySensorTemplate(BinarySensorDevice): """Called when the target device changes state.""" self.update_ha_state(True) - track_state_change(hass, entity_ids, - template_bsensor_state_listener) + track_state_change(hass, entity_ids, template_bsensor_state_listener) @property def name(self): @@ -111,8 +107,8 @@ class BinarySensorTemplate(BinarySensorDevice): def update(self): """Get the latest data and update the state.""" try: - self._state = template.render(self.hass, - self._template).lower() == 'true' + self._state = template.render( + self.hass, self._template).lower() == 'true' except TemplateError as ex: if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py new file mode 100644 index 00000000000..940f80a757b --- /dev/null +++ b/homeassistant/components/binary_sensor/trend.py @@ -0,0 +1,145 @@ +""" +A sensor that monitors trands in other components. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.template/ +""" +import logging +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + SENSOR_CLASSES_SCHEMA) +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_ENTITY_ID, + CONF_SENSOR_CLASS, + STATE_UNKNOWN,) +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.event import track_state_change + +_LOGGER = logging.getLogger(__name__) +CONF_SENSORS = 'sensors' +CONF_ATTRIBUTE = 'attribute' +CONF_INVERT = 'invert' + +SENSOR_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_INVERT, default=False): cv.boolean, + vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA + +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the template sensors.""" + sensors = [] + + for device, device_config in config[CONF_SENSORS].items(): + entity_id = device_config[ATTR_ENTITY_ID] + attribute = device_config.get(CONF_ATTRIBUTE) + friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) + sensor_class = device_config[CONF_SENSOR_CLASS] + invert = device_config[CONF_INVERT] + + sensors.append( + SensorTrend( + hass, + device, + friendly_name, + entity_id, + attribute, + sensor_class, + invert) + ) + if not sensors: + _LOGGER.error("No sensors added") + return False + add_devices(sensors) + return True + + +class SensorTrend(BinarySensorDevice): + """Representation of a Template Sensor.""" + + # pylint: disable=too-many-arguments, too-many-instance-attributes + def __init__(self, hass, device_id, friendly_name, + target_entity, attribute, sensor_class, invert): + """Initialize the sensor.""" + self._hass = hass + self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, + hass=hass) + self._name = friendly_name + self._target_entity = target_entity + self._attribute = attribute + self._sensor_class = sensor_class + self._invert = invert + self._state = None + self.from_state = None + self.to_state = None + + self.update() + + def template_sensor_state_listener(entity, old_state, new_state): + """Called when the target device changes state.""" + self.from_state = old_state + self.to_state = new_state + self.update_ha_state(True) + + track_state_change(hass, target_entity, + template_sensor_state_listener) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state + + @property + def sensor_class(self): + """Return the sensor class of the sensor.""" + return self._sensor_class + + @property + def should_poll(self): + """No polling needed.""" + return False + + def update(self): + """Get the latest data and update the states.""" + if self.from_state is None or self.to_state is None: + return + if (self.from_state.state == STATE_UNKNOWN or + self.to_state.state == STATE_UNKNOWN): + return + try: + if self._attribute: + from_value = float( + self.from_state.attributes.get(self._attribute)) + to_value = float( + self.to_state.attributes.get(self._attribute)) + else: + from_value = float(self.from_state.state) + to_value = float(self.to_state.state) + + self._state = to_value > from_value + if self._invert: + self._state = not self._state + + except (ValueError, TypeError) as ex: + self._state = None + _LOGGER.error(ex) diff --git a/homeassistant/components/binary_sensor/zigbee.py b/homeassistant/components/binary_sensor/zigbee.py index 7e4139d4680..2eb508304d4 100644 --- a/homeassistant/components/binary_sensor/zigbee.py +++ b/homeassistant/components/binary_sensor/zigbee.py @@ -4,18 +4,27 @@ Contains functionality to use a ZigBee device as a binary sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.zigbee/ """ +import voluptuous as vol + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.zigbee import ( - ZigBeeDigitalIn, ZigBeeDigitalInConfig) + ZigBeeDigitalIn, ZigBeeDigitalInConfig, PLATFORM_SCHEMA) -DEPENDENCIES = ["zigbee"] +CONF_ON_STATE = 'on_state' + +DEFAULT_ON_STATE = 'high' +DEPENDENCIES = ['zigbee'] + +STATES = ['high', 'low'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ON_STATE): vol.In(STATES), +}) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the ZigBee binary sensor platform.""" - add_entities([ - ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config)) - ]) + add_devices([ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))]) class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorDevice): diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py index b881dcb9526..e610082951b 100644 --- a/homeassistant/components/bloomsky.py +++ b/homeassistant/components/bloomsky.py @@ -8,30 +8,34 @@ import logging from datetime import timedelta import requests +import voluptuous as vol from homeassistant.const import CONF_API_KEY -from homeassistant.helpers import validate_config, discovery +from homeassistant.helpers import discovery from homeassistant.util import Throttle - -DOMAIN = "bloomsky" -BLOOMSKY = None +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +BLOOMSKY = None +BLOOMSKY_TYPE = ['camera', 'binary_sensor', 'sensor'] + +DOMAIN = 'bloomsky' + # The BloomSky only updates every 5-8 minutes as per the API spec so there's # no point in polling the API more frequently MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + # pylint: disable=unused-argument,too-few-public-methods def setup(hass, config): """Setup BloomSky component.""" - if not validate_config( - config, - {DOMAIN: [CONF_API_KEY]}, - _LOGGER): - return False - api_key = config[DOMAIN][CONF_API_KEY] global BLOOMSKY @@ -40,7 +44,7 @@ def setup(hass, config): except RuntimeError: return False - for component in 'camera', 'binary_sensor', 'sensor': + for component in BLOOMSKY_TYPE: discovery.load_platform(hass, component, DOMAIN, {}, config) return True @@ -50,19 +54,19 @@ class BloomSky(object): """Handle all communication with the BloomSky API.""" # API documentation at http://weatherlution.com/bloomsky-api/ - API_URL = "https://api.bloomsky.com/api/skydata" + API_URL = 'https://api.bloomsky.com/api/skydata' def __init__(self, api_key): """Initialize the BookSky.""" self._api_key = api_key self.devices = {} - _LOGGER.debug("Initial bloomsky device load...") + _LOGGER.debug("Initial BloomSky device load...") self.refresh_devices() @Throttle(MIN_TIME_BETWEEN_UPDATES) def refresh_devices(self): """Use the API to retreive a list of devices.""" - _LOGGER.debug("Fetching bloomsky update") + _LOGGER.debug("Fetching BloomSky update") response = requests.get(self.API_URL, headers={"Authorization": self._api_key}, timeout=10) @@ -73,5 +77,5 @@ class BloomSky(object): return # Create dictionary keyed off of the device unique id self.devices.update({ - device["DeviceID"]: device for device in response.json() + device['DeviceID']: device for device in response.json() }) diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 23d6874cd81..af21537e3c3 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -5,16 +5,14 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.ffmpeg/ """ import logging -from contextlib import closing import voluptuous as vol from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) -from homeassistant.components.camera.mjpeg import extract_image_from_mjpeg import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME -REQUIREMENTS = ['ha-ffmpeg==0.9'] +REQUIREMENTS = ['ha-ffmpeg==0.10'] _LOGGER = logging.getLogger(__name__) @@ -49,22 +47,20 @@ class FFmpegCamera(Camera): self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._ffmpeg_bin = config.get(CONF_FFMPEG_BIN) - def _ffmpeg_stream(self): - """Return a FFmpeg process object.""" - from haffmpeg import CameraMjpeg - - ffmpeg = CameraMjpeg(self._ffmpeg_bin) - ffmpeg.open_camera(self._input, extra_cmd=self._extra_arguments) - return ffmpeg - def camera_image(self): """Return a still image response from the camera.""" - with closing(self._ffmpeg_stream()) as stream: - return extract_image_from_mjpeg(stream) + from haffmpeg import ImageSingle, IMAGE_JPEG + ffmpeg = ImageSingle(self._ffmpeg_bin) + + return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG, + extra_cmd=self._extra_arguments) def mjpeg_stream(self, response): """Generate an HTTP MJPEG stream from the camera.""" - stream = self._ffmpeg_stream() + from haffmpeg import CameraMjpeg + + stream = CameraMjpeg(self._ffmpeg_bin) + stream.open_camera(self._input, extra_cmd=self._extra_arguments) return response( stream, mimetype='multipart/x-mixed-replace;boundary=ffserver', diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index e4215bcea85..726ed4f674c 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.helpers.entity_component import EntityComponent from homeassistant.config import load_yaml_config_file -import homeassistant.util as util from homeassistant.util.temperature import convert as convert_temperature from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa @@ -44,6 +43,8 @@ STATE_FAN_ONLY = "fan_only" ATTR_CURRENT_TEMPERATURE = "current_temperature" ATTR_MAX_TEMP = "max_temp" ATTR_MIN_TEMP = "min_temp" +ATTR_TARGET_TEMP_HIGH = "target_temp_high" +ATTR_TARGET_TEMP_LOW = "target_temp_low" ATTR_AWAY_MODE = "away_mode" ATTR_AUX_HEAT = "aux_heat" ATTR_FAN_MODE = "fan_mode" @@ -68,8 +69,10 @@ SET_AUX_HEAT_SCHEMA = vol.Schema({ vol.Required(ATTR_AUX_HEAT): cv.boolean, }) SET_TEMPERATURE_SCHEMA = vol.Schema({ + vol.Exclusive(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float), vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), }) SET_FAN_MODE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -113,14 +116,19 @@ def set_aux_heat(hass, aux_heat, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) -def set_temperature(hass, temperature, entity_id=None): +def set_temperature(hass, temperature=None, entity_id=None, + target_temp_high=None, target_temp_low=None): """Set new target temperature.""" - data = {ATTR_TEMPERATURE: temperature} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data) + kwargs = { + key: value for key, value in [ + (ATTR_TEMPERATURE, temperature), + (ATTR_TARGET_TEMP_HIGH, target_temp_high), + (ATTR_TARGET_TEMP_LOW, target_temp_low), + (ATTR_ENTITY_ID, entity_id), + ] if value is not None + } + _LOGGER.debug("set_temperature start data=%s", kwargs) + hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) def set_humidity(hass, humidity, entity_id=None): @@ -227,20 +235,9 @@ def setup(hass, config): def temperature_set_service(service): """Set temperature on the target climate devices.""" target_climate = component.extract_from_service(service) - - temperature = util.convert( - service.data.get(ATTR_TEMPERATURE), float) - - if temperature is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE) - return - + kwargs = service.data for climate in target_climate: - climate.set_temperature(convert_temperature( - temperature, hass.config.units.temperature_unit, - climate.unit_of_measurement)) + climate.set_temperature(**kwargs) if climate.should_poll: climate.update_ha_state(True) @@ -351,7 +348,7 @@ class ClimateDevice(Entity): @property def state(self): """Return the current state.""" - return self.target_temperature or STATE_UNKNOWN + return self.current_operation or STATE_UNKNOWN @property def state_attributes(self): @@ -364,6 +361,12 @@ class ClimateDevice(Entity): ATTR_TEMPERATURE: self._convert_for_display(self.target_temperature), } + target_temp_high = self.target_temperature_high + if target_temp_high is not None: + data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( + self.target_temperature_high) + data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( + self.target_temperature_low) humidity = self.target_humidity if humidity is not None: @@ -432,6 +435,16 @@ class ClimateDevice(Entity): """Return the temperature we try to reach.""" return None + @property + def target_temperature_high(self): + """Return the highbound target temperature we try to reach.""" + return None + + @property + def target_temperature_low(self): + """Return the lowbound target temperature we try to reach.""" + return None + @property def is_away_mode_on(self): """Return true if away mode is on.""" @@ -462,7 +475,7 @@ class ClimateDevice(Entity): """List of available swing modes.""" return None - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" raise NotImplementedError() diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 340cc29f582..cb85a153cc8 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -4,17 +4,20 @@ Demo platform that offers a fake climate device. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.climate import ClimateDevice -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.components.climate import ( + ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Demo climate devices.""" add_devices([ DemoClimate("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low", - None, None, "Auto", "Heat", None), + None, None, "Auto", "heat", None, None, None), DemoClimate("Hvac", 21, TEMP_CELSIUS, True, 22, "On High", - 67, 54, "Off", "Cool", False), + 67, 54, "Off", "cool", False, None, None), + DemoClimate("Ecobee", 23, TEMP_CELSIUS, None, 23, "Auto Low", + None, None, "Auto", "auto", None, 24, 21) ]) @@ -26,7 +29,7 @@ class DemoClimate(ClimateDevice): def __init__(self, name, target_temperature, unit_of_measurement, away, current_temperature, current_fan_mode, target_humidity, current_humidity, current_swing_mode, - current_operation, aux): + current_operation, aux, target_temp_high, target_temp_low): """Initialize the climate device.""" self._name = name self._target_temperature = target_temperature @@ -40,8 +43,10 @@ class DemoClimate(ClimateDevice): self._aux = aux self._current_swing_mode = current_swing_mode self._fan_list = ["On Low", "On High", "Auto Low", "Auto High", "Off"] - self._operation_list = ["Heat", "Cool", "Auto Changeover", "Off"] + self._operation_list = ["heat", "cool", "auto", "off"] self._swing_list = ["Auto", "1", "2", "3", "Off"] + self._target_temperature_high = target_temp_high + self._target_temperature_low = target_temp_low @property def should_poll(self): @@ -68,6 +73,16 @@ class DemoClimate(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temperature + @property + def target_temperature_high(self): + """Return the highbound target temperature we try to reach.""" + return self._target_temperature_high + + @property + def target_temperature_low(self): + """Return the lowbound target temperature we try to reach.""" + return self._target_temperature_low + @property def current_humidity(self): """Return the current humidity.""" @@ -108,9 +123,14 @@ class DemoClimate(ClimateDevice): """List of available fan modes.""" return self._fan_list - def set_temperature(self, temperature): - """Set new target temperature.""" - self._target_temperature = temperature + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + if kwargs.get(ATTR_TEMPERATURE) is not None: + self._target_temperature = kwargs.get(ATTR_TEMPERATURE) + if kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None and \ + kwargs.get(ATTR_TARGET_TEMP_LOW) is not None: + self._target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) self.update_ha_state() def set_humidity(self, humidity): diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 2417a8562ce..5d78aeb8597 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -6,23 +6,27 @@ https://home-assistant.io/components/climate.ecobee/ """ import logging from os import path + import voluptuous as vol from homeassistant.components import ecobee from homeassistant.components.climate import ( - DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice) + DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv -DEPENDENCIES = ['ecobee'] -_LOGGER = logging.getLogger(__name__) -ECOBEE_CONFIG_FILE = 'ecobee.conf' _CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +ATTR_FAN_MIN_ON_TIME = 'fan_min_on_time' + +DEPENDENCIES = ['ecobee'] + +SERVICE_SET_FAN_MIN_ON_TIME = 'ecobee_set_fan_min_on_time' -ATTR_FAN_MIN_ON_TIME = "fan_min_on_time" -SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time" SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), @@ -142,7 +146,11 @@ class Thermostat(ClimateDevice): @property def current_operation(self): """Return current operation.""" - return self.operation_mode + if self.operation_mode == 'auxHeatOnly' or \ + self.operation_mode == 'heatPump': + return STATE_HEAT + else: + return self.operation_mode @property def operation_list(self): @@ -211,11 +219,17 @@ class Thermostat(ClimateDevice): """Turn away off.""" self.data.ecobee.resume_program(self.thermostat_index) - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = int(temperature) - low_temp = temperature - 1 - high_temp = temperature + 1 + if kwargs.get(ATTR_TEMPERATURE) is not None: + temperature = kwargs.get(ATTR_TEMPERATURE) + low_temp = temperature - 1 + high_temp = temperature + 1 + if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \ + kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None: + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if self.hold_temp: self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp, high_temp, "indefinite") diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 01114972811..646bf7f2aa8 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -7,14 +7,12 @@ https://home-assistant.io/components/climate.eq3btsmart/ import logging from homeassistant.components.climate import ClimateDevice -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE from homeassistant.util.temperature import convert REQUIREMENTS = ['bluepy_devices==0.2.0'] CONF_MAC = 'mac' -CONF_DEVICES = 'devices' -CONF_ID = 'id' _LOGGER = logging.getLogger(__name__) @@ -28,7 +26,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices.append(EQ3BTSmartThermostat(mac, name)) add_devices(devices) - return True # pylint: disable=too-many-instance-attributes, import-error, abstract-method @@ -63,8 +60,11 @@ class EQ3BTSmartThermostat(ClimateDevice): """Return the temperature we try to reach.""" return self._thermostat.target_temperature - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return self._thermostat.target_temperature = temperature @property diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 11e6707ad47..fd85d7fd46b 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -11,7 +11,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components import switch from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE) from homeassistant.helpers import condition from homeassistant.helpers.event import track_state_change @@ -123,8 +124,11 @@ class GenericThermostat(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temp - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return self._target_temp = temperature self._control_heating() self.update_ha_state() diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index c7dd5534f57..941f211c411 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -10,7 +10,7 @@ https://home-assistant.io/components/climate.heatmiser/ import logging from homeassistant.components.climate import ClimateDevice -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE CONF_IPADDRESS = 'ipaddress' CONF_PORT = 'port' @@ -98,16 +98,18 @@ class HeatmiserV3Thermostat(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temperature - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = int(temperature) + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return self.heatmiser.hmSendAddress( self._id, 18, temperature, 1, self.serport) - self._target_temperature = int(temperature) + self._target_temperature = temperature def update(self): """Get the latest data.""" diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py index e51ad5e67a5..7e0b4fd6450 100644 --- a/homeassistant/components/climate/homematic.py +++ b/homeassistant/components/climate/homematic.py @@ -8,7 +8,7 @@ import logging import homeassistant.components.homematic as homematic from homeassistant.components.climate import ClimateDevice, STATE_AUTO from homeassistant.util.temperature import convert -from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN +from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE DEPENDENCIES = ['homematic'] @@ -29,9 +29,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return - return homematic.setup_hmdevice_discovery_helper(HMThermostat, - discovery_info, - add_callback_devices) + return homematic.setup_hmdevice_discovery_helper( + HMThermostat, + discovery_info, + add_callback_devices + ) # pylint: disable=abstract-method @@ -90,10 +92,13 @@ class HMThermostat(homematic.HMDevice, ClimateDevice): return None return self._data.get('SET_TEMPERATURE', None) - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) if not self.available: return None + if temperature is None: + return self._hmdevice.set_temperature(temperature) def set_operation_mode(self, operation_mode): @@ -113,26 +118,8 @@ class HMThermostat(homematic.HMDevice, ClimateDevice): """Return the maximum temperature - 30.5 means on.""" return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement) - def _check_hm_to_ha_object(self): - """Check if possible to use the Homematic object as this HA type.""" - from pyhomematic.devicetypes.thermostats import HMThermostat\ - as pyHMThermostat - - # 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 isinstance(self._hmdevice, pyHMThermostat): - return True - - _LOGGER.critical("This %s can't be use as thermostat", self._name) - return False - def _init_data_struct(self): """Generate a data dict (self._data) from the Homematic metadata.""" - super()._init_data_struct() - # Add state to data dict self._data.update({"CONTROL_MODE": STATE_UNKNOWN, "SET_TEMPERATURE": STATE_UNKNOWN, diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 1efce2b95de..001bf8806ac 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -9,7 +9,8 @@ import socket from homeassistant.components.climate import ClimateDevice from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, + ATTR_TEMPERATURE) REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.2.1'] @@ -132,8 +133,11 @@ class RoundThermostat(ClimateDevice): return None return self._target_temperature - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return self.device.set_temperature(self._name, temperature) @property @@ -234,8 +238,11 @@ class HoneywellUSThermostat(ClimateDevice): """Return current operation ie. heat, cool, idle.""" return getattr(self._device, 'system_mode', None) - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return import somecomfort try: if self._device.system_mode == 'cool': diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py index 10f02d80cc7..a9d4358a059 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/knx/ import logging from homeassistant.components.climate import ClimateDevice -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.components.knx import ( KNXConfig, KNXMultiAddressDevice) @@ -71,8 +71,11 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): return knx2_to_float(self.value("setpoint")) - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return from knxip.conversion import float_to_knx2 self.set_value("setpoint", float_to_knx2(temperature)) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 39746bff601..f55d1d856eb 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -8,13 +8,13 @@ import voluptuous as vol import homeassistant.components.nest as nest from homeassistant.components.climate import ( - STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice) -from homeassistant.const import TEMP_CELSIUS, CONF_PLATFORM, CONF_SCAN_INTERVAL + STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + TEMP_CELSIUS, CONF_SCAN_INTERVAL, ATTR_TEMPERATURE) DEPENDENCIES = ['nest'] -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): nest.DOMAIN, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1)), }) @@ -132,8 +132,11 @@ class NestThermostat(ClimateDevice): """Return if away mode is on.""" return self.structure.away - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return if self.device.mode == 'range': if self.target_temperature == self.target_temperature_low: temperature = (temperature, self.target_temperature_high) diff --git a/homeassistant/components/climate/proliphix.py b/homeassistant/components/climate/proliphix.py index c6e8ed69617..fa2230fba55 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/climate.proliphix/ from homeassistant.components.climate import ( STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT) + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) REQUIREMENTS = ['proliphix==0.3.1'] @@ -85,6 +85,9 @@ class ProliphixThermostat(ClimateDevice): elif state == 6: return STATE_COOL - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return self._pdp.setback = temperature diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index deee3d53f3f..90611ce20b2 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -11,7 +11,7 @@ from urllib.error import URLError from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF, ClimateDevice) -from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT +from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE REQUIREMENTS = ['radiotherm==1.2'] HOLD_TEMP = 'hold_temp' @@ -107,8 +107,11 @@ class RadioThermostat(ClimateDevice): else: self._current_operation = STATE_IDLE - def set_temperature(self, temperature): + def set_temperature(self, **kwargs): """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return if self._current_operation == STATE_COOL: self.device.t_cool = temperature elif self._current_operation == STATE_HEAT: diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 530e3ea028f..0ba85105c18 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -12,7 +12,8 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.zwave import ( ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity) from homeassistant.components import zwave -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -23,6 +24,10 @@ REMOTEC = 0x5254 REMOTEC_ZXT_120 = 0x8377 REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) +HORSTMANN = 0x0059 +HORSTMANN_HRT4_ZW = 0x3 +HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW) + COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31 COMMAND_CLASS_THERMOSTAT_MODE = 0x40 COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43 @@ -30,9 +35,11 @@ COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44 COMMAND_CLASS_CONFIGURATION = 0x70 WORKAROUND_ZXT_120 = 'zxt_120' +WORKAROUND_HRT4_ZW = 'hrt4_zw' DEVICE_MAPPINGS = { - REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 + REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120, + HORSTMANN_HRT4_ZW_THERMOSTAT: WORKAROUND_HRT4_ZW } SET_TEMP_TO_INDEX = { @@ -63,6 +70,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]] value = node.values[discovery_info[ATTR_VALUE_ID]] value.set_change_verified(False) + if value.index != 1: # Only add 1 device + return add_devices([ZWaveClimate(value, temp_unit)]) _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", discovery_info, zwave.NETWORK) @@ -88,8 +97,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._current_swing_mode = None self._swing_list = None self._unit = temp_unit + self._index_operation = None _LOGGER.debug("temp_unit is %s", self._unit) self._zxt_120 = None + self._hrt4_zw = None self.update_properties() # register listener dispatcher.connect( @@ -99,12 +110,15 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): 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_ZXT_120: _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat" " workaround") self._zxt_120 = 1 + if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_HRT4_ZW: + _LOGGER.debug("Horstmann HRT4-ZW Zwave Thermostat" + " workaround") + self._hrt4_zw = 1 def value_changed(self, value): """Called when a value has changed on the network.""" @@ -120,6 +134,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): for value in self._node.get_values( class_id=COMMAND_CLASS_THERMOSTAT_MODE).values(): self._current_operation = value.data + self._index_operation = SET_TEMP_TO_INDEX.get( + self._current_operation) self._operation_list = list(value.data_items) _LOGGER.debug("self._operation_list=%s", self._operation_list) _LOGGER.debug("self._current_operation=%s", @@ -153,11 +169,14 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values(): if self.current_operation is not None and \ self.current_operation != 'Off': - if SET_TEMP_TO_INDEX.get(self._current_operation) \ - != value.index: + if self._index_operation != value.index: continue if self._zxt_120: - continue + break + self._target_temperature = int(value.data) + break + _LOGGER.debug("Device can't set setpoint based on operation mode." + " Defaulting to index=1") self._target_temperature = int(value.data) @property @@ -215,28 +234,48 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Return the temperature we try to reach.""" return self._target_temperature - def set_temperature(self, temperature): +# pylint: disable=too-many-branches, too-many-statements + def set_temperature(self, **kwargs): """Set new target temperature.""" + if kwargs.get(ATTR_TEMPERATURE) is not None: + temperature = kwargs.get(ATTR_TEMPERATURE) + else: + return + for value in self._node.get_values( class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values(): if self.current_operation is not None: - if SET_TEMP_TO_INDEX.get(self._current_operation) \ - != value.index: + if self._hrt4_zw and self.current_operation == 'Off': + # HRT4-ZW can change setpoint when off. + value.data = int(temperature) + if self._index_operation != value.index: continue - _LOGGER.debug("SET_TEMP_TO_INDEX=%s and" + _LOGGER.debug("self._index_operation=%s and" " self._current_operation=%s", - SET_TEMP_TO_INDEX.get(self._current_operation), + self._index_operation, self._current_operation) if self._zxt_120: + _LOGGER.debug("zxt_120: Setting new setpoint for %s, " + " operation=%s, temp=%s", + self._index_operation, + self._current_operation, temperature) # ZXT-120 does not support get setpoint self._target_temperature = temperature # ZXT-120 responds only to whole int - value.data = int(round(temperature, 0)) + value.data = round(temperature, 0) + break else: - value.data = int(temperature) - break + _LOGGER.debug("Setting new setpoint for %s, " + "operation=%s, temp=%s", + self._index_operation, + self._current_operation, temperature) + value.data = temperature + break else: - value.data = int(temperature) + _LOGGER.debug("Setting new setpoint for no known " + "operation mode. Index=1 and " + "temperature=%s", temperature) + value.data = temperature break def set_fan_mode(self, fan): diff --git a/homeassistant/components/cover/command_line.py b/homeassistant/components/cover/command_line.py index c2c8050f09f..0a1da9d7a20 100644 --- a/homeassistant/components/cover/command_line.py +++ b/homeassistant/components/cover/command_line.py @@ -7,29 +7,54 @@ https://home-assistant.io/components/cover.command_line/ import logging import subprocess -from homeassistant.components.cover import CoverDevice -from homeassistant.const import CONF_VALUE_TEMPLATE +import voluptuous as vol + +from homeassistant.components.cover import (CoverDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, CONF_COMMAND_STATE, + CONF_COMMAND_STOP, CONF_COVERS, CONF_VALUE_TEMPLATE, CONF_FRIENDLY_NAME) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) +COVER_SCHEMA = vol.Schema({ + vol.Optional(CONF_COMMAND_CLOSE, default='true'): cv.string, + vol.Optional(CONF_COMMAND_OPEN, default='true'): cv.string, + vol.Optional(CONF_COMMAND_STATE): cv.string, + vol.Optional(CONF_COMMAND_STOP, default='true'): cv.string, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE, default='{{ value }}'): cv.template, +}) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup cover controlled by shell commands.""" - covers = config.get('covers', {}) - devices = [] + devices = config.get(CONF_COVERS, {}) + covers = [] - for dev_name, properties in covers.items(): - devices.append( + for device_name, device_config in devices.items(): + covers.append( CommandCover( hass, - properties.get('name', dev_name), - properties.get('opencmd', 'true'), - properties.get('closecmd', 'true'), - properties.get('stopcmd', 'true'), - properties.get('statecmd', False), - properties.get(CONF_VALUE_TEMPLATE, '{{ value }}'))) - add_devices_callback(devices) + device_config.get(CONF_FRIENDLY_NAME, device_name), + device_config.get(CONF_COMMAND_OPEN), + device_config.get(CONF_COMMAND_CLOSE), + device_config.get(CONF_COMMAND_STOP), + device_config.get(CONF_COMMAND_STATE), + device_config.get(CONF_VALUE_TEMPLATE), + ) + ) + + if not covers: + _LOGGER.error("No covers added") + return False + + add_devices(covers) # pylint: disable=too-many-arguments, too-many-instance-attributes diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py index 1f1c666f339..acddfcf7c73 100644 --- a/homeassistant/components/cover/demo.py +++ b/homeassistant/components/cover/demo.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ from homeassistant.components.cover import CoverDevice -from homeassistant.const import EVENT_TIME_CHANGED from homeassistant.helpers.event import track_utc_time_change @@ -32,8 +31,8 @@ class DemoCover(CoverDevice): self._tilt_position = tilt_position self._closing = True self._closing_tilt = True - self._listener_cover = None - self._listener_cover_tilt = None + self._unsub_listener_cover = None + self._unsub_listener_cover_tilt = None @property def name(self): @@ -120,10 +119,9 @@ class DemoCover(CoverDevice): """Stop the cover.""" if self._position is None: return - if self._listener_cover is not None: - self.hass.bus.remove_listener(EVENT_TIME_CHANGED, - self._listener_cover) - self._listener_cover = None + if self._unsub_listener_cover is not None: + self._unsub_listener_cover() + self._unsub_listener_cover = None self._set_position = None def stop_cover_tilt(self, **kwargs): @@ -131,16 +129,15 @@ class DemoCover(CoverDevice): if self._tilt_position is None: return - if self._listener_cover_tilt is not None: - self.hass.bus.remove_listener(EVENT_TIME_CHANGED, - self._listener_cover_tilt) - self._listener_cover_tilt = None + if self._unsub_listener_cover_tilt is not None: + self._unsub_listener_cover_tilt() + self._unsub_listener_cover_tilt = None self._set_tilt_position = None def _listen_cover(self): """Listen for changes in cover.""" - if self._listener_cover is None: - self._listener_cover = track_utc_time_change( + if self._unsub_listener_cover is None: + self._unsub_listener_cover = track_utc_time_change( self.hass, self._time_changed_cover) def _time_changed_cover(self, now): @@ -156,8 +153,8 @@ class DemoCover(CoverDevice): def _listen_cover_tilt(self): """Listen for changes in cover tilt.""" - if self._listener_cover_tilt is None: - self._listener_cover_tilt = track_utc_time_change( + if self._unsub_listener_cover_tilt is None: + self._unsub_listener_cover_tilt = track_utc_time_change( self.hass, self._time_changed_cover_tilt) def _time_changed_cover_tilt(self, now): diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py index fd68ac3d265..aea05a9160a 100644 --- a/homeassistant/components/cover/homematic.py +++ b/homeassistant/components/cover/homematic.py @@ -24,9 +24,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return - return homematic.setup_hmdevice_discovery_helper(HMCover, - discovery_info, - add_callback_devices) + return homematic.setup_hmdevice_discovery_helper( + HMCover, + discovery_info, + add_callback_devices + ) # pylint: disable=abstract-method @@ -77,25 +79,8 @@ class HMCover(homematic.HMDevice, CoverDevice): 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 cover!", 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}) diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 5695bc5005a..a2eb40e21e8 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -19,13 +19,13 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ 'binary_sensor', 'camera', 'climate', + 'cover', 'device_tracker', - 'garage_door', + 'fan', 'light', 'lock', 'media_player', 'notify', - 'rollershutter', 'sensor', 'switch', ] diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index a954ae8fd0f..1bf921c2e06 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -7,23 +7,38 @@ https://home-assistant.io/components/device_sun_light_trigger/ import logging from datetime import timedelta +import voluptuous as vol + import homeassistant.util.dt as dt_util from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event_decorators import track_state_change from homeassistant.loader import get_component +import homeassistant.helpers.config_validation as cv -DOMAIN = "device_sun_light_trigger" +DOMAIN = 'device_sun_light_trigger' DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun'] +CONF_DEVICE_GROUP = 'device_group' +CONF_DISABLE_TURN_OFF = 'disable_turn_off' +CONF_LIGHT_GROUP = 'light_group' +CONF_LIGHT_PROFILE = 'light_profile' + +DEFAULT_DISABLE_TURN_OFF = False +DEFAULT_LIGHT_PROFILE = 'relax' + LIGHT_TRANSITION_TIME = timedelta(minutes=15) -# Light profile to be used if none given -LIGHT_PROFILE = 'relax' - -CONF_LIGHT_PROFILE = 'light_profile' -CONF_LIGHT_GROUP = 'light_group' -CONF_DEVICE_GROUP = 'device_group' +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_DEVICE_GROUP): cv.entity_id, + vol.Optional(CONF_DISABLE_TURN_OFF, default=DEFAULT_DISABLE_TURN_OFF): + cv.boolean, + vol.Optional(CONF_LIGHT_GROUP): cv.string, + vol.Optional(CONF_LIGHT_PROFILE, default=DEFAULT_LIGHT_PROFILE): + cv.string, + }), +}, extra=vol.ALLOW_EXTRA) # pylint: disable=too-many-locals @@ -35,10 +50,10 @@ def setup(hass, config): light = get_component('light') sun = get_component('sun') - disable_turn_off = 'disable_turn_off' in config[DOMAIN] + disable_turn_off = config[DOMAIN].get(CONF_DISABLE_TURN_OFF) light_group = config[DOMAIN].get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) - light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE, LIGHT_PROFILE) + light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE) device_group = config[DOMAIN].get(CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES) device_entity_ids = group.get_entity_ids(hass, device_group, @@ -52,7 +67,7 @@ def setup(hass, config): light_ids = group.get_entity_ids(hass, light_group, light.DOMAIN) if not light_ids: - logger.error("No lights found to turn on ") + logger.error("No lights found to turn on") return False def calc_time_for_light_when_sunset(): diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index a4f65ab4ea4..4247213087b 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -62,6 +62,7 @@ ATTR_HOST_NAME = 'host_name' ATTR_LOCATION_NAME = 'location_name' ATTR_GPS = 'gps' ATTR_BATTERY = 'battery' +ATTR_ATTRIBUTES = 'attributes' PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds @@ -86,10 +87,11 @@ def is_on(hass: HomeAssistantType, entity_id: str=None): return hass.states.is_state(entity, STATE_HOME) +# pylint: disable=too-many-arguments def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None, host_name: str=None, location_name: str=None, gps: GPSType=None, gps_accuracy=None, - battery=None): # pylint: disable=too-many-arguments + battery=None, attributes: dict=None): """Call service to notify you see device.""" data = {key: value for key, value in ((ATTR_MAC, mac), @@ -99,6 +101,9 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None, (ATTR_GPS, gps), (ATTR_GPS_ACCURACY, gps_accuracy), (ATTR_BATTERY, battery)) if value is not None} + if attributes: + for key, value in attributes: + data[key] = value hass.services.call(DOMAIN, SERVICE_SEE, data) @@ -164,7 +169,7 @@ def setup(hass: HomeAssistantType, config: ConfigType): """Service to see a device.""" args = {key: value for key, value in call.data.items() if key in (ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME, - ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY)} + ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)} tracker.see(**args) descriptions = load_yaml_config_file( @@ -202,7 +207,7 @@ class DeviceTracker(object): def see(self, mac: str=None, dev_id: str=None, host_name: str=None, location_name: str=None, gps: GPSType=None, gps_accuracy=None, - battery: str=None): + battery: str=None, attributes: dict=None): """Notify the device tracker that you see a device.""" with self.lock: if mac is None and dev_id is None: @@ -218,7 +223,7 @@ class DeviceTracker(object): if device: device.seen(host_name, location_name, gps, gps_accuracy, - battery) + battery, attributes) if device.track: device.update_ha_state() return @@ -232,7 +237,8 @@ class DeviceTracker(object): if mac is not None: self.mac_to_dev[mac] = device - device.seen(host_name, location_name, gps, gps_accuracy, battery) + device.seen(host_name, location_name, gps, gps_accuracy, battery, + attributes) if device.track: device.update_ha_state() @@ -267,6 +273,7 @@ class Device(Entity): gps_accuracy = 0 last_seen = None # type: dt_util.dt.datetime battery = None # type: str + attributes = None # type: dict # Track if the last update of this device was HOME. last_update_home = False @@ -330,6 +337,10 @@ class Device(Entity): if self.battery: attr[ATTR_BATTERY] = self.battery + if self.attributes: + for key, value in self.attributes: + attr[key] = value + return attr @property @@ -338,13 +349,15 @@ class Device(Entity): return self.away_hide and self.state != STATE_HOME def seen(self, host_name: str=None, location_name: str=None, - gps: GPSType=None, gps_accuracy=0, battery: str=None): + gps: GPSType=None, gps_accuracy=0, battery: str=None, + attributes: dict=None): """Mark the device as seen.""" self.last_seen = dt_util.utcnow() self.host_name = host_name self.location_name = location_name self.gps_accuracy = gps_accuracy or 0 self.battery = battery + self.attributes = attributes self.gps = None if gps is not None: try: diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py index a62306b5619..6383bc962a4 100644 --- a/homeassistant/components/device_tracker/aruba.py +++ b/homeassistant/components/device_tracker/aruba.py @@ -9,9 +9,11 @@ import re import threading from datetime import timedelta -from homeassistant.components.device_tracker import DOMAIN +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago @@ -25,15 +27,16 @@ _DEVICES_REGEX = re.compile( r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' + r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s+') +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string +}) + # pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a Aruba scanner.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): - return None - scanner = ArubaDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -90,7 +93,7 @@ class ArubaDeviceScanner(object): def get_aruba_data(self): """Retrieve data from Aruba Access Point and return parsed result.""" import pexpect - connect = "ssh {}@{}" + connect = 'ssh {}@{}' ssh = pexpect.spawn(connect.format(self.username, self.host)) query = ssh.expect(['password:', pexpect.TIMEOUT, pexpect.EOF, 'continue connecting (yes/no)?', @@ -98,22 +101,22 @@ class ArubaDeviceScanner(object): 'Connection refused', 'Connection timed out'], timeout=120) if query == 1: - _LOGGER.error("Timeout") + _LOGGER.error('Timeout') return elif query == 2: - _LOGGER.error("Unexpected response from router") + _LOGGER.error('Unexpected response from router') return elif query == 3: ssh.sendline('yes') ssh.expect('password:') elif query == 4: - _LOGGER.error("Host key Changed") + _LOGGER.error('Host key Changed') return elif query == 5: - _LOGGER.error("Connection refused by server") + _LOGGER.error('Connection refused by server') return elif query == 6: - _LOGGER.error("Connection timed out") + _LOGGER.error('Connection timed out') return ssh.sendline(self.password) ssh.expect('#') diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index a125607a00f..4fd2771db4f 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -26,19 +26,20 @@ CONF_PROTOCOL = 'protocol' CONF_MODE = 'mode' CONF_SSH_KEY = 'ssh_key' CONF_PUB_KEY = 'pub_key' +SECRET_GROUP = 'Password or SSH Key' PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PASSWORD, CONF_PUB_KEY, CONF_SSH_KEY), PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']), vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']), - vol.Optional(CONF_SSH_KEY): cv.isfile, - vol.Optional(CONF_PUB_KEY): cv.isfile + vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, + vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, + vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile })) @@ -101,6 +102,21 @@ class AsusWrtDeviceScanner(object): self.protocol = config[CONF_PROTOCOL] self.mode = config[CONF_MODE] + if self.protocol == 'ssh': + if self.ssh_key: + self.ssh_secret = {'ssh_key': self.ssh_key} + elif self.password: + self.ssh_secret = {'password': self.password} + else: + _LOGGER.error('No password or private key specified') + self.success_init = False + return + else: + if not self.password: + _LOGGER.error('No password specified') + self.success_init = False + return + self.lock = threading.Lock() self.last_results = {} @@ -149,15 +165,17 @@ class AsusWrtDeviceScanner(object): """Retrieve data from ASUSWRT via the ssh protocol.""" from pexpect import pxssh, exceptions + ssh = pxssh.pxssh() + try: + ssh.login(self.host, self.username, **self.ssh_secret) + except exceptions.EOF as err: + _LOGGER.error('Connection refused. Is SSH enabled?') + return None + except pxssh.ExceptionPxssh as err: + _LOGGER.error('Unable to connect via SSH: %s', str(err)) + return None + try: - ssh = pxssh.pxssh() - 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 private key specified') - return None ssh.sendline(_IP_NEIGH_CMD) ssh.prompt() neighbors = ssh.before.split(b'\n')[1:-1] @@ -178,9 +196,6 @@ class AsusWrtDeviceScanner(object): except pxssh.ExceptionPxssh as exc: _LOGGER.error('Unexpected response from router: %s', exc) return None - except exceptions.EOF: - _LOGGER.error('Connection refused or no route to host') - return None def telnet_connection(self): """Retrieve data from ASUSWRT via the telnet protocol.""" diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py new file mode 100644 index 00000000000..927c515b3a5 --- /dev/null +++ b/homeassistant/components/device_tracker/automatic.py @@ -0,0 +1,161 @@ +""" +Support for the Automatic platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.automatic/ +""" +from datetime import timedelta +import logging +import re +import requests + +import voluptuous as vol + +from homeassistant.components.device_tracker import (PLATFORM_SCHEMA, + ATTR_ATTRIBUTES) +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle, datetime as dt_util + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) + +CONF_CLIENT_ID = 'client_id' +CONF_SECRET = 'secret' +CONF_DEVICES = 'devices' + +SCOPE = 'scope:location scope:vehicle:profile scope:user:profile scope:trip' + +ATTR_ACCESS_TOKEN = 'access_token' +ATTR_EXPIRES_IN = 'expires_in' +ATTR_RESULTS = 'results' +ATTR_VEHICLE = 'vehicle' +ATTR_ENDED_AT = 'ended_at' +ATTR_END_LOCATION = 'end_location' + +URL_AUTHORIZE = 'https://accounts.automatic.com/oauth/access_token/' +URL_VEHICLES = 'https://api.automatic.com/vehicle/' +URL_TRIPS = 'https://api.automatic.com/trip/' + +_VEHICLE_ID_REGEX = re.compile( + (URL_VEHICLES + '(.*)?[/]$').replace('/', r'\/')) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_SECRET): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string]) +}) + + +def setup_scanner(hass, config: dict, see): + """Validate the configuration and return an Automatic scanner.""" + try: + AutomaticDeviceScanner(config, see) + except requests.HTTPError as err: + _LOGGER.error(str(err)) + return False + + return True + + +class AutomaticDeviceScanner(object): + """A class representing an Automatic device.""" + + def __init__(self, config: dict, see) -> None: + """Initialize the automatic device scanner.""" + self._devices = config.get(CONF_DEVICES, None) + self._access_token_payload = { + 'username': config.get(CONF_USERNAME), + 'password': config.get(CONF_PASSWORD), + 'client_id': config.get(CONF_CLIENT_ID), + 'client_secret': config.get(CONF_SECRET), + 'grant_type': 'password', + 'scope': SCOPE + } + self._headers = None + self._token_expires = dt_util.now() + self.last_results = {} + self.last_trips = {} + self.see = see + + self.scan_devices() + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + return [item['id'] for item in self.last_results] + + def get_device_name(self, device): + """Get the device name from id.""" + vehicle = [item['display_name'] for item in self.last_results + if item['id'] == device] + + return vehicle[0] + + def _update_headers(self): + """Get the access token from automatic.""" + if self._headers is None or self._token_expires <= dt_util.now(): + resp = requests.post( + URL_AUTHORIZE, + data=self._access_token_payload) + + resp.raise_for_status() + + json = resp.json() + + access_token = json[ATTR_ACCESS_TOKEN] + self._token_expires = dt_util.now() + timedelta( + seconds=json[ATTR_EXPIRES_IN]) + self._headers = { + 'Authorization': 'Bearer {}'.format(access_token) + } + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self) -> None: + """Update the device info.""" + _LOGGER.info('Updating devices') + self._update_headers() + + response = requests.get(URL_VEHICLES, headers=self._headers) + + response.raise_for_status() + + self.last_results = [item for item in response.json()[ATTR_RESULTS] + if self._devices is None or item[ + 'display_name'] in self._devices] + + response = requests.get(URL_TRIPS, headers=self._headers) + + if response.status_code == 200: + for trip in response.json()[ATTR_RESULTS]: + vehicle_id = _VEHICLE_ID_REGEX.match( + trip[ATTR_VEHICLE]).group(1) + if vehicle_id not in self.last_trips: + self.last_trips[vehicle_id] = trip + elif self.last_trips[vehicle_id][ATTR_ENDED_AT] < trip[ + ATTR_ENDED_AT]: + self.last_trips[vehicle_id] = trip + + for vehicle in self.last_results: + dev_id = vehicle.get('id') + + attrs = { + 'fuel_level': vehicle.get('fuel_level_percent') + } + + kwargs = { + 'dev_id': dev_id, + 'mac': dev_id, + ATTR_ATTRIBUTES: attrs + } + + if dev_id in self.last_trips: + end_location = self.last_trips[dev_id][ATTR_END_LOCATION] + kwargs['gps'] = (end_location['lat'], end_location['lon']) + kwargs['gps_accuracy'] = end_location['accuracy_m'] + + self.see(**kwargs) diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index ce8a535ff57..9ee30dd0ce2 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -11,6 +11,7 @@ from homeassistant.components.device_tracker import ( DEFAULT_SCAN_INTERVAL, PLATFORM_SCHEMA, load_config, + DEFAULT_TRACK_NEW ) import homeassistant.util as util import homeassistant.util.dt as dt_util @@ -58,10 +59,13 @@ def setup_scanner(hass, config, see): def discover_ble_devices(): """Discover Bluetooth LE devices.""" _LOGGER.debug("Discovering Bluetooth LE devices") - service = DiscoveryService() - devices = service.discover(duration) - _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) - + try: + service = DiscoveryService() + devices = service.discover(duration) + _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) + except RuntimeError as error: + _LOGGER.error("Error during Bluetooth LE scan: %s", error) + devices = [] return devices yaml_path = hass.config.path(YAML_DEVICES) @@ -85,7 +89,7 @@ def setup_scanner(hass, config, see): # if track new devices is true discover new devices # on every scan. track_new = util.convert(config.get(CONF_TRACK_NEW), bool, - len(devs_to_track) == 0) + DEFAULT_TRACK_NEW) if not devs_to_track and not track_new: _LOGGER.warning("No Bluetooth LE devices to track!") return False diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 298eddc4bc4..86e115c65c4 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -2,15 +2,13 @@ import logging from datetime import timedelta +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import ( - YAML_DEVICES, - CONF_TRACK_NEW, - CONF_SCAN_INTERVAL, - DEFAULT_SCAN_INTERVAL, - load_config, -) -import homeassistant.util as util + YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, + load_config, PLATFORM_SCHEMA, DEFAULT_TRACK_NEW) import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -19,6 +17,10 @@ REQUIREMENTS = ['pybluez==0.22'] BT_PREFIX = 'BT_' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_TRACK_NEW): cv.boolean +}) + def setup_scanner(hass, config, see): """Setup the Bluetooth Scanner.""" @@ -53,10 +55,8 @@ def setup_scanner(hass, config, see): else: devs_donot_track.append(device.mac[3:]) - # if track new devices is true discover new devices - # on startup. - track_new = util.convert(config.get(CONF_TRACK_NEW), bool, - len(devs_to_track) == 0) + # if track new devices is true discover new devices on startup. + track_new = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) if track_new: for dev in discover_devices(): if dev[0] not in devs_to_track and \ @@ -64,16 +64,16 @@ def setup_scanner(hass, config, see): devs_to_track.append(dev[0]) see_device(dev) - if not devs_to_track: - _LOGGER.warning("No bluetooth devices to track!") - return False - - interval = util.convert(config.get(CONF_SCAN_INTERVAL), int, - DEFAULT_SCAN_INTERVAL) + interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) def update_bluetooth(now): """Lookup bluetooth device and update status.""" try: + if track_new: + for dev in discover_devices(): + if dev[0] not in devs_to_track and \ + dev[0] not in devs_donot_track: + devs_to_track.append(dev[0]) for mac in devs_to_track: _LOGGER.debug("Scanning " + mac) result = bluetooth.lookup_name(mac, timeout=5) diff --git a/homeassistant/components/device_tracker/bt_home_hub_5.py b/homeassistant/components/device_tracker/bt_home_hub_5.py index c447fae1635..3b4115ff355 100644 --- a/homeassistant/components/device_tracker/bt_home_hub_5.py +++ b/homeassistant/components/device_tracker/bt_home_hub_5.py @@ -13,9 +13,10 @@ import json from urllib.parse import unquote import requests +import voluptuous as vol -from homeassistant.helpers import validate_config -from homeassistant.components.device_tracker import DOMAIN +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST from homeassistant.util import Throttle @@ -26,14 +27,14 @@ _LOGGER = logging.getLogger(__name__) _MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string +}) + # pylint: disable=unused-argument def get_scanner(hass, config): """Return a BT Home Hub 5 scanner if successful.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST]}, - _LOGGER): - return None scanner = BTHomeHub5DeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -44,7 +45,7 @@ class BTHomeHub5DeviceScanner(object): def __init__(self, config): """Initialise the scanner.""" - _LOGGER.info("Initialising BT Home Hub 5") + _LOGGER.info('Initialising BT Home Hub 5') self.host = config.get(CONF_HOST, '192.168.1.254') self.lock = threading.Lock() @@ -85,7 +86,7 @@ class BTHomeHub5DeviceScanner(object): return False with self.lock: - _LOGGER.info("Scanning") + _LOGGER.info('Scanning') data = _get_homehub_data(self.url) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 02f49fe7475..4dc6229566c 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -10,10 +10,11 @@ import threading from datetime import timedelta import requests +import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago. @@ -24,15 +25,16 @@ _LOGGER = logging.getLogger(__name__) _DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}') _MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string +}) + # pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a DD-WRT scanner.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): - return None - scanner = DdWrtDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -107,7 +109,7 @@ class DdWrtDeviceScanner(object): return False with self.lock: - _LOGGER.info("Checking ARP") + _LOGGER.info('Checking ARP') url = 'http://{}/Status_Wireless.live.asp'.format(self.host) data = self.get_ddwrt_data(url) @@ -143,18 +145,18 @@ class DdWrtDeviceScanner(object): auth=(self.username, self.password), timeout=4) except requests.exceptions.Timeout: - _LOGGER.exception("Connection to the router timed out") + _LOGGER.exception('Connection to the router timed out') return if response.status_code == 200: return _parse_ddwrt_response(response.text) elif response.status_code == 401: # Authentication error _LOGGER.exception( - "Failed to authenticate, " - "please check your username and password") + 'Failed to authenticate, ' + 'please check your username and password') return else: - _LOGGER.error("Invalid response from ddwrt: %s", response) + _LOGGER.error('Invalid response from ddwrt: %s', response) def _parse_ddwrt_response(data_str): diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py index 8def71cce73..202919871ad 100644 --- a/homeassistant/components/device_tracker/fritz.py +++ b/homeassistant/components/device_tracker/fritz.py @@ -7,9 +7,11 @@ https://home-assistant.io/components/device_tracker.fritz/ import logging from datetime import timedelta -from homeassistant.components.device_tracker import DOMAIN +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config from homeassistant.util import Throttle REQUIREMENTS = ['https://github.com/deisi/fritzconnection/archive/' @@ -21,14 +23,17 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +CONF_DEFAULT_IP = '169.254.1.1' # This IP is valid for all FRITZ!Box routers. + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, + vol.Optional(CONF_PASSWORD, default='admin'): cv.string, + vol.Optional(CONF_USERNAME, default=''): cv.string +}) + def get_scanner(hass, config): """Validate the configuration and return FritzBoxScanner.""" - if not validate_config(config, - {DOMAIN: []}, - _LOGGER): - return None - scanner = FritzBoxScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -40,22 +45,14 @@ class FritzBoxScanner(object): def __init__(self, config): """Initialize the scanner.""" self.last_results = [] - self.host = '169.254.1.1' # This IP is valid for all FRITZ!Box router. - self.username = 'admin' - self.password = '' + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] self.success_init = True # pylint: disable=import-error import fritzconnection as fc - # Check for user specific configuration - if CONF_HOST in config.keys(): - self.host = config[CONF_HOST] - if CONF_USERNAME in config.keys(): - self.username = config[CONF_USERNAME] - if CONF_PASSWORD in config.keys(): - self.password = config[CONF_PASSWORD] - # Establish a connection to the FRITZ!Box. try: self.fritz_box = fc.FritzHosts(address=self.host, @@ -70,25 +67,25 @@ class FritzBoxScanner(object): self.success_init = False if self.success_init: - _LOGGER.info("Successfully connected to %s", + _LOGGER.info('Successfully connected to %s', self.fritz_box.modelname) self._update_info() else: - _LOGGER.error("Failed to establish connection to FRITZ!Box " - "with IP: %s", self.host) + _LOGGER.error('Failed to establish connection to FRITZ!Box ' + 'with IP: %s', self.host) def scan_devices(self): """Scan for new devices and return a list of found device ids.""" self._update_info() active_hosts = [] for known_host in self.last_results: - if known_host["status"] == "1": - active_hosts.append(known_host["mac"]) + if known_host['status'] == '1': + active_hosts.append(known_host['mac']) return active_hosts def get_device_name(self, mac): """Return the name of the given device or None if is not known.""" - ret = self.fritz_box.get_specific_host_entry(mac)["NewHostName"] + ret = self.fritz_box.get_specific_host_entry(mac)['NewHostName'] if ret == {}: return None return ret @@ -99,6 +96,6 @@ class FritzBoxScanner(object): if not self.success_init: return False - _LOGGER.info("Scanning") + _LOGGER.info('Scanning') self.last_results = self.fritz_box.get_hosts_info() return True diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 3b0c7c0bbe5..b97993f9afa 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -11,10 +11,11 @@ import threading from datetime import timedelta import requests +import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago. @@ -22,14 +23,15 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string +}) + def get_scanner(hass, config): """Validate the configuration and return a Luci scanner.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): - return None - scanner = LuciDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -93,7 +95,7 @@ class LuciDeviceScanner(object): return False with self.lock: - _LOGGER.info("Checking ARP") + _LOGGER.info('Checking ARP') url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host) result = _req_json_rpc(url, 'net.arptable', @@ -117,19 +119,19 @@ def _req_json_rpc(url, method, *args, **kwargs): try: res = requests.post(url, data=data, timeout=5, **kwargs) except requests.exceptions.Timeout: - _LOGGER.exception("Connection to the router timed out") + _LOGGER.exception('Connection to the router timed out') return if res.status_code == 200: try: result = res.json() except ValueError: # If json decoder could not parse the response - _LOGGER.exception("Failed to parse response from luci") + _LOGGER.exception('Failed to parse response from luci') return try: return result['result'] except KeyError: - _LOGGER.exception("No result in response from luci") + _LOGGER.exception('No result in response from luci') return elif res.status_code == 401: # Authentication error @@ -138,7 +140,7 @@ def _req_json_rpc(url, method, *args, **kwargs): "please check your username and password") return else: - _LOGGER.error("Invalid response from luci: %s", res) + _LOGGER.error('Invalid response from luci: %s', res) def _get_token(host, username, password): diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index 0998e227857..2318eb44dd1 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -9,13 +9,12 @@ import logging import voluptuous as vol import homeassistant.components.mqtt as mqtt +from homeassistant.const import CONF_DEVICES from homeassistant.components.mqtt import CONF_QOS import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['mqtt'] -CONF_DEVICES = 'devices' - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index b20e5aae60e..ff6fe2f1e41 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -8,9 +8,12 @@ import logging import threading from datetime import timedelta -from homeassistant.components.device_tracker import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \ - CONF_PORT +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago. @@ -19,6 +22,17 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pynetgear==0.3.3'] +DEFAULT_HOST = 'routerlogin.net' +DEFAULT_USER = 'admin' +DEFAULT_PORT = 5000 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USER): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port +}) + def get_scanner(hass, config): """Validate the configuration and returns a Netgear scanner.""" @@ -28,10 +42,6 @@ def get_scanner(hass, config): password = info.get(CONF_PASSWORD) port = info.get(CONF_PORT) - if password is not None and host is None: - _LOGGER.warning('Found username or password but no host') - return None - scanner = NetgearDeviceScanner(host, username, password, port) return scanner if scanner.success_init else None @@ -47,16 +57,9 @@ class NetgearDeviceScanner(object): self.last_results = [] self.lock = threading.Lock() - if host is None: - self._api = pynetgear.Netgear() - elif username is None: - self._api = pynetgear.Netgear(password, host) - elif port is None: - self._api = pynetgear.Netgear(password, host, username) - else: - self._api = pynetgear.Netgear(password, host, username, port) + self._api = pynetgear.Netgear(password, host, username, port) - _LOGGER.info("Logging in") + _LOGGER.info('Logging in') results = self._api.get_attached_devices() @@ -65,7 +68,7 @@ class NetgearDeviceScanner(object): if self.success_init: self.last_results = results else: - _LOGGER.error("Failed to Login") + _LOGGER.error('Failed to Login') def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -91,7 +94,7 @@ class NetgearDeviceScanner(object): return with self.lock: - _LOGGER.info("Scanning") + _LOGGER.info('Scanning') results = self._api.get_attached_devices() diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 7b9f2e9036b..e23d5f31145 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -22,7 +22,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) # Interval in minutes to exclude devices from a scan while they are home -CONF_HOME_INTERVAL = "home_interval" +CONF_HOME_INTERVAL = 'home_interval' +CONF_EXCLUDE = 'exclude' REQUIREMENTS = ['python-nmap==0.6.1'] @@ -60,6 +61,7 @@ class NmapDeviceScanner(object): self.last_results = [] self.hosts = config[CONF_HOSTS] + self.exclude = config.get(CONF_EXCLUDE, []) minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0) self.home_interval = timedelta(minutes=minutes) @@ -93,7 +95,8 @@ class NmapDeviceScanner(object): from nmap import PortScanner, PortScannerError scanner = PortScanner() - options = "-F --host-timeout 5s" + options = "-F --host-timeout 5s " + exclude = "--exclude " if self.home_interval: boundary = dt_util.now() - self.home_interval @@ -102,10 +105,16 @@ class NmapDeviceScanner(object): if last_results: # Pylint is confused here. # pylint: disable=no-member - options += " --exclude {}".format(",".join(device.ip for device - in last_results)) + exclude_hosts = self.exclude + [device.ip for device + in last_results] + else: + exclude_hosts = self.exclude else: last_results = [] + exclude_hosts = self.exclude + if exclude_hosts: + exclude = " --exclude {}".format(",".join(exclude_hosts)) + options += exclude try: result = scanner.scan(hosts=self.hosts, arguments=options) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index cdb1f90ba8a..4f6e6647f1c 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -9,9 +9,14 @@ import logging import threading from collections import defaultdict +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv import homeassistant.components.mqtt as mqtt from homeassistant.const import STATE_HOME from homeassistant.util import convert, slugify +from homeassistant.components import zone as zone_comp +from homeassistant.components.device_tracker import PLATFORM_SCHEMA DEPENDENCIES = ['mqtt'] @@ -22,20 +27,35 @@ BEACON_DEV_ID = 'beacon' LOCATION_TOPIC = 'owntracks/+/+' EVENT_TOPIC = 'owntracks/+/+/event' +WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint' _LOGGER = logging.getLogger(__name__) LOCK = threading.Lock() CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' +CONF_WAYPOINT_IMPORT = 'waypoints' +CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist' VALIDATE_LOCATION = 'location' VALIDATE_TRANSITION = 'transition' +VALIDATE_WAYPOINTS = 'waypoints' + +WAYPOINT_LAT_KEY = 'lat' +WAYPOINT_LON_KEY = 'lon' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float), + vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean, + vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(cv.ensure_list, [cv.string]) +}) def setup_scanner(hass, config, see): """Setup an OwnTracks tracker.""" max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY) + waypoint_import = config.get(CONF_WAYPOINT_IMPORT) + waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST) def validate_payload(payload, data_type): """Validate OwnTracks payload.""" @@ -50,7 +70,7 @@ def setup_scanner(hass, config, see): 'because of missing or malformatted data: %s', data_type, data) return None - if data_type == VALIDATE_TRANSITION: + if data_type == VALIDATE_TRANSITION or data_type == VALIDATE_WAYPOINTS: return data if max_gps_accuracy is not None and \ convert(data.get('acc'), float, 0.0) > max_gps_accuracy: @@ -182,6 +202,26 @@ def setup_scanner(hass, config, see): data['event']) return + def owntracks_waypoint_update(topic, payload, qos): + """List of waypoints published by a user.""" + # Docs on available data: + # http://owntracks.org/booklet/tech/json/#_typewaypoints + data = validate_payload(payload, VALIDATE_WAYPOINTS) + if not data: + return + + wayps = data['waypoints'] + _LOGGER.info("Got %d waypoints from %s", len(wayps), topic) + for wayp in wayps: + name = wayp['desc'] + pretty_name = parse_topic(topic, True)[1] + ' - ' + name + lat = wayp[WAYPOINT_LAT_KEY] + lon = wayp[WAYPOINT_LON_KEY] + rad = wayp['rad'] + zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad, + zone_comp.ICON_IMPORT, False, True) + zone_comp.add_zone(hass, pretty_name, zone) + def see_beacons(dev_id, kwargs_param): """Set active beacons to the current location.""" kwargs = kwargs_param.copy() @@ -195,18 +235,39 @@ def setup_scanner(hass, config, see): mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) + if waypoint_import: + if waypoint_whitelist is None: + mqtt.subscribe(hass, WAYPOINT_TOPIC.format('+', '+'), + owntracks_waypoint_update, 1) + else: + for whitelist_user in waypoint_whitelist: + mqtt.subscribe(hass, WAYPOINT_TOPIC.format(whitelist_user, + '+'), + owntracks_waypoint_update, 1) + return True +def parse_topic(topic, pretty=False): + """Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple.""" + parts = topic.split('/') + dev_id_format = '' + if pretty: + dev_id_format = '{} {}' + else: + dev_id_format = '{}_{}' + dev_id = slugify(dev_id_format.format(parts[1], parts[2])) + host_name = parts[1] + return (host_name, dev_id) + + def _parse_see_args(topic, data): """Parse the OwnTracks location parameters, into the format see expects.""" - parts = topic.split('/') - dev_id = slugify('{}_{}'.format(parts[1], parts[2])) - host_name = parts[1] + (host_name, dev_id) = parse_topic(topic, False) kwargs = { 'dev_id': dev_id, 'host_name': host_name, - 'gps': (data['lat'], data['lon']) + 'gps': (data[WAYPOINT_LAT_KEY], data[WAYPOINT_LON_KEY]) } if 'acc' in data: kwargs['gps_accuracy'] = data['acc'] diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 9981b4d7ca6..56f9eb4aae6 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -9,9 +9,11 @@ import logging import threading from datetime import timedelta -from homeassistant.components.device_tracker import DOMAIN +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST -from homeassistant.helpers import validate_config from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago. @@ -23,15 +25,16 @@ REQUIREMENTS = ['pysnmp==4.3.2'] CONF_COMMUNITY = "community" CONF_BASEOID = "baseoid" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_COMMUNITY): cv.string, + vol.Required(CONF_BASEOID): cv.string +}) + # pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return an snmp scanner.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_COMMUNITY, CONF_BASEOID]}, - _LOGGER): - return None - scanner = SnmpScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index f5282feb733..f463c5a809d 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -11,10 +11,11 @@ import threading from datetime import timedelta import requests +import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago. @@ -24,15 +25,16 @@ CONF_HTTP_ID = "http_id" _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_HTTP_ID): cv.string +}) + def get_scanner(hass, config): """Validate the configuration and returns a Tomato scanner.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, - CONF_PASSWORD, CONF_HTTP_ID]}, - _LOGGER): - return None - return TomatoDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 17beab02532..ad295099bf5 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -277,8 +277,10 @@ class Tplink4DeviceScanner(TplinkDeviceScanner): _LOGGER.info("Retrieving auth tokens...") url = 'http://{}/userRpm/LoginRpm.htm?Save=Save'.format(self.host) - # Generate md5 hash of password - password = hashlib.md5(self.password.encode('utf')).hexdigest() + # Generate md5 hash of password. The C7 appears to use the first 15 + # characters of the password only, so we truncate to remove additional + # characters from being hashed. + password = hashlib.md5(self.password.encode('utf')[:15]).hexdigest() credentials = '{}:{}'.format(self.username, password).encode('utf') # Encode the credentials to be sent as a cookie. diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 736c1ba3168..5eaa4bf2fca 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -11,10 +11,11 @@ import threading from datetime import timedelta import requests +import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago. @@ -22,14 +23,15 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string +}) + def get_scanner(hass, config): """Validate the configuration and return an ubus scanner.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): - return None - scanner = UbusDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index 2ae3f76e5e6..d654c3e3eef 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -6,10 +6,11 @@ https://home-assistant.io/components/device_tracker.unifi/ """ import logging import urllib +import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers import validate_config # Unifi package doesn't list urllib3 as a requirement REQUIREMENTS = ['urllib3', 'unifi==1.2.5'] @@ -18,28 +19,24 @@ _LOGGER = logging.getLogger(__name__) CONF_PORT = 'port' CONF_SITE_ID = 'site_id' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default='localhost'): cv.string, + vol.Optional(CONF_SITE_ID, default='default'): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PORT, default=8443): cv.port +}) + def get_scanner(hass, config): """Setup Unifi device_tracker.""" from unifi.controller import Controller - if not validate_config(config, {DOMAIN: [CONF_USERNAME, - CONF_PASSWORD]}, - _LOGGER): - _LOGGER.error('Invalid configuration') - return False - - this_config = config[DOMAIN] - 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)) - except ValueError: - _LOGGER.error('Invalid port (must be numeric like 8443)') - return False + host = config[DOMAIN].get(CONF_HOST) + username = config[DOMAIN].get(CONF_USERNAME) + password = config[DOMAIN].get(CONF_PASSWORD) + site_id = config[DOMAIN].get(CONF_SITE_ID) + port = config[DOMAIN].get(CONF_PORT) try: ctrl = Controller(host, username, password, port, 'v4', site_id) diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index b752743d2d4..57b6bd4dc6d 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -12,10 +12,11 @@ import threading import requests import voluptuous as vol -from homeassistant.helpers import validate_config import homeassistant.helpers.config_validation as cv from homeassistant.util import sanitize_filename +_LOGGER = logging.getLogger(__name__) + ATTR_SUBDIR = 'subdir' ATTR_URL = 'url' @@ -30,15 +31,16 @@ SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({ vol.Optional(ATTR_SUBDIR): cv.string, }) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DOWNLOAD_DIR): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + # pylint: disable=too-many-branches def setup(hass, config): """Listen for download events to download files.""" - logger = logging.getLogger(__name__) - - if not validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger): - return False - download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] # If path is relative, we assume relative to HASS config dir @@ -46,8 +48,7 @@ def setup(hass, config): download_path = hass.config.path(download_path) if not os.path.isdir(download_path): - - logger.error( + _LOGGER.error( "Download path %s does not exist. File Downloader not active.", download_path) @@ -113,16 +114,16 @@ def setup(hass, config): final_path = "{}_{}.{}".format(path, tries, ext) - logger.info("%s -> %s", url, final_path) + _LOGGER.info("%s -> %s", url, final_path) with open(final_path, 'wb') as fil: for chunk in req.iter_content(1024): fil.write(chunk) - logger.info("Downloading of %s done", url) + _LOGGER.info("Downloading of %s done", url) except requests.exceptions.ConnectionError: - logger.exception("ConnectionError occured for %s", url) + _LOGGER.exception("ConnectionError occured for %s", url) # Remove file if we started downloading but failed if final_path and os.path.isfile(final_path): diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 702c7fd6304..24d47365a54 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/ecobee/ import logging import os from datetime import timedelta + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -15,14 +16,23 @@ from homeassistant.const import CONF_API_KEY from homeassistant.loader import get_component from homeassistant.util import Throttle -DOMAIN = "ecobee" -NETWORK = None -CONF_HOLD_TEMP = 'hold_temp' - REQUIREMENTS = [ 'https://github.com/nkgilley/python-ecobee-api/archive/' '4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6'] +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +CONF_HOLD_TEMP = 'hold_temp' + +DOMAIN = 'ecobee' + +ECOBEE_CONFIG_FILE = 'ecobee.conf' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) + +NETWORK = None + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_API_KEY): cv.string, @@ -30,14 +40,6 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) -_LOGGER = logging.getLogger(__name__) - -ECOBEE_CONFIG_FILE = 'ecobee.conf' -_CONFIGURING = {} - -# Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) - def request_configuration(network, hass, config): """Request configuration steps from the user.""" @@ -97,7 +99,7 @@ class EcobeeData(object): def update(self): """Get the latest data from pyecobee.""" self.ecobee.update() - _LOGGER.info("ecobee data updated successfully.") + _LOGGER.info("Ecobee data updated successfully") def setup(hass, config): @@ -116,9 +118,6 @@ def setup(hass, config): # Create ecobee.conf if it doesn't exist if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): - if config[DOMAIN].get(CONF_API_KEY) is None: - _LOGGER.error("No ecobee api_key found in config.") - return jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index f7a353d5c7f..d39a1602ec2 100755 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -44,7 +44,7 @@ DEFAULT_LISTEN_PORT = 8300 DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene'] DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ - 'switch', 'light', 'group', 'input_boolean', 'media_player' + 'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan' ] HUE_API_STATE_ON = 'on' diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 13244569dbb..a129ece3609 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -39,6 +39,7 @@ SERVICE_OSCILLATE = 'oscillate' SPEED_OFF = 'off' SPEED_LOW = 'low' SPEED_MED = 'med' +SPEED_MEDIUM = 'medium' SPEED_HIGH = 'high' ATTR_SPEED = 'speed' diff --git a/homeassistant/components/fan/demo.py b/homeassistant/components/fan/demo.py index 83508063fa9..ba2deb83125 100644 --- a/homeassistant/components/fan/demo.py +++ b/homeassistant/components/fan/demo.py @@ -1,5 +1,5 @@ """ -Demo garage door platform that has a fake fan. +Demo fan platform that has a fake fan. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ @@ -19,7 +19,7 @@ DEMO_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Setup demo garage door platform.""" + """Setup demo fan platform.""" add_devices_callback([ DemoFan(hass, FAN_NAME, STATE_OFF), ]) diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py new file mode 100644 index 00000000000..9d824a715c2 --- /dev/null +++ b/homeassistant/components/fan/mqtt.py @@ -0,0 +1,276 @@ +""" +Support for MQTT fans. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/fan.mqtt/ +""" +import logging +from functools import partial + +import voluptuous as vol + +import homeassistant.components.mqtt as mqtt +from homeassistant.const import (CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, + STATE_ON, STATE_OFF) +from homeassistant.components.mqtt import ( + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import render_with_possible_json_value +from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_MEDIUM, + SPEED_HIGH, FanEntity, + SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, + SPEED_OFF, ATTR_SPEED) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ["mqtt"] + +CONF_STATE_VALUE_TEMPLATE = "state_value_template" +CONF_SPEED_STATE_TOPIC = "speed_state_topic" +CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" +CONF_SPEED_VALUE_TEMPLATE = "speed_value_template" +CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" +CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" +CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" +CONF_PAYLOAD_ON = "payload_on" +CONF_PAYLOAD_OFF = "payload_off" +CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on" +CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" +CONF_PAYLOAD_LOW_SPEED = "payload_low_speed" +CONF_PAYLOAD_MEDIUM_SPEED = "payload_medium_speed" +CONF_PAYLOAD_HIGH_SPEED = "payload_high_speed" +CONF_SPEED_LIST = "speeds" + +DEFAULT_NAME = "MQTT Fan" +DEFAULT_PAYLOAD_ON = "ON" +DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_OPTIMISTIC = False + +OSCILLATE_ON_PAYLOAD = "oscillate_on" +OSCILLATE_OFF_PAYLOAD = "oscillate_off" + +OSCILLATION = "oscillation" + +PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_SPEED_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SPEED_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_OSCILLATION_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_OSCILLATION_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_OSCILLATION_ON, + default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_PAYLOAD_OSCILLATION_OFF, + default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, + vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MED): cv.string, + vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, + vol.Optional(CONF_SPEED_LIST, + default=[SPEED_OFF, SPEED_LOW, + SPEED_MED, SPEED_HIGH]): cv.ensure_list, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup MQTT fan platform.""" + add_devices_callback([MqttFan( + hass, + config[CONF_NAME], + { + key: config.get(key) for key in ( + CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, + CONF_SPEED_STATE_TOPIC, + CONF_SPEED_COMMAND_TOPIC, + CONF_OSCILLATION_STATE_TOPIC, + CONF_OSCILLATION_COMMAND_TOPIC, + ) + }, + { + CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), + ATTR_SPEED: config.get(CONF_SPEED_VALUE_TEMPLATE), + OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE) + }, + config[CONF_QOS], + config[CONF_RETAIN], + { + STATE_ON: config[CONF_PAYLOAD_ON], + STATE_OFF: config[CONF_PAYLOAD_OFF], + OSCILLATE_ON_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_ON], + OSCILLATE_OFF_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_OFF], + SPEED_LOW: config[CONF_PAYLOAD_LOW_SPEED], + SPEED_MEDIUM: config[CONF_PAYLOAD_MEDIUM_SPEED], + SPEED_HIGH: config[CONF_PAYLOAD_HIGH_SPEED], + }, + config[CONF_SPEED_LIST], + config[CONF_OPTIMISTIC], + )]) + + +# pylint: disable=too-many-instance-attributes +class MqttFan(FanEntity): + """A MQTT fan component.""" + + # pylint: disable=too-many-arguments + def __init__(self, hass, name, topic, templates, qos, retain, payload, + speed_list, optimistic): + """Initialize MQTT fan.""" + self._hass = hass + self._name = name + self._topic = topic + self._qos = qos + self._retain = retain + self._payload = payload + self._speed_list = speed_list + self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None + self._optimistic_oscillation = (optimistic or + topic[CONF_OSCILLATION_STATE_TOPIC] + is None) + self._optimistic_speed = (optimistic or + topic[CONF_SPEED_STATE_TOPIC] is None) + self._state = False + self._supported_features = 0 + self._supported_features |= (topic[CONF_OSCILLATION_STATE_TOPIC] + is not None and SUPPORT_OSCILLATE) + self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] + is not None and SUPPORT_SET_SPEED) + + templates = {key: ((lambda value: value) if tpl is None else + partial(render_with_possible_json_value, hass, tpl)) + for key, tpl in templates.items()} + + def state_received(topic, payload, qos): + """A new MQTT message has been received.""" + payload = templates[CONF_STATE](payload) + if payload == self._payload[STATE_ON]: + self._state = True + elif payload == self._payload[STATE_OFF]: + self._state = False + + self.update_ha_state() + + if self._topic[CONF_STATE_TOPIC] is not None: + mqtt.subscribe(self._hass, self._topic[CONF_STATE_TOPIC], + state_received, self._qos) + + def speed_received(topic, payload, qos): + """A new MQTT message for the speed has been received.""" + payload = templates[ATTR_SPEED](payload) + if payload == self._payload[SPEED_LOW]: + self._speed = SPEED_LOW + elif payload == self._payload[SPEED_MEDIUM]: + self._speed = SPEED_MED + elif payload == self._payload[SPEED_HIGH]: + self._speed = SPEED_HIGH + self.update_ha_state() + + if self._topic[CONF_SPEED_STATE_TOPIC] is not None: + mqtt.subscribe(self._hass, self._topic[CONF_SPEED_STATE_TOPIC], + speed_received, self._qos) + self._speed = SPEED_OFF + elif self._topic[CONF_SPEED_COMMAND_TOPIC] is not None: + self._speed = SPEED_OFF + else: + self._speed = SPEED_OFF + + def oscillation_received(topic, payload, qos): + """A new MQTT message has been received.""" + payload = templates[OSCILLATION](payload) + if payload == self._payload[OSCILLATE_ON_PAYLOAD]: + self._oscillation = True + elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]: + self._oscillation = False + self.update_ha_state() + + if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: + mqtt.subscribe(self._hass, + self._topic[CONF_OSCILLATION_STATE_TOPIC], + oscillation_received, self._qos) + self._oscillation = False + if self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None: + self._oscillation = False + else: + self._oscillation = False + + @property + def should_poll(self): + """No polling needed for a MQTT fan.""" + return False + + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def name(self) -> str: + """Get entity name.""" + return self._name + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return self._speed_list + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def speed(self): + """Return the current speed.""" + return self._speed + + @property + def oscillating(self): + """Return the oscillation state.""" + return self._oscillation + + def turn_on(self, speed: str=SPEED_MED) -> None: + """Turn on the entity.""" + mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC], + self._payload[STATE_ON], self._qos, self._retain) + self.set_speed(speed) + + def turn_off(self) -> None: + """Turn off the entity.""" + mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC], + self._payload[STATE_OFF], self._qos, self._retain) + + def set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + if self._topic[CONF_SPEED_COMMAND_TOPIC] is not None: + mqtt_payload = SPEED_OFF + if speed == SPEED_LOW: + mqtt_payload = self._payload[SPEED_LOW] + elif speed == SPEED_MED: + mqtt_payload = self._payload[SPEED_MEDIUM] + elif speed == SPEED_HIGH: + mqtt_payload = self._payload[SPEED_HIGH] + else: + mqtt_payload = speed + self._speed = speed + mqtt.publish(self._hass, self._topic[CONF_SPEED_COMMAND_TOPIC], + mqtt_payload, self._qos, self._retain) + self.update_ha_state() + + def oscillate(self, oscillating: bool) -> None: + """Set oscillation.""" + if self._topic[CONF_SPEED_COMMAND_TOPIC] is not None: + self._oscillation = oscillating + mqtt.publish(self._hass, + self._topic[CONF_OSCILLATION_COMMAND_TOPIC], + self._oscillation, self._qos, self._retain) + self.update_ha_state() diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 4cc0223ce9b..ce3d46b4751 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -1,5 +1,5 @@ """ -Support for RSS/Atom feed. +Support for RSS/Atom feeds. For more details about this component, please refer to the documentation at https://home-assistant.io/components/feedreader/ @@ -9,22 +9,39 @@ from logging import getLogger from os.path import exists from threading import Lock import pickle + import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.event import track_utc_time_change +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['feedparser==5.2.1'] + _LOGGER = getLogger(__name__) -DOMAIN = "feedreader" -EVENT_FEEDREADER = "feedreader" -# pylint: disable=no-value-for-parameter + +CONF_URLS = 'urls' + +DOMAIN = 'feedreader' + +EVENT_FEEDREADER = 'feedreader' + +MAX_ENTRIES = 20 + CONFIG_SCHEMA = vol.Schema({ DOMAIN: { - 'urls': [vol.Url()], + vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), } }, extra=vol.ALLOW_EXTRA) -MAX_ENTRIES = 20 + + +def setup(hass, config): + """Setup the feedreader component.""" + urls = config.get(DOMAIN)[CONF_URLS] + data_file = hass.config.path("{}.pickle".format(DOMAIN)) + storage = StoredData(data_file) + feeds = [FeedManager(url, hass, storage) for url in urls] + return len(feeds) > 0 # pylint: disable=too-few-public-methods @@ -83,9 +100,8 @@ class FeedManager(object): def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" - # We are lucky, `published_parsed` data available, - # let's make use of it to publish only new available - # entries since the last run + # We are lucky, `published_parsed` data available, let's make use of + # it to publish only new available entries since the last run if 'published_parsed' in entry.keys(): self._has_published_parsed = True self._last_entry_timestamp = max(entry.published_parsed, @@ -163,12 +179,3 @@ class StoredData(object): _LOGGER.error('Error saving pickled data to %s', self._data_file) self._cache_outdated = True - - -def setup(hass, config): - """Setup the feedreader component.""" - urls = config.get(DOMAIN)['urls'] - data_file = hass.config.path("{}.pickle".format(DOMAIN)) - storage = StoredData(data_file) - feeds = [FeedManager(url, hass, storage) for url in urls] - return len(feeds) > 0 diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare.py index 6fcd2312bab..b08ba89ca77 100644 --- a/homeassistant/components/foursquare.py +++ b/homeassistant/components/foursquare.py @@ -7,42 +7,51 @@ https://home-assistant.io/components/foursquare/ import logging import os import json -import requests +import requests import voluptuous as vol +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView -DOMAIN = "foursquare" - -SERVICE_CHECKIN = "checkin" - -EVENT_PUSH = "foursquare.push" -EVENT_CHECKIN = "foursquare.checkin" - -CHECKIN_SERVICE_SCHEMA = vol.Schema({ - vol.Required("venueId"): cv.string, - vol.Optional("eventId"): cv.string, - vol.Optional("shout"): cv.string, - vol.Optional("mentions"): cv.string, - vol.Optional("broadcast"): cv.string, - vol.Optional("ll"): cv.string, - vol.Optional("llAcc"): cv.string, - vol.Optional("alt"): cv.string, - vol.Optional("altAcc"): cv.string, -}) - _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ["http"] +CONF_PUSH_SECRET = 'push_secret' + +DEPENDENCIES = ['http'] +DOMAIN = 'foursquare' + +EVENT_CHECKIN = 'foursquare.checkin' +EVENT_PUSH = 'foursquare.push' + +SERVICE_CHECKIN = 'checkin' + +CHECKIN_SERVICE_SCHEMA = vol.Schema({ + vol.Optional('alt'): cv.string, + vol.Optional('altAcc'): cv.string, + vol.Optional('broadcast'): cv.string, + vol.Optional('eventId'): cv.string, + vol.Optional('ll'): cv.string, + vol.Optional('llAcc'): cv.string, + vol.Optional('mentions'): cv.string, + vol.Optional('shout'): cv.string, + vol.Required('venueId'): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_PUSH_SECRET): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Setup the Foursquare component.""" descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), "services.yaml")) + os.path.join(os.path.dirname(__file__), 'services.yaml')) config = config[DOMAIN] @@ -51,7 +60,7 @@ def setup(hass, config): url = ("https://api.foursquare.com/v2/checkins/add" "?oauth_token={}" "&v=20160802" - "&m=swarm").format(config["access_token"]) + "&m=swarm").format(config[CONF_ACCESS_TOKEN]) response = requests.post(url, data=call.data, timeout=10) if response.status_code not in (200, 201): @@ -62,12 +71,12 @@ def setup(hass, config): hass.bus.fire(EVENT_CHECKIN, response.text) # Register our service with Home Assistant. - hass.services.register(DOMAIN, "checkin", checkin_user, + hass.services.register(DOMAIN, 'checkin', checkin_user, descriptions[DOMAIN][SERVICE_CHECKIN], schema=CHECKIN_SERVICE_SCHEMA) - hass.wsgi.register_view(FoursquarePushReceiver(hass, - config["push_secret"])) + hass.wsgi.register_view(FoursquarePushReceiver( + hass, config[CONF_PUSH_SECRET])) return True diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index b1bd204e1ce..796d91cade8 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -2,7 +2,7 @@ FINGERPRINTS = { "core.js": "1fd10c1fcdf56a61f60cf861d5a0368c", - "frontend.html": "610cc799225ede933a9894b64bb35717", + "frontend.html": "20defe06c11b2fa2f076dc92b6c3b0dd", "mdi.html": "710b84acc99b32514f52291aba9cd8e8", "panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b", "panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169", diff --git a/homeassistant/components/frontend/www_static/core.js.gz b/homeassistant/components/frontend/www_static/core.js.gz index 5accff51795..e9a51484a98 100644 Binary files a/homeassistant/components/frontend/www_static/core.js.gz and b/homeassistant/components/frontend/www_static/core.js.gz differ diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 367b8d15a5a..3d70d83b986 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,5 +1,5 @@ \ No newline at end of file +var r=t.propertyDataFromStyles(n._styles,this),i=!this.__notStyleScopeCacheable;i&&(r.key.customStyle=this.customStyle,e=n._styleCache.retrieve(this.is,r.key,this._styles));var a=Boolean(e);a?this._styleProperties=e._styleProperties:this._computeStyleProperties(r.properties),this._computeOwnStyleProperties(),a||(e=o.retrieve(this.is,this._ownStyleProperties,this._styles));var l=Boolean(e)&&!a,h=this._applyStyleProperties(e);a||(h=h&&s?h.cloneNode(!0):h,e={style:h,_scopeSelector:this._scopeSelector,_styleProperties:this._styleProperties},i&&(r.key.customStyle={},this.mixin(r.key.customStyle,this.customStyle),n._styleCache.store(this.is,e,r.key,this._styles)),l||o.store(this.is,Object.create(e),this._ownStyleProperties,this._styles))},_computeStyleProperties:function(e){var n=this._findStyleHost();n._styleProperties||n._computeStyleProperties();var r=Object.create(n._styleProperties),s=t.hostAndRootPropertiesForScope(this);this.mixin(r,s.hostProps),e=e||t.propertyDataFromStyles(n._styles,this).properties,this.mixin(r,e),this.mixin(r,s.rootProps),t.mixinCustomStyle(r,this.customStyle),t.reify(r),this._styleProperties=r},_computeOwnStyleProperties:function(){for(var e,t={},n=0;n0&&l.push(t);return[{removed:a,added:l}]}},Polymer.Collection.get=function(e){return Polymer._collections.get(e)||new Polymer.Collection(e)},Polymer.Collection.applySplices=function(e,t){var n=Polymer._collections.get(e);return n?n._applySplices(t):null},Polymer({is:"dom-repeat",extends:"template",_template:null,properties:{items:{type:Array},as:{type:String,value:"item"},indexAs:{type:String,value:"index"},sort:{type:Function,observer:"_sortChanged"},filter:{type:Function,observer:"_filterChanged"},observe:{type:String,observer:"_observeChanged"},delay:Number,renderedItemCount:{type:Number,notify:!0,readOnly:!0},initialCount:{type:Number,observer:"_initializeChunking"},targetFramerate:{type:Number,value:20},_targetFrameTime:{type:Number,computed:"_computeFrameTime(targetFramerate)"}},behaviors:[Polymer.Templatizer],observers:["_itemsChanged(items.*)"],created:function(){this._instances=[],this._pool=[],this._limit=1/0;var e=this;this._boundRenderChunk=function(){e._renderChunk()}},detached:function(){this.__isDetached=!0;for(var e=0;e=0;t--){var n=this._instances[t];n.isPlaceholder&&t=this._limit&&(n=this._downgradeInstance(t,n.__key__)),e[n.__key__]=t,n.isPlaceholder||n.__setProperty(this.indexAs,t,!0)}this._pool.length=0,this._setRenderedItemCount(this._instances.length),this.fire("dom-change"),this._tryRenderChunk()},_applyFullRefresh:function(){var e,t=this.collection;if(this._sortFn)e=t?t.getKeys():[];else{e=[];var n=this.items;if(n)for(var r=0;r=r;a--)this._detachAndRemoveInstance(a)},_numericSort:function(e,t){return e-t},_applySplicesUserSort:function(e){for(var t,n,r=this.collection,s={},i=0;i=0;i--){var h=a[i];void 0!==h&&this._detachAndRemoveInstance(h)}var c=this;if(l.length){this._filterFn&&(l=l.filter(function(e){return c._filterFn(r.getItem(e))})),l.sort(function(e,t){return c._sortFn(r.getItem(e),r.getItem(t))});var u=0;for(i=0;i>1,a=this._instances[o].__key__,l=this._sortFn(n.getItem(a),r);if(l<0)e=o+1;else{if(!(l>0)){i=o;break}s=o-1}}return i<0&&(i=s+1),this._insertPlaceholder(i,t),i},_applySplicesArrayOrder:function(e){for(var t,n=0;n=0?(e=this.as+"."+e.substring(n+1),i._notifyPath(e,t,!0)):i.__setProperty(this.as,t,!0))}},itemForElement:function(e){var t=this.modelForElement(e);return t&&t[this.as]},keyForElement:function(e){var t=this.modelForElement(e);return t&&t.__key__},indexForElement:function(e){var t=this.modelForElement(e);return t&&t[this.indexAs]}}),Polymer({is:"array-selector",_template:null,properties:{items:{type:Array,observer:"clearSelection"},multi:{type:Boolean,value:!1,observer:"clearSelection"},selected:{type:Object,notify:!0},selectedItem:{type:Object,notify:!0},toggle:{type:Boolean,value:!1}},clearSelection:function(){if(Array.isArray(this.selected))for(var e=0;e \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 5f719af4014..3c052db8e0c 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 659ec6552f7..ba588fc779d 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 659ec6552f761ff4779dd52ee35d26f7be5e111f +Subproject commit ba588fc779d34a2fbf7cc9a23103c38e3e3e0356 diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz index d32bfd90fb1..83efff86001 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz index d42283913c0..aa8a5cc8517 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz index 8ff19b321b3..85ebe5f75f9 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz index b7118f9fc63..edfb67103e0 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz index 3671424d539..3f4d3c358f5 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz index aa972aff21b..6a8e59f4d2f 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz index 05702a62ffa..974200ba1f7 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz index c68590101cf..845f8a31b18 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz index 30adcaafce3..e1700035dbd 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 120314a120f..e9ac4725af5 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","682fb6c521778b1f879d1d0c2b000886"],["/frontend/panels/dev-event-3cc881ae8026c0fba5aa67d334a3ab2b.html","e22ed0d2d10777c87eb9620d81f525b4"],["/frontend/panels/dev-info-34e2df1af32e60fffcafe7e008a92169.html","7e939dc762dc0c0ec769db4ea76a4b09"],["/frontend/panels/dev-service-bb5c587ada694e0fd42ceaaedd6fe6aa.html","782c4860c5e8ab274231ba9dfd528f29"],["/frontend/panels/dev-state-4608326978256644c42b13940c028e0a.html","26758b741ac1b7c8e9cfcb24762d8774"],["/frontend/panels/dev-template-0a099d4589636ed3038a3e9f020468a7.html","99114026cf9193263c74cc25f9f6a469"],["/frontend/panels/map-af7d04aff7dd5479c5a0016bc8d4dd7d.html","6031df1b4d23d5b321208449b2d293f8"],["/static/core-1fd10c1fcdf56a61f60cf861d5a0368c.js","800ebb1bbb48274790f2ee1a2e53a24c"],["/static/frontend-610cc799225ede933a9894b64bb35717.html","a5c2eecc2b0be94714accb1f07f15e9f"],["/static/mdi-710b84acc99b32514f52291aba9cd8e8.html","149c8eaf6bb78a9b642c7bcedab86900"],["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/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]],cacheName="sw-precache-v2--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},createCacheKey=function(e,t,a,n){var c=new URL(e);return n&&c.toString().match(n)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(a)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},stripIgnoredUrlParameters=function(e,t){var a=new URL(e);return a.search=a.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),a.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],a=e[1],n=new URL(t,self.location),c=createCacheKey(n,hashParamName,a,!1);return[n.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(a){if(!t.has(a))return e.add(new Request(a,{credentials:"same-origin"}))}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(a){return Promise.all(a.map(function(a){if(!t.has(a.url))return e["delete"](a)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,a=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(a);var n="index.html";!t&&n&&(a=addDirectoryIndex(a,n),t=urlsToCacheKeys.has(a));var c="/";!t&&c&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(a=new URL(c,self.location).toString(),t=urlsToCacheKeys.has(a)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(a))})["catch"](function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url,t&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var a,n;for(a=0;a 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! - """ + def _init_data(self): + """Generate a data dict (self._data) from the Homematic metadata.""" # Add all attributes to data dict for data_note in self._hmdevice.ATTRIBUTENODE: self._data.update({data_note: STATE_UNKNOWN}) + + # init device specified data + self._init_data_struct() + + def _init_data_struct(self): + """Generate a data dict from the Homematic device metadata.""" + raise NotImplementedError diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index b9a81858d39..37deb41eef4 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -24,21 +24,21 @@ from homeassistant.core import split_entity_id import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv -DOMAIN = "http" -REQUIREMENTS = ("cherrypy==7.1.0", "static3==0.7.0", "Werkzeug==0.11.10") +DOMAIN = 'http' +REQUIREMENTS = ('cherrypy==7.1.0', 'static3==0.7.0', 'Werkzeug==0.11.11') -CONF_API_PASSWORD = "api_password" -CONF_SERVER_HOST = "server_host" -CONF_SERVER_PORT = "server_port" -CONF_DEVELOPMENT = "development" +CONF_API_PASSWORD = 'api_password' +CONF_SERVER_HOST = 'server_host' +CONF_SERVER_PORT = 'server_port' +CONF_DEVELOPMENT = 'development' CONF_SSL_CERTIFICATE = 'ssl_certificate' CONF_SSL_KEY = 'ssl_key' 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 +# 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 @@ -478,7 +478,7 @@ class HomeAssistantView(object): authenticated = True if self.requires_auth and not authenticated: - _LOGGER.warning('Login attempt or request with an invalid' + _LOGGER.warning('Login attempt or request with an invalid ' 'password from %s', request.remote_addr) raise Unauthorized() diff --git a/homeassistant/components/ifttt.py b/homeassistant/components/ifttt.py index a30ef184d7e..123d1a9d382 100644 --- a/homeassistant/components/ifttt.py +++ b/homeassistant/components/ifttt.py @@ -9,21 +9,22 @@ import logging import requests import voluptuous as vol -from homeassistant.helpers import validate_config import homeassistant.helpers.config_validation as cv +REQUIREMENTS = ['pyfttt==0.3'] + _LOGGER = logging.getLogger(__name__) -DOMAIN = "ifttt" - -SERVICE_TRIGGER = 'trigger' - ATTR_EVENT = 'event' ATTR_VALUE1 = 'value1' ATTR_VALUE2 = 'value2' ATTR_VALUE3 = 'value3' -REQUIREMENTS = ['pyfttt==0.3'] +CONF_KEY = 'key' + +DOMAIN = 'ifttt' + +SERVICE_TRIGGER = 'trigger' SERVICE_TRIGGER_SCHEMA = vol.Schema({ vol.Required(ATTR_EVENT): cv.string, @@ -32,6 +33,12 @@ SERVICE_TRIGGER_SCHEMA = vol.Schema({ vol.Optional(ATTR_VALUE3): cv.string, }) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_KEY): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + def trigger(hass, event, value1=None, value2=None, value3=None): """Trigger a Maker IFTTT recipe.""" @@ -46,10 +53,7 @@ def trigger(hass, event, value1=None, value2=None, value3=None): def setup(hass, config): """Setup the IFTTT service component.""" - if not validate_config(config, {DOMAIN: ['key']}, _LOGGER): - return False - - key = config[DOMAIN]['key'] + key = config[DOMAIN][CONF_KEY] def trigger_service(call): """Handle IFTTT trigger service calls.""" diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 1a33b7dc082..65f94c730bc 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -11,8 +11,9 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP) -DOMAIN = "keyboard" -REQUIREMENTS = ['pyuserinput==0.1.9'] +REQUIREMENTS = ['pyuserinput==0.1.11'] + +DOMAIN = 'keyboard' TAP_KEY_SCHEMA = vol.Schema({}) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 23afa58b628..f1bc83dfd17 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -98,6 +98,7 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({ LIGHT_TURN_OFF_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.entity_ids, ATTR_TRANSITION: VALID_TRANSITION, + ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), }) LIGHT_TOGGLE_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index b3aa7e59901..0d48e4c794b 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, + SUPPORT_EFFECT, SUPPORT_RGB_COLOR, Light) import homeassistant.helpers.config_validation as cv @@ -33,7 +34,8 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional('automatic_add', default=False): cv.boolean, }, extra=vol.ALLOW_EXTRA) -SUPPORT_FLUX_LED = SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR +SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | + SUPPORT_RGB_COLOR) def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 2e233e0e3ff..3f8eb1a22a5 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -22,9 +22,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return - return homematic.setup_hmdevice_discovery_helper(HMLight, - discovery_info, - add_callback_devices) + return homematic.setup_hmdevice_discovery_helper( + HMLight, + discovery_info, + add_callback_devices + ) class HMLight(homematic.HMDevice, Light): @@ -70,41 +72,8 @@ class HMLight(homematic.HMDevice, Light): 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) + self._state = "LEVEL" + self._data.update({self._state: STATE_UNKNOWN}) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index b818f4ee932..3ac7f3ae5f6 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -264,8 +264,10 @@ class HueLight(Light): if flash == FLASH_LONG: command['alert'] = 'lselect' + del command['on'] elif flash == FLASH_SHORT: command['alert'] = 'select' + del command['on'] elif self.bridge_type == 'hue': command['alert'] = 'none' @@ -290,6 +292,17 @@ class HueLight(Light): # 900 seconds. command['transitiontime'] = min(9000, kwargs[ATTR_TRANSITION] * 10) + flash = kwargs.get(ATTR_FLASH) + + if flash == FLASH_LONG: + command['alert'] = 'lselect' + del command['on'] + elif flash == FLASH_SHORT: + command['alert'] = 'select' + del command['on'] + elif self.bridge_type == 'hue': + command['alert'] = 'none' + self.bridge.set_light(self.light_id, command) def update(self): diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 41a226031d6..a54cf2bcc32 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/light.osramlightify/ """ import logging import socket +import random from datetime import timedelta from homeassistant import util @@ -14,9 +15,12 @@ from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_TRANSITION, + EFFECT_RANDOM, SUPPORT_BRIGHTNESS, + SUPPORT_EFFECT, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, @@ -33,7 +37,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | - SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) + SUPPORT_EFFECT | SUPPORT_RGB_COLOR | + SUPPORT_TRANSITION) def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -150,6 +155,13 @@ class OsramLightifyLight(Light): (TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN) self._light.set_temperature(kelvin, fade) + effect = kwargs.get(ATTR_EFFECT) + if effect == EFFECT_RANDOM: + self._light.set_rgb(random.randrange(0, 255), + random.randrange(0, 255), + random.randrange(0, 255), + fade) + self._light.set_luminance(brightness, fade) self.update_ha_state() diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 392be490dc3..d6a6931652b 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -60,6 +60,12 @@ turn_off: description: Duration in seconds it takes to get to next state example: 60 + flash: + description: If the light should flash + values: + - short + - long + toggle: description: Toggles a light diff --git a/homeassistant/components/light/zigbee.py b/homeassistant/components/light/zigbee.py index 1ab6a0b265a..f4406abf7bd 100644 --- a/homeassistant/components/light/zigbee.py +++ b/homeassistant/components/light/zigbee.py @@ -4,18 +4,27 @@ Functionality to use a ZigBee device as a light. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.zigbee/ """ +import voluptuous as vol + from homeassistant.components.light import Light from homeassistant.components.zigbee import ( - ZigBeeDigitalOut, ZigBeeDigitalOutConfig) + ZigBeeDigitalOut, ZigBeeDigitalOutConfig, PLATFORM_SCHEMA) -DEPENDENCIES = ["zigbee"] +CONF_ON_STATE = 'on_state' + +DEFAULT_ON_STATE = 'high' +DEPENDENCIES = ['zigbee'] + +STATES = ['high', 'low'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ON_STATE, default=DEFAULT_ON_STATE): vol.In(STATES), +}) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Create and add an entity based on the configuration.""" - add_entities([ - ZigBeeLight(hass, ZigBeeDigitalOutConfig(config)) - ]) + add_devices([ZigBeeLight(hass, ZigBeeDigitalOutConfig(config))]) class ZigBeeLight(ZigBeeDigitalOut, Light): diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index 81ab179efd4..b8f8ad9c5b3 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -8,25 +8,26 @@ import logging import voluptuous as vol -import homeassistant.components.mqtt as mqtt from homeassistant.components.lock import LockDevice -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) +from homeassistant.const import ( + CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) from homeassistant.helpers import template +import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['mqtt'] CONF_PAYLOAD_LOCK = 'payload_lock' CONF_PAYLOAD_UNLOCK = 'payload_unlock' DEFAULT_NAME = 'MQTT Lock' +DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_LOCK = 'LOCK' DEFAULT_PAYLOAD_UNLOCK = 'UNLOCK' -DEFAULT_OPTIMISTIC = False +DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -43,15 +44,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the MQTT lock.""" add_devices([MqttLock( hass, - config[CONF_NAME], + config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), - config[CONF_COMMAND_TOPIC], - config[CONF_QOS], - config[CONF_RETAIN], - config[CONF_PAYLOAD_LOCK], - config[CONF_PAYLOAD_UNLOCK], - config[CONF_OPTIMISTIC], - config.get(CONF_VALUE_TEMPLATE))]) + config.get(CONF_COMMAND_TOPIC), + config.get(CONF_QOS), + config.get(CONF_RETAIN), + config.get(CONF_PAYLOAD_LOCK), + config.get(CONF_PAYLOAD_UNLOCK), + config.get(CONF_OPTIMISTIC), + config.get(CONF_VALUE_TEMPLATE) + )]) # pylint: disable=too-many-arguments, too-many-instance-attributes @@ -88,8 +90,8 @@ class MqttLock(LockDevice): # Force into optimistic mode. self._optimistic = True else: - mqtt.subscribe(hass, self._state_topic, message_received, - self._qos) + mqtt.subscribe( + hass, self._state_topic, message_received, self._qos) @property def should_poll(self): diff --git a/homeassistant/components/lock/verisure.py b/homeassistant/components/lock/verisure.py index fe7a9eeaf5a..d758f4dc91d 100644 --- a/homeassistant/components/lock/verisure.py +++ b/homeassistant/components/lock/verisure.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/verisure/ import logging from homeassistant.components.verisure import HUB as hub +from homeassistant.components.verisure import (CONF_LOCKS, CONF_CODE_DIGITS) from homeassistant.components.lock import LockDevice from homeassistant.const import ( ATTR_CODE, STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED) @@ -17,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Verisure platform.""" locks = [] - if int(hub.config.get('locks', '1')): + if int(hub.config.get(CONF_LOCKS, 1)): hub.update_locks() locks.extend([ VerisureDoorlock(device_id) @@ -34,7 +35,7 @@ class VerisureDoorlock(LockDevice): """Initialize the lock.""" self._id = device_id self._state = STATE_UNKNOWN - self._digits = int(hub.config.get('code_digits', '4')) + self._digits = hub.config.get(CONF_CODE_DIGITS) self._changed_by = None @property diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index 3e9e8fdbd44..b4bab417742 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -8,20 +8,28 @@ import logging import os import json import re + +import voluptuous as vol + 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) + SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, + PLATFORM_SCHEMA) +from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = [ - 'https://github.com/aparraga/braviarc/archive/0.3.3.zip' - '#braviarc==0.3.3'] + 'https://github.com/aparraga/braviarc/archive/0.3.5.zip' + '#braviarc==0.3.5'] BRAVIA_CONFIG_FILE = 'bravia.conf' + CLIENTID_PREFIX = 'HomeAssistant' + +DEFAULT_NAME = 'Sony Bravia TV' + NICKNAME = 'Home Assistant' # Map ip to request id for configuring @@ -34,6 +42,11 @@ SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + def _get_mac_address(ip_address): """Get the MAC address of the device.""" @@ -82,7 +95,7 @@ def _config_from_file(filename, config=None): # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Sony Bravia TV platform.""" host = config.get(CONF_HOST) @@ -98,22 +111,20 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): pin = host_config['pin'] mac = host_config['mac'] name = config.get(CONF_NAME) - add_devices_callback([BraviaTVDevice(host, mac, name, pin)]) + add_devices([BraviaTVDevice(host, mac, name, pin)]) return - setup_bravia(config, pin, hass, add_devices_callback) + setup_bravia(config, pin, hass, add_devices) # pylint: disable=too-many-branches -def setup_bravia(config, pin, hass, add_devices_callback): +def setup_bravia(config, pin, hass, add_devices): """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) + request_configuration(config, hass, add_devices) return else: mac = _get_mac_address(host) @@ -132,15 +143,13 @@ def setup_bravia(config, pin, hass, add_devices_callback): {host: {'pin': pin, 'host': host, 'mac': mac}}): _LOGGER.error('failed to save config file') - add_devices_callback([BraviaTVDevice(host, mac, name, pin)]) + add_devices([BraviaTVDevice(host, mac, name, pin)]) -def request_configuration(config, hass, add_devices_callback): +def request_configuration(config, hass, add_devices): """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') @@ -158,9 +167,9 @@ def request_configuration(config, hass, add_devices_callback): braviarc = braviarc.BraviaRC(host) braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) if braviarc.is_connected(): - setup_bravia(config, pin, hass, add_devices_callback) + setup_bravia(config, pin, hass, add_devices) else: - request_configuration(config, hass, add_devices_callback) + request_configuration(config, hass, add_devices) _CONFIGURING[host] = configurator.request_config( hass, name, bravia_configuration_callback, diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 351fb47a368..2b10448b241 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -7,38 +7,49 @@ https://home-assistant.io/components/media_player.cast/ # pylint: disable=import-error import logging +import voluptuous as vol + 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_STOP, MediaPlayerDevice) + SUPPORT_STOP, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pychromecast==0.7.2'] + +_LOGGER = logging.getLogger(__name__) + CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' + +DEFAULT_PORT = 8009 + SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP + KNOWN_HOSTS = [] -DEFAULT_PORT = 8009 +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST): cv.string, +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the cast platform.""" import pychromecast - logger = logging.getLogger(__name__) # import CEC IGNORE attributes ignore_cec = config.get(CONF_IGNORE_CEC, []) if isinstance(ignore_cec, list): pychromecast.IGNORE_CEC += ignore_cec else: - logger.error('CEC config "%s" must be a list.', CONF_IGNORE_CEC) + _LOGGER.error('CEC config "%s" must be a list.', CONF_IGNORE_CEC) hosts = [] @@ -49,7 +60,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hosts = [discovery_info] elif CONF_HOST in config: - hosts = [(config[CONF_HOST], DEFAULT_PORT)] + hosts = [(config.get(CONF_HOST), DEFAULT_PORT)] else: hosts = [tuple(dev[:2]) for dev in pychromecast.discover_chromecasts() diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index 4726a1fa6a9..dde2e1d28e6 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -6,34 +6,47 @@ https://home-assistant.io/components/media_player.cmus/ """ import logging +import voluptuous as vol + + 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, + SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK, PLATFORM_SCHEMA, MediaPlayerDevice) -from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING, - CONF_HOST, CONF_NAME, CONF_PASSWORD, - CONF_PORT) +from homeassistant.const import ( + STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, + CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pycmus==0.1.0'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pycmus==0.1.0'] + +DEFAULT_NAME = 'cmus' +DEFAULT_PORT = 3000 SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_PLAY_MEDIA | SUPPORT_SEEK +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Inclusive(CONF_HOST, 'remote'): cv.string, + vol.Inclusive(CONF_PASSWORD, 'remote'): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + 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 + host = config.get(CONF_HOST) + password = config.get(CONF_PASSWORD) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME) + try: cmus_remote = CmusDevice(host, password, port, name) except exceptions.InvalidPassword: @@ -43,7 +56,7 @@ def setup_platform(hass, config, add_devices, discover_info=None): class CmusDevice(MediaPlayerDevice): - """Representation of a running CMUS.""" + """Representation of a running cmus.""" # pylint: disable=no-member, too-many-public-methods, abstract-method def __init__(self, server, password, port, name): @@ -51,13 +64,12 @@ class CmusDevice(MediaPlayerDevice): 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 + self.cmus = remote.PyCmus( + server=server, password=password, port=port) + auto_name = 'cmus-{}'.format(server) else: self.cmus = remote.PyCmus() - auto_name = "cmus-local" + auto_name = 'cmus-local' self._name = name or auto_name self.status = {} self.update() diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py index b4bcc9ae5ba..78df50dde76 100644 --- a/homeassistant/components/media_player/denon.py +++ b/homeassistant/components/media_player/denon.py @@ -7,32 +7,34 @@ https://home-assistant.io/components/media_player.denon/ import logging import telnetlib +import voluptuous as vol + from homeassistant.components.media_player import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) -from homeassistant.const import CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = 'Music station' + SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Denon platform.""" - if not config.get(CONF_HOST): - _LOGGER.error( - "Missing required configuration items in %s: %s", - DOMAIN, - CONF_HOST) - return False + denon = DenonDevice(config.get(CONF_NAME), config.get(CONF_HOST)) - denon = DenonDevice( - config.get("name", "Music station"), - config.get("host") - ) if denon.update(): add_devices([denon]) return True @@ -48,21 +50,21 @@ class DenonDevice(MediaPlayerDevice): """Initialize the Denon device.""" self._name = name self._host = host - self._pwstate = "PWSTANDBY" + self._pwstate = 'PWSTANDBY' self._volume = 0 self._muted = False - self._mediasource = "" + self._mediasource = '' @classmethod def telnet_request(cls, telnet, command): """Execute `command` and return the response.""" - telnet.write(command.encode("ASCII") + b"\r") - return telnet.read_until(b"\r", timeout=0.2).decode("ASCII").strip() + telnet.write(command.encode('ASCII') + b'\r') + return telnet.read_until(b'\r', timeout=0.2).decode('ASCII').strip() def telnet_command(self, command): """Establish a telnet connection and sends `command`.""" telnet = telnetlib.Telnet(self._host) - telnet.write(command.encode("ASCII") + b"\r") + telnet.write(command.encode('ASCII') + b'\r') telnet.read_very_eager() # skip response telnet.close() @@ -70,17 +72,17 @@ class DenonDevice(MediaPlayerDevice): """Get the latest details from the device.""" try: telnet = telnetlib.Telnet(self._host) - except ConnectionRefusedError: + except OSError: return False - self._pwstate = self.telnet_request(telnet, "PW?") + self._pwstate = self.telnet_request(telnet, 'PW?') # PW? sends also SISTATUS, which is not interesting telnet.read_until(b"\r", timeout=0.2) - volume_str = self.telnet_request(telnet, "MV?")[len("MV"):] + volume_str = self.telnet_request(telnet, 'MV?')[len('MV'):] self._volume = int(volume_str) / 60 - self._muted = (self.telnet_request(telnet, "MU?") == "MUON") - self._mediasource = self.telnet_request(telnet, "SI?")[len("SI"):] + self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON') + self._mediasource = self.telnet_request(telnet, 'SI?')[len('SI'):] telnet.close() return True @@ -93,9 +95,9 @@ class DenonDevice(MediaPlayerDevice): @property def state(self): """Return the state of the device.""" - if self._pwstate == "PWSTANDBY": + if self._pwstate == 'PWSTANDBY': return STATE_OFF - if self._pwstate == "PWON": + if self._pwstate == 'PWON': return STATE_ON return STATE_UNKNOWN @@ -122,41 +124,41 @@ class DenonDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" - self.telnet_command("PWSTANDBY") + self.telnet_command('PWSTANDBY') def volume_up(self): """Volume up media player.""" - self.telnet_command("MVUP") + self.telnet_command('MVUP') def volume_down(self): """Volume down media player.""" - self.telnet_command("MVDOWN") + self.telnet_command('MVDOWN') def set_volume_level(self, volume): """Set volume level, range 0..1.""" # 60dB max - self.telnet_command("MV" + str(round(volume * 60)).zfill(2)) + self.telnet_command('MV' + str(round(volume * 60)).zfill(2)) def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" - self.telnet_command("MU" + ("ON" if mute else "OFF")) + self.telnet_command('MU' + ('ON' if mute else 'OFF')) def media_play(self): """Play media media player.""" - self.telnet_command("NS9A") + self.telnet_command('NS9A') def media_pause(self): """Pause media player.""" - self.telnet_command("NS9B") + self.telnet_command('NS9B') def media_next_track(self): """Send the next track command.""" - self.telnet_command("NS9D") + self.telnet_command('NS9D') def media_previous_track(self): """Send the previous track command.""" - self.telnet_command("NS9E") + self.telnet_command('NS9E') def turn_on(self): """Turn the media player on.""" - self.telnet_command("PWON") + self.telnet_command('PWON') diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index 7a32f02dc56..0a53ffbbed6 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -1,14 +1,22 @@ -"""Support for the DirecTV recievers.""" +""" +Support for the DirecTV recievers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.directv/ +""" +import voluptuous as vol from homeassistant.components.media_player import ( MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice) from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING) + CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['directpy==0.1'] +DEFAULT_NAME = 'DirecTV Receiver' DEFAULT_PORT = 8080 SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ @@ -17,6 +25,12 @@ SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ KNOWN_HOSTS = [] +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the DirecTV platform.""" @@ -34,8 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif CONF_HOST in config: hosts.append([ - config.get(CONF_NAME, 'DirecTV Receiver'), - config[CONF_HOST], DEFAULT_PORT + config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT) ]) dtvs = [] diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 02b456a207c..518982a7038 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -7,46 +7,55 @@ https://home-assistant.io/components/media_player.firetv/ import logging import requests +import voluptuous as vol from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, - STATE_UNKNOWN) + STATE_UNKNOWN, CONF_HOST, CONF_PORT, CONF_NAME, CONF_DEVICE, CONF_DEVICES) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) SUPPORT_FIRETV = SUPPORT_PAUSE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET -DOMAIN = 'firetv' -DEVICE_LIST_URL = 'http://{0}/devices/list' -DEVICE_STATE_URL = 'http://{0}/devices/state/{1}' -DEVICE_ACTION_URL = 'http://{0}/devices/action/{1}/{2}' +DEFAULT_DEVICE = 'default' +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'Amazon Fire TV' +DEFAULT_PORT = 5556 +DEVICE_ACTION_URL = 'http://{0}:{1}/devices/action/{2}/{3}' +DEVICE_LIST_URL = 'http://{0}:{1}/devices/list' +DEVICE_STATE_URL = 'http://{0}:{1}/devices/state/{2}' -_LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the FireTV platform.""" - host = config.get('host', 'localhost:5556') - device_id = config.get('device', 'default') + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + device_id = config.get(CONF_DEVICE) + try: - response = requests.get(DEVICE_LIST_URL.format(host)).json() - if device_id in response['devices'].keys(): - add_devices([ - FireTVDevice( - host, - device_id, - config.get('name', 'Amazon Fire TV') - ) - ]) - _LOGGER.info( - 'Device %s accessible and ready for control', device_id) + response = requests.get(DEVICE_LIST_URL.format(host, port)).json() + if device_id in response[CONF_DEVICES].keys(): + add_devices([FireTVDevice(host, port, device_id, name)]) + _LOGGER.info('Device %s accessible and ready for control', + device_id) else: - _LOGGER.warning( - 'Device %s is not registered with firetv-server', device_id) + _LOGGER.warning('Device %s is not registered with firetv-server', + device_id) except requests.exceptions.RequestException: _LOGGER.error('Could not connect to firetv-server at %s', host) @@ -62,9 +71,10 @@ class FireTV(object): be running via Python 2). """ - def __init__(self, host, device_id): + def __init__(self, host, port, device_id): """Initialize the FireTV server.""" self.host = host + self.port = port self.device_id = device_id @property @@ -73,10 +83,7 @@ class FireTV(object): try: response = requests.get( DEVICE_STATE_URL.format( - self.host, - self.device_id - ) - ).json() + self.host, self.port, self.device_id), timeout=10).json() return response.get('state', STATE_UNKNOWN) except requests.exceptions.RequestException: _LOGGER.error( @@ -86,13 +93,8 @@ class FireTV(object): def action(self, action_id): """Perform an action on the device.""" try: - requests.get( - DEVICE_ACTION_URL.format( - self.host, - self.device_id, - action_id - ) - ) + requests.get(DEVICE_ACTION_URL.format( + self.host, self.port, self.device_id, action_id), timeout=10) except requests.exceptions.RequestException: _LOGGER.error( 'Action request for %s was not accepted for device %s', @@ -103,9 +105,9 @@ class FireTVDevice(MediaPlayerDevice): """Representation of an Amazon Fire TV device on the network.""" # pylint: disable=abstract-method - def __init__(self, host, device, name): + def __init__(self, host, port, device, name): """Initialize the FireTV device.""" - self._firetv = FireTV(host, device) + self._firetv = FireTV(host, port, device) self._name = name self._state = STATE_UNKNOWN diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index 4fcdff872e2..430db46bca2 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -9,24 +9,41 @@ import json import os import socket +import voluptuous as vol + from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, - SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_SEEK, MediaPlayerDevice) + SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_SEEK, MediaPlayerDevice, + PLATFORM_SCHEMA) from homeassistant.const import ( - STATE_PLAYING, STATE_PAUSED, STATE_OFF) + STATE_PLAYING, STATE_PAUSED, STATE_OFF, CONF_HOST, CONF_PORT, CONF_NAME) from homeassistant.loader import get_component +import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['websocket-client==0.37.0'] + +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'GPM Desktop Player' +DEFAULT_PORT = 5672 + +GPMDP_CONFIG_FILE = 'gpmpd.conf' + SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_SEEK | SUPPORT_VOLUME_SET -GPMDP_CONFIG_FILE = 'gpmpd.conf' -_CONFIGURING = {} PLAYBACK_DICT = {'0': STATE_PAUSED, # Stopped '1': STATE_PAUSED, '2': STATE_PLAYING} +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + def request_configuration(hass, config, url, add_devices_callback): """Request configuration steps from the user.""" @@ -78,7 +95,7 @@ def request_configuration(hass, config, url, add_devices_callback): break _CONFIGURING['gpmdp'] = configurator.request_config( - hass, "GPM Desktop Player", gpmdp_configuration_callback, + hass, DEFAULT_NAME, gpmdp_configuration_callback, description=( 'Enter the pin that is displayed in the ' 'Google Play Music Desktop Player.'), @@ -87,21 +104,22 @@ def request_configuration(hass, config, url, add_devices_callback): ) -def setup_gpmdp(hass, config, code, add_devices_callback): +def setup_gpmdp(hass, config, code, add_devices): """Setup gpmdp.""" - name = config.get("name", "GPM Desktop Player") - address = config.get("address") - url = "ws://" + address + ":5672" + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + url = 'ws://{}:{}'.format(host, port) if not code: - request_configuration(hass, config, url, add_devices_callback) + request_configuration(hass, config, url, add_devices) return if 'gpmdp' in _CONFIGURING: configurator = get_component('configurator') configurator.request_done(_CONFIGURING.pop('gpmdp')) - add_devices_callback([GPMDP(name, url, code)]) + add_devices([GPMDP(name, url, code)]) def _load_config(filename): @@ -110,7 +128,7 @@ def _load_config(filename): return {} try: - with open(filename, "r") as fdesc: + with open(filename, 'r') as fdesc: inp = fdesc.read() # In case empty file @@ -126,10 +144,10 @@ def _load_config(filename): def _save_config(filename, config): """Save configuration.""" try: - with open(filename, "w") as fdesc: + with open(filename, 'w') as fdesc: fdesc.write(json.dumps(config, indent=4, sort_keys=True)) except (IOError, TypeError) as error: - _LOGGER.error("Saving config file failed: %s", error) + _LOGGER.error("Saving configuration file failed: %s", error) return False return True @@ -138,7 +156,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the GPMDP platform.""" codeconfig = _load_config(hass.config.path(GPMDP_CONFIG_FILE)) if len(codeconfig): - code = codeconfig.get("CODE") + code = codeconfig.get('CODE') elif discovery_info is not None: if 'gpmdp' in _CONFIGURING: return @@ -258,7 +276,7 @@ class GPMDP(MediaPlayerDevice): @property def media_seek_position(self): - """Time in seconds of current seek positon.""" + """Time in seconds of current seek position.""" return self._seek_position @property @@ -306,9 +324,9 @@ class GPMDP(MediaPlayerDevice): websocket = self.get_ws() if websocket is None: return - websocket.send(json.dumps({"namespace": "playback", - "method": "setCurrentTime", - "arguments": [position*1000]})) + websocket.send(json.dumps({'namespace': 'playback', + 'method': 'setCurrentTime', + 'arguments': [position*1000]})) self.update_ha_state() def volume_up(self): @@ -332,7 +350,7 @@ class GPMDP(MediaPlayerDevice): websocket = self.get_ws() if websocket is None: return - websocket.send(json.dumps({"namespace": "volume", - "method": "setVolume", - "arguments": [volume*100]})) + websocket.send(json.dumps({'namespace': 'volume', + 'method': 'setVolume', + 'arguments': [volume*100]})) self.update_ha_state() diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index e28d84417d6..224f0d48827 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -7,24 +7,45 @@ https://home-assistant.io/components/media_player.kodi/ import logging import urllib +import voluptuous as vol + from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, - SUPPORT_TURN_OFF, MediaPlayerDevice) + SUPPORT_TURN_OFF, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( - STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) + STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, + CONF_PORT, CONF_USERNAME, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['jsonrpc-requests==0.3'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['jsonrpc-requests==0.3'] + +CONF_TURN_OFF_ACTION = 'turn_off_action' + +DEFAULT_NAME = 'Kodi' +DEFAULT_PORT = 8080 + +TURN_OFF_ACTION = [None, 'quit', 'hibernate', 'suspend', 'reboot', 'shutdown'] SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ SUPPORT_PLAY_MEDIA | SUPPORT_STOP +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION), + vol.Optional(CONF_USERNAME): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Kodi platform.""" - url = '{}:{}'.format(config.get('host'), config.get('port', '8080')) + url = '{}:{}'.format(config.get(CONF_HOST), config.get(CONF_PORT)) jsonrpc_url = config.get('url') # deprecated if jsonrpc_url: @@ -32,12 +53,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ KodiDevice( - config.get('name', 'Kodi'), + config.get(CONF_NAME), url, - auth=( - config.get('user', ''), - config.get('password', '')), - turn_off_action=config.get('turn_off_action', 'none')), + auth=(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)), + turn_off_action=config.get(CONF_TURN_OFF_ACTION)), ]) @@ -176,19 +195,14 @@ class KodiDevice(MediaPlayerDevice): if self._item is not None: return self._item.get( 'title', - self._item.get( - 'label', - self._item.get( - 'file', - 'unknown'))) + self._item.get('label', self._item.get('file', 'unknown'))) @property def supported_media_commands(self): """Flag of media commands that are supported.""" supported_media_commands = SUPPORT_KODI - if self._turn_off_action in [ - 'quit', 'hibernate', 'suspend', 'reboot', 'shutdown']: + if self._turn_off_action in TURN_OFF_ACTION: supported_media_commands |= SUPPORT_TURN_OFF return supported_media_commands diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py index fc0609a7c34..26b7341f747 100644 --- a/homeassistant/components/media_player/lg_netcast.py +++ b/homeassistant/components/media_player/lg_netcast.py @@ -9,32 +9,32 @@ import logging from requests import RequestException import voluptuous as vol + import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( - SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, MEDIA_TYPE_CHANNEL, MediaPlayerDevice) from homeassistant.const import ( - CONF_PLATFORM, CONF_HOST, CONF_NAME, CONF_ACCESS_TOKEN, + CONF_HOST, CONF_NAME, CONF_ACCESS_TOKEN, STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) import homeassistant.util as util -_LOGGER = logging.getLogger(__name__) - REQUIREMENTS = ['https://github.com/wokar/pylgnetcast/archive/' 'v0.2.0.zip#pylgnetcast==0.2.0'] +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'LG TV Remote' + +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) + SUPPORT_LGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) - -DEFAULT_NAME = 'LG TV Remote' - -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): "lg_netcast", +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_ACCESS_TOKEN, default=None): @@ -46,7 +46,9 @@ PLATFORM_SCHEMA = vol.Schema({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the LG TV platform.""" from pylgnetcast import LgNetCastClient - client = LgNetCastClient(config[CONF_HOST], config[CONF_ACCESS_TOKEN]) + client = LgNetCastClient(config.get(CONF_HOST), + config.get(CONF_ACCESS_TOKEN)) + add_devices([LgTVDevice(client, config[CONF_NAME])]) diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index 8f551f8ae8f..8563b551a09 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -6,29 +6,39 @@ https://home-assistant.io/components/media_player.mpchc/ """ import logging import re + import requests +import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_NEXT_TRACK, - SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_STEP, MediaPlayerDevice) + SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_STEP, MediaPlayerDevice, + PLATFORM_SCHEMA) from homeassistant.const import ( - STATE_OFF, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) + STATE_OFF, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, CONF_NAME, CONF_HOST, + CONF_PORT) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = 'MPC-HC' +DEFAULT_PORT = 13579 + SUPPORT_MPCHC = SUPPORT_VOLUME_MUTE | SUPPORT_PAUSE | SUPPORT_STOP | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_STEP +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the MPC-HC platform.""" - name = config.get("name", "MPC-HC") - url = '{}:{}'.format(config.get('host'), config.get('port', '13579')) - - if config.get('host') is None: - _LOGGER.error("Missing NPC-HC host address in config") - return False + name = config.get(CONF_NAME) + url = '{}:{}'.format(config.get(CONF_HOST), config.get(CONF_PORT)) add_devices([MpcHcDevice(name, url)]) @@ -49,7 +59,7 @@ class MpcHcDevice(MediaPlayerDevice): self._player_variables = dict() try: - response = requests.get("{}/variables.html".format(self._url), + response = requests.get('{}/variables.html'.format(self._url), data=None, timeout=3) mpchc_variables = re.findall(r'

(.+?)

', diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index c04184d6bda..56af3cd88f9 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -7,28 +7,46 @@ https://home-assistant.io/components/media_player.mpd/ import logging import socket +import voluptuous as vol + from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_PLAYLIST, MediaPlayerDevice) -from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import ( + STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD, + CONF_HOST) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['python-mpd2==0.5.5'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-mpd2==0.5.5'] + +CONF_LOCATION = 'location' + +DEFAULT_LOCATION = 'MPD' +DEFAULT_PORT = 6600 SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_PLAY_MEDIA +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the MPD platform.""" - daemon = config.get('server', None) - port = config.get('port', 6600) - location = config.get('location', 'MPD') - password = config.get('password', None) + daemon = config.get(CONF_HOST) + port = config.get(CONF_PORT) + location = config.get(CONF_LOCATION) + password = config.get(CONF_PASSWORD) import mpd @@ -43,18 +61,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): mpd_client.close() mpd_client.disconnect() except socket.error: - _LOGGER.error( - "Unable to connect to MPD. " - "Please check your settings") - + _LOGGER.error("Unable to connect to MPD") return False except mpd.CommandError as error: if "incorrect password" in str(error): - _LOGGER.error( - "MPD reported incorrect password. " - "Please check your password.") - + _LOGGER.error("MPD reported incorrect password") return False else: raise @@ -65,7 +77,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MpdDevice(MediaPlayerDevice): """Representation of a MPD server.""" - # MPD confuses pylint # pylint: disable=no-member, too-many-public-methods, abstract-method def __init__(self, server, port, location, password): """Initialize the MPD device.""" diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index d1b5282fa6e..53cd0b68df9 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -6,55 +6,69 @@ https://home-assistant.io/components/media_player.onkyo/ """ import logging +import voluptuous as vol + from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, MediaPlayerDevice) -from homeassistant.const import STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME + SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['https://github.com/danieljkemp/onkyo-eiscp/archive/' 'python3.zip#onkyo-eiscp==0.9.2'] + _LOGGER = logging.getLogger(__name__) +CONF_SOURCES = 'sources' + +DEFAULT_NAME = 'Onkyo Receiver' + +KNOWN_HOSTS = [] + SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE -KNOWN_HOSTS = [] -DEFAULT_SOURCES = {"tv": "TV", "bd": "Bluray", "game": "Game", "aux1": "Aux1", - "video1": "Video 1", "video2": "Video 2", - "video3": "Video 3", "video4": "Video 4", - "video5": "Video 5", "video6": "Video 6", - "video7": "Video 7"} -CONFIG_SOURCE_LIST = "sources" + +DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', + 'video1': 'Video 1', 'video2': 'Video 2', + 'video3': 'Video 3', 'video4': 'Video 4', + 'video5': 'Video 5', 'video6': 'Video 6', + 'video7': 'Video 7'} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): + {cv.string: cv.string}, +}) def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Onkyo platform.""" import eiscp from eiscp import eISCP + + host = config.get(CONF_HOST) hosts = [] - if CONF_HOST in config and config[CONF_HOST] not in KNOWN_HOSTS: + if CONF_HOST in config and host not in KNOWN_HOSTS: try: - hosts.append(OnkyoDevice(eiscp.eISCP(config[CONF_HOST]), - config.get(CONFIG_SOURCE_LIST, - DEFAULT_SOURCES), - name=config[CONF_NAME])) - KNOWN_HOSTS.append(config[CONF_HOST]) + hosts.append(OnkyoDevice(eiscp.eISCP(host), + config.get(CONF_SOURCES), + name=config.get(CONF_NAME))) + KNOWN_HOSTS.append(host) except OSError: - _LOGGER.error('Unable to connect to receiver at %s.', - config[CONF_HOST]) + _LOGGER.error('Unable to connect to receiver at %s.', host) else: for receiver in eISCP.discover(): if receiver.host not in KNOWN_HOSTS: - hosts.append(OnkyoDevice(receiver, - config.get(CONFIG_SOURCE_LIST, - DEFAULT_SOURCES))) + hosts.append(OnkyoDevice(receiver, config.get(CONF_SOURCES))) KNOWN_HOSTS.append(receiver.host) add_devices(hosts) # pylint: disable=too-many-instance-attributes class OnkyoDevice(MediaPlayerDevice): - """Representation of a Onkyo device.""" + """Representation of an Onkyo device.""" # pylint: disable=too-many-public-methods, abstract-method def __init__(self, receiver, sources, name=None): @@ -90,7 +104,7 @@ class OnkyoDevice(MediaPlayerDevice): self._current_source = '_'.join( [i for i in current_source_raw[1]]) self._muted = bool(mute_raw[1] == 'on') - self._volume = int(volume_raw[1], 16)/80.0 + self._volume = int(volume_raw[1], 16) / 80.0 @property def name(self): diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index ddc547ff807..488e4e6b9d8 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -7,33 +7,42 @@ https://home-assistant.io/components/media_player.panasonic_viera/ import logging import socket -from homeassistant.components.media_player import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, MediaPlayerDevice) -from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) -from homeassistant.helpers import validate_config +import voluptuous as vol -CONF_PORT = "port" +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['panasonic_viera==0.2'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['panasonic_viera==0.2'] +DEFAULT_NAME = 'Panasonic Viera TV' +DEFAULT_PORT = 55000 SUPPORT_VIERATV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_TURN_OFF +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Panasonic Viera TV platform.""" - from panasonic_viera import DEFAULT_PORT, RemoteControl + from panasonic_viera import RemoteControl - name = config.get(CONF_NAME, 'Panasonic Viera TV') - port = config.get(CONF_PORT, DEFAULT_PORT) + name = config.get(CONF_NAME) + port = config.get(CONF_PORT) if discovery_info: _LOGGER.debug('%s', discovery_info) @@ -46,13 +55,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([PanasonicVieraTVDevice(name, remote)]) return True - # Validate that all required config options are given - if not validate_config({DOMAIN: config}, {DOMAIN: [CONF_HOST]}, _LOGGER): - return False - - host = config.get(CONF_HOST, None) - + host = config.get(CONF_HOST) remote = RemoteControl(host, port) + try: remote.get_mute() except (socket.timeout, TimeoutError, OSError): diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py index 207e38ecf40..599edf08b37 100644 --- a/homeassistant/components/media_player/pioneer.py +++ b/homeassistant/components/media_player/pioneer.py @@ -7,36 +7,35 @@ https://home-assistant.io/components/media_player.pioneer/ import logging import telnetlib +import voluptuous as vol + from homeassistant.components.media_player import ( - DOMAIN, SUPPORT_PAUSE, SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - MediaPlayerDevice) + SUPPORT_PAUSE, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( - CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, - CONF_NAME) + CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_NAME) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = 'Pioneer AVR' + SUPPORT_PIONEER = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE MAX_VOLUME = 185 MAX_SOURCE_NUMBERS = 60 +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Pioneer platform.""" - if not config.get(CONF_HOST): - _LOGGER.error( - "Missing required configuration items in %s: %s", - DOMAIN, - CONF_HOST) - return False + pioneer = PioneerDevice(config.get(CONF_NAME), config.get(CONF_HOST)) - pioneer = PioneerDevice( - config.get(CONF_NAME, "Pioneer AVR"), - config.get(CONF_HOST) - ) if pioneer.update(): add_devices([pioneer]) return True @@ -53,7 +52,7 @@ class PioneerDevice(MediaPlayerDevice): """Initialize the Pioneer device.""" self._name = name self._host = host - self._pwstate = "PWR1" + self._pwstate = 'PWR1' self._volume = 0 self._muted = False self._selected_source = '' diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 7951530e3e8..e7a87d2d773 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -6,13 +6,15 @@ https://home-assistant.io/components/media_player.roku/ """ import logging +import voluptuous as vol + from homeassistant.components.media_player import ( MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, MediaPlayerDevice) - + SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = [ 'https://github.com/bah2830/python-roku/archive/3.1.2.zip' @@ -27,6 +29,10 @@ SUPPORT_ROKU = SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\ SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_SELECT_SOURCE +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST): cv.string, +}) + # pylint: disable=abstract-method def setup_platform(hass, config, add_devices, discovery_info=None): @@ -41,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hosts.append(discovery_info[0]) elif CONF_HOST in config: - hosts.append(config[CONF_HOST]) + hosts.append(config.get(CONF_HOST)) rokus = [] for host in hosts: diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 61768b91f96..5c096c86bb0 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -7,44 +7,51 @@ https://home-assistant.io/components/media_player.samsungtv/ import logging import socket -from homeassistant.components.media_player import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - MediaPlayerDevice) -from homeassistant.const import ( - CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) -from homeassistant.helpers import validate_config +import voluptuous as vol -CONF_PORT = "port" -CONF_TIMEOUT = "timeout" +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + MediaPlayerDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['samsungctl==0.5.1'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['samsungctl==0.5.1'] +CONF_TIMEOUT = 'timeout' + +DEFAULT_NAME = 'Samsung TV Remote' +DEFAULT_PORT = 55000 +DEFAULT_TIMEOUT = 0 SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, +}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Samsung TV platform.""" - # Validate that all required config options are given - if not validate_config({DOMAIN: config}, {DOMAIN: [CONF_HOST]}, _LOGGER): - return False + name = config.get(CONF_NAME) - # Default the entity_name to 'Samsung TV Remote' - name = config.get(CONF_NAME, 'Samsung TV Remote') - - # Generate a config for the Samsung lib + # Generate a configuration for the Samsung library remote_config = { - "name": "HomeAssistant", - "description": config.get(CONF_NAME, ''), - "id": "ha.component.samsung", - "port": config.get(CONF_PORT, 55000), - "host": config.get(CONF_HOST), - "timeout": config.get(CONF_TIMEOUT, 0), + 'name': 'HomeAssistant', + 'description': config.get(CONF_NAME), + 'id': 'ha.component.samsung', + 'port': config.get(CONF_PORT), + 'host': config.get(CONF_HOST), + 'timeout': config.get(CONF_TIMEOUT), } add_devices([SamsungTVDevice(name, remote_config)]) @@ -56,7 +63,7 @@ class SamsungTVDevice(MediaPlayerDevice): # pylint: disable=too-many-public-methods def __init__(self, name, config): - """Initialize the samsung device.""" + """Initialize the Samsung device.""" from samsungctl import Remote # Save a reference to the imported class self._remote_class = Remote @@ -124,19 +131,19 @@ class SamsungTVDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" - self.send_key("KEY_POWEROFF") + self.send_key('KEY_POWEROFF') def volume_up(self): """Volume up the media player.""" - self.send_key("KEY_VOLUP") + self.send_key('KEY_VOLUP') def volume_down(self): """Volume down media player.""" - self.send_key("KEY_VOLDOWN") + self.send_key('KEY_VOLDOWN') def mute_volume(self, mute): """Send mute command.""" - self.send_key("KEY_MUTE") + self.send_key('KEY_MUTE') def media_play_pause(self): """Simulate play pause media player.""" @@ -148,21 +155,21 @@ class SamsungTVDevice(MediaPlayerDevice): def media_play(self): """Send play command.""" self._playing = True - self.send_key("KEY_PLAY") + self.send_key('KEY_PLAY') def media_pause(self): """Send media pause command to media player.""" self._playing = False - self.send_key("KEY_PAUSE") + self.send_key('KEY_PAUSE') def media_next_track(self): """Send next track command.""" - self.send_key("KEY_FF") + self.send_key('KEY_FF') def media_previous_track(self): """Send the previous track command.""" - self.send_key("KEY_REWIND") + self.send_key('KEY_REWIND') def turn_on(self): """Turn the media player on.""" - self.send_key("KEY_POWERON") + self.send_key('KEY_POWERON') diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 998490fb9b9..2be3c36816c 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -7,35 +7,45 @@ https://home-assistant.io/components/media_player.snapcast/ import logging import socket +import voluptuous as vol + from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, - MediaPlayerDevice) + PLATFORM_SCHEMA, MediaPlayerDevice) from homeassistant.const import ( - STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN) + STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['snapcast==1.2.2'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'snapcast' SUPPORT_SNAPCAST = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_SELECT_SOURCE -DOMAIN = 'snapcast' -REQUIREMENTS = ['snapcast==1.2.1'] -_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Snapcast platform.""" import snapcast.control - host = config.get('host') - port = config.get('port', snapcast.control.CONTROL_PORT) - if not host: - _LOGGER.error('No snapserver host specified') - return + host = config.get(CONF_HOST) + port = config.get(CONF_PORT, snapcast.control.CONTROL_PORT) + try: server = snapcast.control.Snapserver(host, port) except socket.gaierror: _LOGGER.error('Could not connect to Snapcast server at %s:%d', host, port) - return + return False + add_devices([SnapcastDevice(client) for client in server.clients]) @@ -75,18 +85,18 @@ class SnapcastDevice(MediaPlayerDevice): return { 'idle': STATE_IDLE, 'playing': STATE_PLAYING, - 'unkown': STATE_UNKNOWN, + 'unknown': STATE_UNKNOWN, }.get(self._client.stream.status, STATE_UNKNOWN) @property def source(self): """Return the current input source.""" - return self._client.stream.identifier + return self._client.stream.name @property def source_list(self): """List of available input sources.""" - return self._client.available_streams() + return list(self._client.streams_by_name().keys()) def mute_volume(self, mute): """Send the mute command.""" @@ -98,4 +108,6 @@ class SnapcastDevice(MediaPlayerDevice): def select_source(self, source): """Set input source.""" - self._client.stream = source + streams = self._client.streams_by_name() + if source in streams: + self._client.stream = streams[source].identifier diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 62b2aeabf51..5fc0166aefa 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -199,6 +199,7 @@ class SonosDevice(MediaPlayerDevice): self.hass = hass self.volume_increment = 5 self._player = player + self._name = None self.update() self.soco_snapshot = Snapshot(self._player) @@ -216,11 +217,6 @@ class SonosDevice(MediaPlayerDevice): """Return the name of the device.""" return self._name - @property - def unique_id(self): - """Return a unique ID.""" - return "{}.{}".format(self.__class__, self._player.uid) - @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index d49eba609e1..2f8c214fe3a 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -8,48 +8,53 @@ import logging import telnetlib import urllib.parse +import voluptuous as vol + from homeassistant.components.media_player import ( - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_IDLE, STATE_OFF, - STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) + STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, CONF_PORT) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DEFAULT_PORT = 9090 + +KNOWN_DEVICES = [] + SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF -KNOWN_DEVICES = [] +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, +}) def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the squeezebox platform.""" + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + if discovery_info is not None: host = discovery_info[0] - port = 9090 + port = DEFAULT_PORT else: host = config.get(CONF_HOST) - port = int(config.get('port', 9090)) - - if not host: - _LOGGER.error( - "Missing required configuration items in %s: %s", - DOMAIN, - CONF_HOST) - return False + port = config.get(CONF_PORT) # Only add a media server once if host in KNOWN_DEVICES: return False KNOWN_DEVICES.append(host) - lms = LogitechMediaServer( - host, port, - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) + lms = LogitechMediaServer(host, port, username, password) if not lms.init_success: return False @@ -77,18 +82,13 @@ class LogitechMediaServer(object): try: http_port = self.query('pref', 'httpport', '?') if not http_port: - _LOGGER.error( - "Unable to read data from server %s:%s", - self.host, - self.port) + _LOGGER.error("Unable to read data from server %s:%s", + self.host, self.port) return return http_port except ConnectionError as ex: - _LOGGER.error( - "Failed to connect to server %s:%s - %s", - self.host, - self.port, - ex) + _LOGGER.error("Failed to connect to server %s:%s - %s", + self.host, self.port, ex) return def create_players(self): diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index 1d6ad0e3abc..b0391f9ba45 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/modbus/ """ import logging +import threading from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) @@ -37,7 +38,7 @@ ATTR_ADDRESS = "address" ATTR_UNIT = "unit" ATTR_VALUE = "value" -NETWORK = None +HUB = None TYPE = None @@ -50,46 +51,116 @@ def setup(hass, config): # Connect to Modbus network # pylint: disable=global-statement, import-error - global NETWORK if TYPE == "serial": from pymodbus.client.sync import ModbusSerialClient as ModbusClient - NETWORK = ModbusClient(method=config[DOMAIN][METHOD], - port=config[DOMAIN][SERIAL_PORT], - baudrate=config[DOMAIN][BAUDRATE], - stopbits=config[DOMAIN][STOPBITS], - bytesize=config[DOMAIN][BYTESIZE], - parity=config[DOMAIN][PARITY]) + client = ModbusClient(method=config[DOMAIN][METHOD], + port=config[DOMAIN][SERIAL_PORT], + baudrate=config[DOMAIN][BAUDRATE], + stopbits=config[DOMAIN][STOPBITS], + bytesize=config[DOMAIN][BYTESIZE], + parity=config[DOMAIN][PARITY]) elif TYPE == "tcp": from pymodbus.client.sync import ModbusTcpClient as ModbusClient - NETWORK = ModbusClient(host=config[DOMAIN][HOST], - port=config[DOMAIN][IP_PORT]) + client = ModbusClient(host=config[DOMAIN][HOST], + port=config[DOMAIN][IP_PORT]) elif TYPE == "udp": from pymodbus.client.sync import ModbusUdpClient as ModbusClient - NETWORK = ModbusClient(host=config[DOMAIN][HOST], - port=config[DOMAIN][IP_PORT]) + client = ModbusClient(host=config[DOMAIN][HOST], + port=config[DOMAIN][IP_PORT]) else: return False + global HUB + HUB = ModbusHub(client) + def stop_modbus(event): """Stop Modbus service.""" - NETWORK.close() + HUB.close() def start_modbus(event): """Start Modbus service.""" - NETWORK.connect() + HUB.connect() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) # Register services for modbus hass.services.register(DOMAIN, SERVICE_WRITE_REGISTER, write_register) def write_register(service): - """Write modbus register.""" + """Write modbus registers.""" unit = int(float(service.data.get(ATTR_UNIT))) address = int(float(service.data.get(ATTR_ADDRESS))) - value = int(float(service.data.get(ATTR_VALUE))) - NETWORK.write_register(address, value, unit=unit) + value = service.data.get(ATTR_VALUE) + if isinstance(value, list): + HUB.write_registers( + unit, + address, + [int(float(i)) for i in value]) + else: + HUB.write_register( + unit, + address, + int(float(value))) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus) return True + + +class ModbusHub(object): + """Thread safe wrapper class for pymodbus.""" + + def __init__(self, modbus_client): + """Initialize the modbus hub.""" + self._client = modbus_client + self._lock = threading.Lock() + + def close(self): + """Disconnect client.""" + with self._lock: + self._client.close() + + def connect(self): + """Connect client.""" + with self._lock: + self._client.connect() + + def read_coils(self, unit, address, count): + """Read coils.""" + with self._lock: + return self._client.read_coils( + address, + count, + unit=unit) + + def read_holding_registers(self, unit, address, count): + """Read holding registers.""" + with self._lock: + return self._client.read_holding_registers( + address, + count, + unit=unit) + + def write_coil(self, unit, address, value): + """Write coil.""" + with self._lock: + self._client.write_coil( + address, + value, + unit=unit) + + def write_register(self, unit, address, value): + """Write register.""" + with self._lock: + self._client.write_register( + address, + value, + unit=unit) + + def write_registers(self, unit, address, values): + """Write registers.""" + with self._lock: + self._client.write_registers( + address, + values, + unit=unit) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e06f60b6e1a..6cf8ed047ee 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -170,9 +170,14 @@ def subscribe(hass, topic, callback, qos=DEFAULT_QOS): callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD], event.data[ATTR_QOS]) - hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED, mqtt_topic_subscriber) + remove = hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED, + mqtt_topic_subscriber) + + # Future: track subscriber count and unsubscribe in remove MQTT_CLIENT.subscribe(topic, qos) + return remove + def _setup_server(hass, config): """Try to start embedded MQTT broker.""" diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 430b9baa956..d875ab0e2c0 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -1,18 +1,21 @@ """ -Support for Nest thermostats and protect smoke alarms. +Support for Nest devices. -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.nest/ +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/nest/ """ import logging import socket import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE) + +_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['python-nest==2.9.2'] + DOMAIN = 'nest' NEST = None @@ -21,14 +24,12 @@ STRUCTURES_TO_INCLUDE = None CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, cv.string) }) }, extra=vol.ALLOW_EXTRA) -_LOGGER = logging.getLogger(__name__) - def devices(): """Generator returning list of devices and their location.""" diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index a808985ae0e..f56c9b515b9 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -12,17 +12,15 @@ from homeassistant.helpers import validate_config, discovery REQUIREMENTS = [ 'https://github.com/jabesq/netatmo-api-python/archive/' - 'v0.5.0.zip#lnetatmo==0.5.0'] + 'master.zip#lnetatmo==0.5.0'] _LOGGER = logging.getLogger(__name__) CONF_SECRET_KEY = 'secret_key' -DOMAIN = "netatmo" +DOMAIN = 'netatmo' NETATMO_AUTH = None -_LOGGER = logging.getLogger(__name__) - def setup(hass, config): """Setup the Netatmo devices.""" diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index a28a50d766f..9fb6ca1f842 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = vol.Schema({ NOTIFY_SERVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_MESSAGE): cv.template, - vol.Optional(ATTR_TITLE, default=ATTR_TITLE_DEFAULT): cv.string, + vol.Optional(ATTR_TITLE): cv.string, vol.Optional(ATTR_TARGET): cv.string, vol.Optional(ATTR_DATA): dict, }) @@ -91,19 +91,22 @@ def setup(hass, config): def notify_message(notify_service, call): """Handle sending notification message service calls.""" + kwargs = {} message = call.data[ATTR_MESSAGE] + title = call.data.get(ATTR_TITLE) + + if title: + kwargs[ATTR_TITLE] = template.render(hass, title) - title = template.render( - hass, call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)) if targets.get(call.service) is not None: - target = targets[call.service] + kwargs[ATTR_TARGET] = targets[call.service] else: - target = call.data.get(ATTR_TARGET) - message = template.render(hass, message) - data = call.data.get(ATTR_DATA) + kwargs[ATTR_TARGET] = call.data.get(ATTR_TARGET) - notify_service.send_message(message, title=title, target=target, - data=data) + kwargs[ATTR_MESSAGE] = template.render(hass, message) + kwargs[ATTR_DATA] = call.data.get(ATTR_DATA) + + notify_service.send_message(**kwargs) service_call_handler = partial(notify_message, notify_service) diff --git a/homeassistant/components/notify/aws_lambda.py b/homeassistant/components/notify/aws_lambda.py index 68f0de7a934..3e3003763ea 100644 --- a/homeassistant/components/notify/aws_lambda.py +++ b/homeassistant/components/notify/aws_lambda.py @@ -7,29 +7,30 @@ https://home-assistant.io/components/notify.aws_lambda/ import logging import json import base64 + import voluptuous as vol from homeassistant.const import ( CONF_PLATFORM, CONF_NAME) from homeassistant.components.notify import ( - ATTR_TARGET, BaseNotificationService) + ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ["boto3==1.3.1"] -CONF_REGION = "region_name" -CONF_ACCESS_KEY_ID = "aws_access_key_id" -CONF_SECRET_ACCESS_KEY = "aws_secret_access_key" -CONF_PROFILE_NAME = "profile_name" -CONF_CONTEXT = "context" +CONF_REGION = 'region_name' +CONF_ACCESS_KEY_ID = 'aws_access_key_id' +CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key' +CONF_PROFILE_NAME = 'profile_name' +CONF_CONTEXT = 'context' +ATTR_CREDENTIALS = 'credentials' -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): "aws_lambda", - vol.Optional(CONF_NAME): vol.Coerce(str), - vol.Optional(CONF_REGION, default="us-east-1"): vol.Coerce(str), - vol.Inclusive(CONF_ACCESS_KEY_ID, "credentials"): vol.Coerce(str), - vol.Inclusive(CONF_SECRET_ACCESS_KEY, "credentials"): vol.Coerce(str), - vol.Exclusive(CONF_PROFILE_NAME, "credentials"): vol.Coerce(str), +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_REGION, default="us-east-1"): cv.string, + vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, + vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, vol.Optional(CONF_CONTEXT, default=dict()): vol.Coerce(dict) }) diff --git a/homeassistant/components/notify/aws_sns.py b/homeassistant/components/notify/aws_sns.py index dec72b18633..88233234eca 100644 --- a/homeassistant/components/notify/aws_sns.py +++ b/homeassistant/components/notify/aws_sns.py @@ -6,28 +6,30 @@ https://home-assistant.io/components/notify.aws_sns/ """ import logging import json + import voluptuous as vol from homeassistant.const import ( CONF_PLATFORM, CONF_NAME) from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TARGET, BaseNotificationService) + ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_TARGET, PLATFORM_SCHEMA, + BaseNotificationService) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ["boto3==1.3.1"] -CONF_REGION = "region_name" -CONF_ACCESS_KEY_ID = "aws_access_key_id" -CONF_SECRET_ACCESS_KEY = "aws_secret_access_key" -CONF_PROFILE_NAME = "profile_name" +CONF_REGION = 'region_name' +CONF_ACCESS_KEY_ID = 'aws_access_key_id' +CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key' +CONF_PROFILE_NAME = 'profile_name' +ATTR_CREDENTIALS = 'credentials' -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): "aws_sns", - vol.Optional(CONF_NAME): vol.Coerce(str), - vol.Optional(CONF_REGION, default="us-east-1"): vol.Coerce(str), - vol.Inclusive(CONF_ACCESS_KEY_ID, "credentials"): vol.Coerce(str), - vol.Inclusive(CONF_SECRET_ACCESS_KEY, "credentials"): vol.Coerce(str), - vol.Exclusive(CONF_PROFILE_NAME, "credentials"): vol.Coerce(str) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_REGION, default="us-east-1"): cv.string, + vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, + vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, }) @@ -76,5 +78,6 @@ class AWSSNS(BaseNotificationService): for k, v in kwargs.items() if v} for target in targets: self.client.publish(TargetArn=target, Message=message, - Subject=kwargs.get(ATTR_TITLE), + Subject=kwargs.get(ATTR_TITLE, + ATTR_TITLE_DEFAULT), MessageAttributes=message_attributes) diff --git a/homeassistant/components/notify/aws_sqs.py b/homeassistant/components/notify/aws_sqs.py index a600878cda7..a1ddbcea3dd 100644 --- a/homeassistant/components/notify/aws_sqs.py +++ b/homeassistant/components/notify/aws_sqs.py @@ -6,28 +6,29 @@ https://home-assistant.io/components/notify.aws_sqs/ """ import logging import json + import voluptuous as vol from homeassistant.const import ( CONF_PLATFORM, CONF_NAME) from homeassistant.components.notify import ( - ATTR_TARGET, BaseNotificationService) + ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ["boto3==1.3.1"] -CONF_REGION = "region_name" -CONF_ACCESS_KEY_ID = "aws_access_key_id" -CONF_SECRET_ACCESS_KEY = "aws_secret_access_key" -CONF_PROFILE_NAME = "profile_name" +CONF_REGION = 'region_name' +CONF_ACCESS_KEY_ID = 'aws_access_key_id' +CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key' +CONF_PROFILE_NAME = 'profile_name' +ATTR_CREDENTIALS = 'credentials' -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): "aws_sqs", - vol.Optional(CONF_NAME): vol.Coerce(str), - vol.Optional(CONF_REGION, default="us-east-1"): vol.Coerce(str), - vol.Inclusive(CONF_ACCESS_KEY_ID, "credentials"): vol.Coerce(str), - vol.Inclusive(CONF_SECRET_ACCESS_KEY, "credentials"): vol.Coerce(str), - vol.Exclusive(CONF_PROFILE_NAME, "credentials"): vol.Coerce(str) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_REGION, default="us-east-1"): cv.string, + vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, + vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, }) diff --git a/homeassistant/components/notify/command_line.py b/homeassistant/components/notify/command_line.py index df77560c22b..9b637d71188 100644 --- a/homeassistant/components/notify/command_line.py +++ b/homeassistant/components/notify/command_line.py @@ -6,21 +6,25 @@ https://home-assistant.io/components/notify.command_line/ """ import logging import subprocess -from homeassistant.helpers import validate_config + +import voluptuous as vol + +from homeassistant.const import (CONF_COMMAND, CONF_NAME) from homeassistant.components.notify import ( - DOMAIN, BaseNotificationService) + BaseNotificationService, PLATFORM_SCHEMA) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_NAME): cv.string, +}) + def get_service(hass, config): """Get the Command Line notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: ['command']}, - _LOGGER): - return None - - command = config['command'] + command = config[CONF_COMMAND] return CommandLineNotificationService(command) diff --git a/homeassistant/components/notify/ecobee.py b/homeassistant/components/notify/ecobee.py index 861d5439e4c..4ac4a9ca8db 100644 --- a/homeassistant/components/notify/ecobee.py +++ b/homeassistant/components/notify/ecobee.py @@ -5,16 +5,28 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.ecobee/ """ import logging + +import voluptuous as vol + from homeassistant.components import ecobee -from homeassistant.components.notify import BaseNotificationService +from homeassistant.components.notify import ( + BaseNotificationService, PLATFORM_SCHEMA) # NOQA +import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['ecobee'] _LOGGER = logging.getLogger(__name__) +CONF_INDEX = 'index' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_INDEX, default=0): cv.positive_int, +}) + + def get_service(hass, config): """Get the Ecobee notification service.""" - index = int(config['index']) if 'index' in config else 0 + index = config.get(CONF_INDEX) return EcobeeNotificationService(index) diff --git a/homeassistant/components/notify/file.py b/homeassistant/components/notify/file.py index 3d04bf13334..82ec2420df8 100644 --- a/homeassistant/components/notify/file.py +++ b/homeassistant/components/notify/file.py @@ -7,24 +7,28 @@ https://home-assistant.io/components/notify.file/ import logging import os +import voluptuous as vol + import homeassistant.util.dt as dt_util from homeassistant.components.notify import ( - ATTR_TITLE, DOMAIN, BaseNotificationService) -from homeassistant.helpers import validate_config + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_FILENAME +import homeassistant.helpers.config_validation as cv + +CONF_TIMESTAMP = 'timestamp' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FILENAME): cv.string, + vol.Optional(CONF_TIMESTAMP, default=False): cv.boolean, +}) _LOGGER = logging.getLogger(__name__) def get_service(hass, config): """Get the file notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: ['filename', - 'timestamp']}, - _LOGGER): - return None - - filename = config['filename'] - timestamp = config['timestamp'] + filename = config[CONF_FILENAME] + timestamp = config[CONF_TIMESTAMP] return FileNotificationService(hass, filename, timestamp) @@ -43,14 +47,13 @@ class FileNotificationService(BaseNotificationService): with open(self.filepath, 'a') as file: if os.stat(self.filepath).st_size == 0: title = '{} notifications (Log started: {})\n{}\n'.format( - kwargs.get(ATTR_TITLE), + kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), dt_util.utcnow().isoformat(), '-' * 80) file.write(title) - if self.add_timestamp == 1: + if self.add_timestamp: text = '{} {}\n'.format(dt_util.utcnow().isoformat(), message) - file.write(text) else: text = '{}\n'.format(message) - file.write(text) + file.write(text) diff --git a/homeassistant/components/notify/free_mobile.py b/homeassistant/components/notify/free_mobile.py index e12cc5893b8..e5209e06582 100644 --- a/homeassistant/components/notify/free_mobile.py +++ b/homeassistant/components/notify/free_mobile.py @@ -6,22 +6,25 @@ https://home-assistant.io/components/notify.free_mobile/ """ import logging -from homeassistant.components.notify import DOMAIN, BaseNotificationService +import voluptuous as vol + +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME -from homeassistant.helpers import validate_config +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['freesms==0.1.0'] +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, +}) + + def get_service(hass, config): """Get the Free Mobile SMS notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_USERNAME, - CONF_ACCESS_TOKEN]}, - _LOGGER): - return None - return FreeSMSNotificationService(config[CONF_USERNAME], config[CONF_ACCESS_TOKEN]) diff --git a/homeassistant/components/notify/gntp.py b/homeassistant/components/notify/gntp.py index 5b5d377e1ea..fa7db0d6e6e 100644 --- a/homeassistant/components/notify/gntp.py +++ b/homeassistant/components/notify/gntp.py @@ -7,8 +7,12 @@ https://home-assistant.io/components/notify.gntp/ import logging import os +import voluptuous as vol + from homeassistant.components.notify import ( - ATTR_TITLE, BaseNotificationService) + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_PASSWORD, CONF_PORT +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['gntp==1.0.3'] @@ -18,20 +22,37 @@ _GNTP_LOGGER = logging.getLogger('gntp') _GNTP_LOGGER.setLevel(logging.ERROR) +CONF_APP_NAME = 'app_name' +CONF_APP_ICON = 'app_icon' +CONF_HOSTNAME = 'hostname' + +DEFAULT_APP_NAME = 'HomeAssistant' +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 23053 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_APP_NAME, default=DEFAULT_APP_NAME): cv.string, + vol.Optional(CONF_APP_ICON): vol.Url, + vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + + def get_service(hass, config): """Get the GNTP notification service.""" - if config.get('app_icon') is None: + if config.get(CONF_APP_ICON) is None: icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend", "www_static", "icons", "favicon-192x192.png") app_icon = open(icon_file, 'rb').read() else: - app_icon = config.get('app_icon') + app_icon = config.get(CONF_APP_ICON) - return GNTPNotificationService(config.get('app_name', 'HomeAssistant'), - config.get('app_icon', app_icon), - config.get('hostname', 'localhost'), - config.get('password'), - config.get('port', 23053)) + return GNTPNotificationService(config.get(CONF_APP_NAME), + app_icon, + config.get(CONF_HOSTNAME), + config.get(CONF_PASSWORD), + config.get(CONF_PORT)) # pylint: disable=too-few-public-methods @@ -59,5 +80,6 @@ class GNTPNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - self.gntp.notify(noteType="Notification", title=kwargs.get(ATTR_TITLE), + self.gntp.notify(noteType="Notification", + title=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), description=message) diff --git a/homeassistant/components/notify/group.py b/homeassistant/components/notify/group.py index 522b231d8cf..0d480a9ddac 100644 --- a/homeassistant/components/notify/group.py +++ b/homeassistant/components/notify/group.py @@ -8,8 +8,9 @@ import collections import logging import voluptuous as vol -from homeassistant.const import (CONF_PLATFORM, CONF_NAME, ATTR_SERVICE) +from homeassistant.const import ATTR_SERVICE from homeassistant.components.notify import (DOMAIN, ATTR_MESSAGE, ATTR_DATA, + PLATFORM_SCHEMA, BaseNotificationService) import homeassistant.helpers.config_validation as cv @@ -17,9 +18,7 @@ _LOGGER = logging.getLogger(__name__) CONF_SERVICES = "services" -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): "group", - vol.Required(CONF_NAME): vol.Coerce(str), +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SERVICES): vol.All(cv.ensure_list, [{ vol.Required(ATTR_SERVICE): cv.slug, vol.Optional(ATTR_DATA): dict, diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 54727a60d3f..103ccc7885b 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -18,8 +18,8 @@ from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, URL_ROOT) from homeassistant.util import ensure_unique_string from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_TITLE, ATTR_DATA, BaseNotificationService, - PLATFORM_SCHEMA) + ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, + BaseNotificationService, PLATFORM_SCHEMA) from homeassistant.components.http import HomeAssistantView from homeassistant.components.frontend import add_manifest_json_key from homeassistant.helpers import config_validation as cv @@ -332,7 +332,7 @@ class HTML5NotificationService(BaseNotificationService): 'icon': '/static/icons/favicon-192x192.png', ATTR_TAG: tag, 'timestamp': (timestamp*1000), # Javascript ms since epoch - ATTR_TITLE: kwargs.get(ATTR_TITLE) + ATTR_TITLE: kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) } data = kwargs.get(ATTR_DATA) diff --git a/homeassistant/components/notify/instapush.py b/homeassistant/components/notify/instapush.py index 028afb32468..3a8f2d9ee0a 100644 --- a/homeassistant/components/notify/instapush.py +++ b/homeassistant/components/notify/instapush.py @@ -8,11 +8,25 @@ import json import logging import requests +import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TITLE, DOMAIN, BaseNotificationService) + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import CONF_API_KEY -from homeassistant.helpers import validate_config + + +CONF_APP_SECRET = 'app_secret' +CONF_EVENT = 'event' +CONF_TRACKER = 'tracker' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_APP_SECRET): cv.string, + vol.Required(CONF_EVENT): cv.string, + vol.Required(CONF_TRACKER): cv.string, +}) + _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://api.instapush.im/v1/' @@ -20,16 +34,8 @@ _RESOURCE = 'https://api.instapush.im/v1/' def get_service(hass, config): """Get the instapush notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_API_KEY, - 'app_secret', - 'event', - 'tracker']}, - _LOGGER): - return None - headers = {'x-instapush-appid': config[CONF_API_KEY], - 'x-instapush-appsecret': config['app_secret']} + 'x-instapush-appsecret': config[CONF_APP_SECRET]} try: response = requests.get(_RESOURCE + 'events/list', @@ -42,15 +48,16 @@ def get_service(hass, config): _LOGGER.error(response['msg']) return None - if len([app for app in response if app['title'] == config['event']]) == 0: + if len([app for app in response + if app['title'] == config[CONF_EVENT]]) == 0: _LOGGER.error( "No app match your given value. " "Please create an app at https://instapush.im") return None return InstapushNotificationService( - config[CONF_API_KEY], config['app_secret'], config['event'], - config['tracker']) + config[CONF_API_KEY], config[CONF_APP_SECRET], config[CONF_EVENT], + config[CONF_TRACKER]) # pylint: disable=too-few-public-methods @@ -70,7 +77,7 @@ class InstapushNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - title = kwargs.get(ATTR_TITLE) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = {"event": self._event, "trackers": {self._tracker: title + " : " + message}} diff --git a/homeassistant/components/notify/joaoapps_join.py b/homeassistant/components/notify/joaoapps_join.py index 67ecd493a06..1478c2330ed 100644 --- a/homeassistant/components/notify/joaoapps_join.py +++ b/homeassistant/components/notify/joaoapps_join.py @@ -7,8 +7,9 @@ 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 + ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, + BaseNotificationService) +from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv REQUIREMENTS = [ @@ -19,10 +20,8 @@ _LOGGER = logging.getLogger(__name__) CONF_DEVICE_ID = 'device_id' -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'joaoapps_join', +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_API_KEY): cv.string }) @@ -52,7 +51,7 @@ class JoinNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" from pyjoin import send_notification - title = kwargs.get(ATTR_TITLE) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) or {} send_notification(device_id=self._device_id, text=message, diff --git a/homeassistant/components/notify/message_bird.py b/homeassistant/components/notify/message_bird.py index 86bcfe79cd0..2e7f2f9bb07 100644 --- a/homeassistant/components/notify/message_bird.py +++ b/homeassistant/components/notify/message_bird.py @@ -6,26 +6,22 @@ https://home-assistant.io/components/notify.message_bird/ """ import logging -from homeassistant.components.notify import ( - ATTR_TARGET, DOMAIN, BaseNotificationService) -from homeassistant.const import CONF_API_KEY -from homeassistant.helpers import validate_config +import voluptuous as vol -CONF_SENDER = 'sender' +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_API_KEY, CONF_SENDER _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['messagebird==1.2.0'] -def is_valid_sender(sender): - """Test if the sender config option is valid.""" - length = len(sender) - if length > 1: - if sender[0] == '+': - return sender[1:].isdigit() - elif length <= 11: - return sender.isalpha() - return False +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_SENDER, default='HA'): + vol.Match(r"^(\+?[1-9]\d{1,14}|\w{1,11})$"), +}) # pylint: disable=unused-argument @@ -33,17 +29,6 @@ def get_service(hass, config): """Get the MessageBird notification service.""" import messagebird - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_API_KEY]}, - _LOGGER): - return None - - sender = config.get(CONF_SENDER, 'HA') - if not is_valid_sender(sender): - _LOGGER.error('Sender is invalid: It must be a phone number or ' - 'a string not longer than 11 characters.') - return None - client = messagebird.Client(config[CONF_API_KEY]) try: # validates the api key @@ -52,7 +37,7 @@ def get_service(hass, config): _LOGGER.error('The specified MessageBird API key is invalid.') return None - return MessageBirdNotificationService(sender, client) + return MessageBirdNotificationService(config.get(CONF_SENDER), client) # pylint: disable=too-few-public-methods diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py index f37f5ca8bd0..ffa4ae229c7 100644 --- a/homeassistant/components/notify/nma.py +++ b/homeassistant/components/notify/nma.py @@ -8,23 +8,24 @@ import logging import xml.etree.ElementTree as ET import requests +import voluptuous as vol from homeassistant.components.notify import ( - ATTR_TITLE, DOMAIN, BaseNotificationService) + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import CONF_API_KEY -from homeassistant.helpers import validate_config +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://www.notifymyandroid.com/publicapi/' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, +}) + + def get_service(hass, config): """Get the NMA notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_API_KEY]}, - _LOGGER): - return None - response = requests.get(_RESOURCE + 'verify', params={"apikey": config[CONF_API_KEY]}) tree = ET.fromstring(response.content) @@ -49,7 +50,7 @@ class NmaNotificationService(BaseNotificationService): data = { "apikey": self._api_key, "application": 'home-assistant', - "event": kwargs.get(ATTR_TITLE), + "event": kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), "description": message, "priority": 0, } diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 20a6daebf05..d5402548508 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -6,24 +6,29 @@ https://home-assistant.io/components/notify.pushbullet/ """ import logging +import voluptuous as vol + from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_TITLE, BaseNotificationService) + ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, + BaseNotificationService) from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pushbullet.py==0.10.0'] +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, +}) + + # pylint: disable=unused-argument def get_service(hass, config): """Get the PushBullet notification service.""" from pushbullet import PushBullet from pushbullet import InvalidKeyError - if CONF_API_KEY not in config: - _LOGGER.error("Unable to find config key '%s'", CONF_API_KEY) - return None - try: pushbullet = PushBullet(config[CONF_API_KEY]) except InvalidKeyError: @@ -73,7 +78,7 @@ class PushBulletNotificationService(BaseNotificationService): call which doesn't require a push object. """ targets = kwargs.get(ATTR_TARGET) - title = kwargs.get(ATTR_TITLE) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) refreshed = False if not targets: diff --git a/homeassistant/components/notify/pushetta.py b/homeassistant/components/notify/pushetta.py index 234c8978452..ab304dc6514 100644 --- a/homeassistant/components/notify/pushetta.py +++ b/homeassistant/components/notify/pushetta.py @@ -6,37 +6,42 @@ https://home-assistant.io/components/notify.pushetta/ """ import logging +import voluptuous as vol + from homeassistant.components.notify import ( - ATTR_TITLE, DOMAIN, BaseNotificationService) + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import CONF_API_KEY -from homeassistant.helpers import validate_config +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) - REQUIREMENTS = ['pushetta==1.0.15'] +CONF_CHANNEL_NAME = 'channel_name' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_CHANNEL_NAME): cv.string, +}) + + def get_service(hass, config): """Get the Pushetta notification service.""" from pushetta import Pushetta, exceptions - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_API_KEY, 'channel_name']}, - _LOGGER): - return None - try: pushetta = Pushetta(config[CONF_API_KEY]) - pushetta.pushMessage(config['channel_name'], "Home Assistant started") + pushetta.pushMessage(config[CONF_CHANNEL_NAME], + "Home Assistant started") except exceptions.TokenValidationError: _LOGGER.error("Please check your access token") return None except exceptions.ChannelNotFoundError: - _LOGGER.error("Channel '%s' not found", config['channel_name']) + _LOGGER.error("Channel '%s' not found", config[CONF_CHANNEL_NAME]) return None return PushettaNotificationService(config[CONF_API_KEY], - config['channel_name']) + config[CONF_CHANNEL_NAME]) # pylint: disable=too-few-public-methods @@ -52,6 +57,6 @@ class PushettaNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - title = kwargs.get(ATTR_TITLE) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) self.pushetta.pushMessage(self._channel_name, "{} {}".format(title, message)) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 5ded1ebe778..c0a067fe918 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -9,7 +9,8 @@ import logging import voluptuous as vol from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TARGET, ATTR_DATA, BaseNotificationService) + ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_TARGET, ATTR_DATA, + BaseNotificationService) from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv @@ -17,8 +18,10 @@ REQUIREMENTS = ['python-pushover==0.2'] _LOGGER = logging.getLogger(__name__) +CONF_USER_KEY = 'user_key' + PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ - vol.Required('user_key'): cv.string, + vol.Required(CONF_USER_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string, }) @@ -29,7 +32,7 @@ def get_service(hass, config): from pushover import InitError try: - return PushoverNotificationService(config['user_key'], + return PushoverNotificationService(config[CONF_USER_KEY], config[CONF_API_KEY]) except InitError: _LOGGER.error( @@ -56,7 +59,7 @@ class PushoverNotificationService(BaseNotificationService): # Make a copy and use empty dict if necessary data = dict(kwargs.get(ATTR_DATA) or {}) - data['title'] = kwargs.get(ATTR_TITLE) + data['title'] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) target = kwargs.get(ATTR_TARGET) if target is not None: diff --git a/homeassistant/components/notify/rest.py b/homeassistant/components/notify/rest.py index 5cc556a1957..0a82b8d5d72 100644 --- a/homeassistant/components/notify/rest.py +++ b/homeassistant/components/notify/rest.py @@ -10,7 +10,8 @@ import requests import voluptuous as vol from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_TITLE, BaseNotificationService, PLATFORM_SCHEMA) + ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService, + PLATFORM_SCHEMA) from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -71,7 +72,8 @@ class RestNotificationService(BaseNotificationService): } if self._title_param_name is not None: - data[self._title_param_name] = kwargs.get(ATTR_TITLE) + data[self._title_param_name] = kwargs.get(ATTR_TITLE, + ATTR_TITLE_DEFAULT) if self._target_param_name is not None: data[self._target_param_name] = kwargs.get(ATTR_TARGET) diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index 894b35a85d4..42921e2be2c 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -6,24 +6,30 @@ https://home-assistant.io/components/notify.sendgrid/ """ import logging -from homeassistant.components.notify import ( - ATTR_TITLE, DOMAIN, BaseNotificationService) -from homeassistant.helpers import validate_config +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['sendgrid==3.4.0'] -REQUIREMENTS = ['sendgrid==3.2.10'] _LOGGER = logging.getLogger(__name__) +# pylint: disable=no-value-for-parameter +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SENDER): vol.Email(), + vol.Required(CONF_RECIPIENT): vol.Email(), +}) + def get_service(hass, config): """Get the SendGrid notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: ['api_key', 'sender', 'recipient']}, - _LOGGER): - return None - - api_key = config['api_key'] - sender = config['sender'] - recipient = config['recipient'] + api_key = config.get(CONF_API_KEY) + sender = config.get(CONF_SENDER) + recipient = config.get(CONF_RECIPIENT) return SendgridNotificationService(api_key, sender, recipient) @@ -44,7 +50,7 @@ class SendgridNotificationService(BaseNotificationService): def send_message(self, message='', **kwargs): """Send an email to a user via SendGrid.""" - subject = kwargs.get(ATTR_TITLE) + subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = { "personalizations": [ diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 39ca0197d0f..780a27b9795 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -6,27 +6,33 @@ https://home-assistant.io/components/notify.slack/ """ import logging -from homeassistant.components.notify import DOMAIN, BaseNotificationService -from homeassistant.const import CONF_API_KEY -from homeassistant.helpers import validate_config +import voluptuous as vol + +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['slacker==0.9.25'] -REQUIREMENTS = ['slacker==0.9.24'] _LOGGER = logging.getLogger(__name__) +CONF_CHANNEL = 'default_channel' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_CHANNEL): cv.string, +}) + # pylint: disable=unused-variable def get_service(hass, config): """Get the Slack notification service.""" import slacker - if not validate_config({DOMAIN: config}, - {DOMAIN: ['default_channel', CONF_API_KEY]}, - _LOGGER): - return None - try: return SlackNotificationService( - config['default_channel'], + config[CONF_CHANNEL], config[CONF_API_KEY]) except slacker.Error: @@ -52,14 +58,12 @@ class SlackNotificationService(BaseNotificationService): channel = kwargs.get('target') or self._default_channel data = kwargs.get('data') - if data: - attachments = data.get('attachments') - else: - attachments = None + attachments = data.get('attachments') if data else None try: self.slack.chat.post_message(channel, message, as_user=True, - attachments=attachments) - except slacker.Error: - _LOGGER.exception("Could not send slack notification") + attachments=attachments, + link_names=True) + except slacker.Error as err: + _LOGGER.error("Could not send slack notification. Error: %s", err) diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index 9ac73a49e3d..84aae3f2c8f 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -12,30 +12,36 @@ from email.mime.image import MIMEImage import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_PORT) + ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA, + BaseNotificationService) +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SENDER, CONF_RECIPIENT) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) ATTR_IMAGES = 'images' # optional embedded image file attachments CONF_STARTTLS = 'starttls' -CONF_SENDER = 'sender' -CONF_RECIPIENT = 'recipient' CONF_DEBUG = 'debug' CONF_SERVER = 'server' +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 25 +DEFAULT_DEBUG = False +DEFAULT_STARTTLS = False + +# pylint: disable=no-value-for-parameter PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_RECIPIENT): cv.string, - vol.Optional(CONF_SERVER, default='localhost'): cv.string, - vol.Optional(CONF_PORT, default=25): cv.port, - vol.Optional(CONF_SENDER): cv.string, - vol.Optional(CONF_STARTTLS, default=False): cv.boolean, + vol.Required(CONF_RECIPIENT): vol.Email(), + vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SENDER): vol.Email(), + vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_DEBUG, default=False): cv.boolean, + vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, }) @@ -95,16 +101,14 @@ class MailNotificationService(BaseNotificationService): except smtplib.socket.gaierror: _LOGGER.exception( "SMTP server not found (%s:%s). " - "Please check the IP address or hostname of your SMTP server.", + "Please check the IP address or hostname of your SMTP server", self._server, self._port) - return False except (smtplib.SMTPAuthenticationError, ConnectionRefusedError): _LOGGER.exception( "Login not possible. " - "Please check your setting and/or your credentials.") - + "Please check your setting and/or your credentials") return False finally: @@ -120,7 +124,7 @@ class MailNotificationService(BaseNotificationService): Will send plain text normally, or will build a multipart HTML message with inline image attachments if images config is defined. """ - subject = kwargs.get(ATTR_TITLE) + subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) if data: @@ -154,13 +158,13 @@ class MailNotificationService(BaseNotificationService): def _build_text_msg(message): """Build plaintext email.""" - _LOGGER.debug('Building plain text email.') + _LOGGER.debug('Building plain text email') return MIMEText(message) def _build_multipart_msg(message, images): """Build Multipart message with in-line images.""" - _LOGGER.debug('Building multipart email with embedded attachment(s).') + _LOGGER.debug('Building multipart email with embedded attachment(s)') msg = MIMEMultipart('related') msg_alt = MIMEMultipart('alternative') msg.attach(msg_alt) @@ -177,7 +181,7 @@ def _build_multipart_msg(message, images): msg.attach(attachment) attachment.add_header('Content-ID', '<{}>'.format(cid)) except FileNotFoundError: - _LOGGER.warning('Attachment %s not found. Skipping.', + _LOGGER.warning('Attachment %s not found. Skipping', atch_name) body_html = MIMEText(''.join(body_text), 'html') diff --git a/homeassistant/components/notify/syslog.py b/homeassistant/components/notify/syslog.py index 381a92394c3..792ed2ad631 100644 --- a/homeassistant/components/notify/syslog.py +++ b/homeassistant/components/notify/syslog.py @@ -6,63 +6,76 @@ https://home-assistant.io/components/notify.syslog/ """ import logging +import voluptuous as vol + from homeassistant.components.notify import ( - ATTR_TITLE, DOMAIN, BaseNotificationService) -from homeassistant.helpers import validate_config + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) + + +CONF_FACILITY = 'facility' +CONF_OPTION = 'option' +CONF_PRIORITY = 'priority' + +SYSLOG_FACILITY = { + 'kernel': 'LOG_KERN', + 'user': 'LOG_USER', + 'mail': 'LOG_MAIL', + 'daemon': 'LOG_DAEMON', + 'auth': 'LOG_KERN', + 'LPR': 'LOG_LPR', + 'news': 'LOG_NEWS', + 'uucp': 'LOG_UUCP', + 'cron': 'LOG_CRON', + 'syslog': 'LOG_SYSLOG', + 'local0': 'LOG_LOCAL0', + 'local1': 'LOG_LOCAL1', + 'local2': 'LOG_LOCAL2', + 'local3': 'LOG_LOCAL3', + 'local4': 'LOG_LOCAL4', + 'local5': 'LOG_LOCAL5', + 'local6': 'LOG_LOCAL6', + 'local7': 'LOG_LOCAL7', +} + +SYSLOG_OPTION = { + 'pid': 'LOG_PID', + 'cons': 'LOG_CONS', + 'ndelay': 'LOG_NDELAY', + 'nowait': 'LOG_NOWAIT', + 'perror': 'LOG_PERROR', +} + +SYSLOG_PRIORITY = { + 5: 'LOG_EMERG', + 4: 'LOG_ALERT', + 3: 'LOG_CRIT', + 2: 'LOG_ERR', + 1: 'LOG_WARNING', + 0: 'LOG_NOTICE', + -1: 'LOG_INFO', + -2: 'LOG_DEBUG', +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_FACILITY, default='syslog'): + vol.In(SYSLOG_FACILITY.keys()), + vol.Optional(CONF_OPTION, default='pid'): vol.In(SYSLOG_OPTION.keys()), + vol.Optional(CONF_PRIORITY, default=-1): vol.In(SYSLOG_PRIORITY.keys()), +}) + _LOGGER = logging.getLogger(__name__) def get_service(hass, config): """Get the syslog notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: ['facility', 'option', 'priority']}, - _LOGGER): - return None - import syslog - _facility = { - 'kernel': syslog.LOG_KERN, - 'user': syslog.LOG_USER, - 'mail': syslog.LOG_MAIL, - 'daemon': syslog.LOG_DAEMON, - 'auth': syslog.LOG_KERN, - 'LPR': syslog.LOG_LPR, - 'news': syslog.LOG_NEWS, - 'uucp': syslog.LOG_UUCP, - 'cron': syslog.LOG_CRON, - 'syslog': syslog.LOG_SYSLOG, - 'local0': syslog.LOG_LOCAL0, - 'local1': syslog.LOG_LOCAL1, - 'local2': syslog.LOG_LOCAL2, - 'local3': syslog.LOG_LOCAL3, - 'local4': syslog.LOG_LOCAL4, - 'local5': syslog.LOG_LOCAL5, - 'local6': syslog.LOG_LOCAL6, - 'local7': syslog.LOG_LOCAL7, - }.get(config['facility'], 40) + facility = getattr(syslog, SYSLOG_FACILITY[config.get(CONF_FACILITY)]) + option = getattr(syslog, SYSLOG_OPTION[config.get(CONF_OPTION)]) + priority = getattr(syslog, SYSLOG_PRIORITY[config.get(CONF_PRIORITY)]) - _option = { - 'pid': syslog.LOG_PID, - 'cons': syslog.LOG_CONS, - 'ndelay': syslog.LOG_NDELAY, - 'nowait': syslog.LOG_NOWAIT, - 'perror': syslog.LOG_PERROR - }.get(config['option'], 10) - - _priority = { - 5: syslog.LOG_EMERG, - 4: syslog.LOG_ALERT, - 3: syslog.LOG_CRIT, - 2: syslog.LOG_ERR, - 1: syslog.LOG_WARNING, - 0: syslog.LOG_NOTICE, - -1: syslog.LOG_INFO, - -2: syslog.LOG_DEBUG - }.get(config['priority'], -1) - - return SyslogNotificationService(_facility, _option, _priority) + return SyslogNotificationService(facility, option, priority) # pylint: disable=too-few-public-methods @@ -80,7 +93,7 @@ class SyslogNotificationService(BaseNotificationService): """Send a message to a user.""" import syslog - title = kwargs.get(ATTR_TITLE) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) syslog.openlog(title, self._option, self._facility) syslog.syslog(self._priority, message) diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index 8da916eb1f3..cc8b284b974 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -7,14 +7,15 @@ https://home-assistant.io/components/notify.telegram/ import io import logging import urllib + import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_DATA, BaseNotificationService) -from homeassistant.const import (CONF_API_KEY, CONF_NAME, ATTR_LOCATION, - ATTR_LATITUDE, ATTR_LONGITUDE, CONF_PLATFORM) + ATTR_TITLE, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import (CONF_API_KEY, ATTR_LOCATION, ATTR_LATITUDE, + ATTR_LONGITUDE) _LOGGER = logging.getLogger(__name__) @@ -23,12 +24,14 @@ REQUIREMENTS = ['python-telegram-bot==5.0.0'] ATTR_PHOTO = "photo" ATTR_DOCUMENT = "document" ATTR_CAPTION = "caption" +ATTR_URL = 'url' +ATTR_FILE = 'file' +ATTR_USERNAME = 'username' +ATTR_PASSWORD = 'password' CONF_CHAT_ID = 'chat_id' -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): "telegram", - vol.Optional(CONF_NAME): cv.string, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_CHAT_ID): cv.string, }) @@ -106,10 +109,18 @@ class TelegramNotificationService(BaseNotificationService): elif data is not None and ATTR_DOCUMENT in data: return self.send_document(data.get(ATTR_DOCUMENT)) + if title: + text = '{} {}'.format(title, message) + else: + text = message + + parse_mode = telegram.parsemode.ParseMode.MARKDOWN + # send message try: self.bot.sendMessage(chat_id=self._chat_id, - text=title + " " + message) + text=text, + parse_mode=parse_mode) except telegram.error.TelegramError: _LOGGER.exception("Error sending message.") return @@ -117,11 +128,16 @@ class TelegramNotificationService(BaseNotificationService): def send_photo(self, data): """Send a photo.""" import telegram - caption = data.pop(ATTR_CAPTION, None) + caption = data.get(ATTR_CAPTION) # send photo try: - photo = load_data(**data) + photo = load_data( + url=data.get(ATTR_URL), + file=data.get(ATTR_FILE), + username=data.get(ATTR_USERNAME), + password=data.get(ATTR_PASSWORD), + ) self.bot.sendPhoto(chat_id=self._chat_id, photo=photo, caption=caption) except telegram.error.TelegramError: @@ -131,11 +147,16 @@ class TelegramNotificationService(BaseNotificationService): def send_document(self, data): """Send a document.""" import telegram - caption = data.pop(ATTR_CAPTION, None) + caption = data.get(ATTR_CAPTION) # send photo try: - document = load_data(**data) + document = load_data( + url=data.get(ATTR_URL), + file=data.get(ATTR_FILE), + username=data.get(ATTR_USERNAME), + password=data.get(ATTR_PASSWORD), + ) self.bot.sendDocument(chat_id=self._chat_id, document=document, caption=caption) except telegram.error.TelegramError: diff --git a/homeassistant/components/notify/twilio_sms.py b/homeassistant/components/notify/twilio_sms.py index f7700240b67..ddcf8849b78 100644 --- a/homeassistant/components/notify/twilio_sms.py +++ b/homeassistant/components/notify/twilio_sms.py @@ -6,27 +6,29 @@ https://home-assistant.io/components/notify.twilio_sms/ """ import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TARGET, DOMAIN, BaseNotificationService) -from homeassistant.helpers import validate_config + ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ["twilio==5.4.0"] + CONF_ACCOUNT_SID = "account_sid" CONF_AUTH_TOKEN = "auth_token" CONF_FROM_NUMBER = "from_number" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ACCOUNT_SID): cv.string, + vol.Required(CONF_AUTH_TOKEN): cv.string, + vol.Required(CONF_FROM_NUMBER): vol.Match(r"^\+?[1-9]\d{1,14}$"), +}) + def get_service(hass, config): """Get the Twilio SMS notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_ACCOUNT_SID, - CONF_AUTH_TOKEN, - CONF_FROM_NUMBER]}, - _LOGGER): - return None - # pylint: disable=import-error from twilio.rest import TwilioRestClient diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 9284c4fac93..bafdc2403be 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -6,9 +6,12 @@ https://home-assistant.io/components/notify.twitter/ """ import logging -from homeassistant.components.notify import DOMAIN, BaseNotificationService +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import (PLATFORM_SCHEMA, + BaseNotificationService) from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.helpers import validate_config _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['TwitterAPI==2.4.2'] @@ -17,16 +20,16 @@ CONF_CONSUMER_KEY = "consumer_key" CONF_CONSUMER_SECRET = "consumer_secret" CONF_ACCESS_TOKEN_SECRET = "access_token_secret" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CONSUMER_KEY): cv.string, + vol.Required(CONF_CONSUMER_SECRET): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_ACCESS_TOKEN_SECRET): cv.string, +}) + def get_service(hass, config): """Get the Twitter notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_CONSUMER_KEY, CONF_CONSUMER_SECRET, - CONF_ACCESS_TOKEN, - CONF_ACCESS_TOKEN_SECRET]}, - _LOGGER): - return None - return TwitterNotificationService(config[CONF_CONSUMER_KEY], config[CONF_CONSUMER_SECRET], config[CONF_ACCESS_TOKEN], diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index 34463dc6e45..e8276255925 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -6,33 +6,30 @@ https://home-assistant.io/components/notify.webostv/ """ import logging -from homeassistant.components.notify import (BaseNotificationService, DOMAIN) -from homeassistant.const import (CONF_HOST, CONF_NAME) -from homeassistant.helpers import validate_config +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import (BaseNotificationService, + PLATFORM_SCHEMA) +from homeassistant.const import CONF_HOST + +_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv' '/archive/v0.1.2.zip' '#pylgtv==0.1.2'] -_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, +}) def get_service(hass, config): """Return the notify service.""" - if not validate_config({DOMAIN: config}, {DOMAIN: [CONF_HOST, CONF_NAME]}, - _LOGGER): - return None - - host = config.get(CONF_HOST, None) - - if not host: - _LOGGER.error('No host provided.') - return None - from pylgtv import WebOsClient from pylgtv import PyLGTVPairException - client = WebOsClient(host) + client = WebOsClient(config.get(CONF_HOST)) try: client.register() diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 68c0ce2979f..ed64a8b4e07 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -6,30 +6,39 @@ https://home-assistant.io/components/notify.xmpp/ """ import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TITLE, DOMAIN, BaseNotificationService) -from homeassistant.helpers import validate_config + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT REQUIREMENTS = ['sleekxmpp==1.3.1', 'dnspython3==1.12.0', 'pyasn1==0.1.9', 'pyasn1-modules==0.0.8'] + +CONF_TLS = 'tls' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SENDER): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_RECIPIENT): cv.string, + vol.Optional(CONF_TLS, default=True): cv.boolean, +}) + + _LOGGER = logging.getLogger(__name__) def get_service(hass, config): """Get the Jabber (XMPP) notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: ['sender', 'password', 'recipient']}, - _LOGGER): - return None - return XmppNotificationService( config.get('sender'), config.get('password'), config.get('recipient'), - config.get('tls', True)) + config.get('tls')) # pylint: disable=too-few-public-methods @@ -45,7 +54,7 @@ class XmppNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - title = kwargs.get(ATTR_TITLE) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = "{}: {}".format(title, message) if title else message send_message(self._sender + '/home-assistant', self._password, diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index bd90e67d0df..871f81759e0 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -5,37 +5,41 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/octoprint/ """ import logging - import time + import requests +import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_HOST -from homeassistant.helpers import validate_config, discovery - -DOMAIN = "octoprint" -OCTOPRINT = None +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DISCOVER_SENSORS = 'octoprint.sensors' DISCOVER_BINARY_SENSORS = 'octoprint.binary_sensor' +DISCOVER_SENSORS = 'octoprint.sensors' +DOMAIN = 'octoprint' + +OCTOPRINT = None + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_HOST): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Set up OctoPrint API.""" - if not validate_config(config, {DOMAIN: [CONF_API_KEY], - DOMAIN: [CONF_HOST]}, - _LOGGER): - return False - - base_url = config[DOMAIN][CONF_HOST] + "/api/" + base_url = 'http://{}/api/'.format(config[DOMAIN][CONF_HOST]) api_key = config[DOMAIN][CONF_API_KEY] global OCTOPRINT try: OCTOPRINT = OctoPrintAPI(base_url, api_key) - OCTOPRINT.get("printer") - OCTOPRINT.get("job") + OCTOPRINT.get('printer') + OCTOPRINT.get('job') except requests.exceptions.RequestException as conn_err: _LOGGER.error("Error setting up OctoPrint API: %r", conn_err) return False @@ -55,7 +59,7 @@ class OctoPrintAPI(object): def __init__(self, api_url, key): """Initialize OctoPrint API and set headers needed later.""" self.api_url = api_url - self.headers = {'content-type': 'application/json', + self.headers = {'content-type': CONTENT_TYPE_JSON, 'X-Api-Key': key} self.printer_last_reading = [{}, None] self.job_last_reading = [{}, None] diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py index 07771acee00..764b972d393 100644 --- a/homeassistant/components/pilight.py +++ b/homeassistant/components/pilight.py @@ -4,7 +4,6 @@ Component to create an interface to a Pilight daemon (https://pilight.org/). For more details about this component, please refer to the documentation at https://home-assistant.io/components/pilight/ """ -# pylint: disable=import-error import logging import socket @@ -12,46 +11,49 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ensure_list -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT, + CONF_WHITELIST) REQUIREMENTS = ['pilight==0.0.2'] -DOMAIN = "pilight" +_LOGGER = logging.getLogger(__name__) + +ATTR_PROTOCOL = 'protocol' + +DEFAULT_HOST = '127.0.0.1' +DEFAULT_PORT = 5000 +DOMAIN = 'pilight' + EVENT = 'pilight_received' -SERVICE_NAME = 'send' - -CONF_WHITELIST = 'whitelist' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_HOST, default='127.0.0.1'): cv.string, - vol.Required(CONF_PORT, default=5000): vol.Coerce(int), - vol.Optional(CONF_WHITELIST): {cv.string: [cv.string]} - }), -}, extra=vol.ALLOW_EXTRA) # The pilight code schema depends on the protocol # Thus only require to have the protocol information -ATTR_PROTOCOL = 'protocol' RF_CODE_SCHEMA = vol.Schema({vol.Required(ATTR_PROTOCOL): cv.string}, extra=vol.ALLOW_EXTRA) +SERVICE_NAME = 'send' -_LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_WHITELIST, default={}): {cv.string: [cv.string]} + }), +}, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Setup the pilight component.""" from pilight import pilight + host = config[DOMAIN][CONF_HOST] + port = config[DOMAIN][CONF_PORT] + try: - pilight_client = pilight.Client(host=config[DOMAIN][CONF_HOST], - port=config[DOMAIN][CONF_PORT]) + pilight_client = pilight.Client(host=host, port=port) except (socket.error, socket.timeout) as err: - _LOGGER.error( - "Unable to connect to %s on port %s: %s", - config[CONF_HOST], config[CONF_PORT], err) + _LOGGER.error("Unable to connect to %s on port %s: %s", + host, port, err) return False # Start / stop pilight-daemon connection with HA start/stop @@ -74,7 +76,7 @@ def setup(hass, config): # Patch data because of bug: # https://github.com/pilight/pilight/issues/296 # Protocol has to be in a list otherwise segfault in pilight-daemon - message_data["protocol"] = ensure_list(message_data["protocol"]) + message_data['protocol'] = ensure_list(message_data['protocol']) try: pilight_client.send_code(message_data) @@ -86,7 +88,7 @@ def setup(hass, config): # Publish received codes on the HA event bus # A whitelist of codes to be published in the event bus - whitelist = config[DOMAIN].get('whitelist', False) + whitelist = config[DOMAIN].get(CONF_WHITELIST) def handle_received_code(data): """Called when RF codes are received.""" diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 6e3e2db064d..671623ec564 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -92,7 +92,8 @@ class States(Base): # type: ignore else: dbstate.domain = state.domain dbstate.state = state.state - dbstate.attributes = json.dumps(dict(state.attributes)) + dbstate.attributes = json.dumps(dict(state.attributes), + cls=JSONEncoder) dbstate.last_changed = state.last_changed dbstate.last_updated = state.last_updated diff --git a/homeassistant/components/rollershutter/demo.py b/homeassistant/components/rollershutter/demo.py index 31915019c5e..6799d062e43 100644 --- a/homeassistant/components/rollershutter/demo.py +++ b/homeassistant/components/rollershutter/demo.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ from homeassistant.components.rollershutter import RollershutterDevice -from homeassistant.const import EVENT_TIME_CHANGED from homeassistant.helpers.event import track_utc_time_change @@ -27,7 +26,7 @@ class DemoRollershutter(RollershutterDevice): self._name = name self._position = position self._moving_up = True - self._listener = None + self._unsub_listener = None @property def name(self): @@ -70,15 +69,15 @@ class DemoRollershutter(RollershutterDevice): def stop(self, **kwargs): """Stop the roller shutter.""" - if self._listener is not None: - self.hass.bus.remove_listener(EVENT_TIME_CHANGED, self._listener) - self._listener = None + if self._unsub_listener is not None: + self._unsub_listener() + self._unsub_listener = None def _listen(self): """Listen for changes.""" - if self._listener is None: - self._listener = track_utc_time_change(self.hass, - self._time_changed) + if self._unsub_listener is None: + self._unsub_listener = track_utc_time_change(self.hass, + self._time_changed) def _time_changed(self, now): """Track time changes.""" diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py deleted file mode 100644 index 613d7884919..00000000000 --- a/homeassistant/components/rollershutter/homematic.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -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) - - -# pylint: disable=abstract-method -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 move_position(self, **kwargs): - """Move the roller shutter to a specific position.""" - 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}) diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 0d859314bb4..51b5f9bba3b 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -17,6 +17,16 @@ from homeassistant.util import Throttle REQUIREMENTS = ['blockchain==1.3.3'] +_LOGGER = logging.getLogger(__name__) + +CONF_CURRENCY = 'currency' + +DEFAULT_CURRENCY = 'USD' + +ICON = 'mdi:currency-btc' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) + OPTION_TYPES = { 'exchangerate': ['Exchange rate (1 BTC)', None], 'trade_volume_btc': ['Trade volume', 'BTC'], @@ -41,20 +51,12 @@ OPTION_TYPES = { 'market_price_usd': ['Market price', 'USD'] } -ICON = 'mdi:currency-btc' -CONF_CURRENCY = 'currency' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DISPLAY_OPTIONS, default=[]): - [vol.In(OPTION_TYPES)], - vol.Optional(CONF_CURRENCY, default='USD'): cv.string, + vol.All(cv.ensure_list, [vol.In(OPTION_TYPES)]), + vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, }) -_LOGGER = logging.getLogger(__name__) - -# Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Bitcoin sensors.""" @@ -63,8 +65,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): currency = config.get(CONF_CURRENCY) if currency not in exchangerates.get_ticker(): - _LOGGER.error('Currency "%s" is not available. Using "USD"', currency) - currency = 'USD' + _LOGGER.warning('Currency "%s" is not available. Using "USD"', + currency) + currency = DEFAULT_CURRENCY data = BitcoinData() dev = [] diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py index a9d2c0c6631..1026e2a92db 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -6,58 +6,64 @@ https://home-assistant.io/components/sensor.bloomsky/ """ import logging -from homeassistant.const import TEMP_FAHRENHEIT +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity from homeassistant.loader import get_component +import homeassistant.helpers.config_validation as cv -DEPENDENCIES = ["bloomsky"] +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['bloomsky'] # These are the available sensors -SENSOR_TYPES = ["Temperature", - "Humidity", - "Pressure", - "Luminance", - "UVIndex", - "Voltage"] +SENSOR_TYPES = ['Temperature', + 'Humidity', + 'Pressure', + 'Luminance', + 'UVIndex', + 'Voltage'] # Sensor units - these do not currently align with the API documentation -SENSOR_UNITS = {"Temperature": TEMP_FAHRENHEIT, - "Humidity": "%", - "Pressure": "inHg", - "Luminance": "cd/m²", - "Voltage": "mV"} +SENSOR_UNITS = {'Temperature': TEMP_FAHRENHEIT, + 'Humidity': '%', + 'Pressure': 'inHg', + 'Luminance': 'cd/m²', + 'Voltage': 'mV'} # Which sensors to format numerically -FORMAT_NUMBERS = ["Temperature", "Pressure", "Voltage"] +FORMAT_NUMBERS = ['Temperature', 'Pressure', 'Voltage'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the available BloomSky weather sensors.""" - logger = logging.getLogger(__name__) bloomsky = get_component('bloomsky') - sensors = config.get('monitored_conditions', SENSOR_TYPES) + # Default needed in case of discovery + sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) for device in bloomsky.BLOOMSKY.devices.values(): for variable in sensors: - if variable in SENSOR_TYPES: - add_devices([BloomSkySensor(bloomsky.BLOOMSKY, - device, - variable)]) - else: - logger.error("Cannot find definition for device: %s", variable) + add_devices([BloomSkySensor(bloomsky.BLOOMSKY, device, variable)]) class BloomSkySensor(Entity): """Representation of a single sensor in a BloomSky device.""" def __init__(self, bs, device, sensor_name): - """Initialize a bloomsky sensor.""" + """Initialize a BloomSky sensor.""" self._bloomsky = bs - self._device_id = device["DeviceID"] + self._device_id = device['DeviceID'] self._sensor_name = sensor_name - self._name = "{} {}".format(device["DeviceName"], sensor_name) - self._unique_id = "bloomsky_sensor {}".format(self._name) + self._name = '{} {}'.format(device['DeviceName'], sensor_name) + self._unique_id = 'bloomsky_sensor {}'.format(self._name) self.update() @property @@ -85,9 +91,9 @@ class BloomSkySensor(Entity): self._bloomsky.refresh_devices() state = \ - self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] + self._bloomsky.devices[self._device_id]['Data'][self._sensor_name] if self._sensor_name in FORMAT_NUMBERS: - self._state = "{0:.2f}".format(state) + self._state = '{0:.2f}'.format(state) else: self._state = state diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py new file mode 100644 index 00000000000..83adcac7fea --- /dev/null +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -0,0 +1,125 @@ +""" +Details about crypto currencies from CoinMarketCap. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.coinmarketcap/ +""" +import logging +from datetime import timedelta +import json +from urllib.error import HTTPError + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['coinmarketcap==2.0.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_24H_VOLUME_USD = '24h_volume_usd' +ATTR_AVAILABLE_SUPPLY = 'available_supply' +ATTR_MARKET_CAP = 'market_cap_usd' +ATTR_NAME = 'name' +ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' +ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' +ATTR_PRICE = 'price_usd' +ATTR_SYMBOL = 'symbol' +ATTR_TOTAL_SUPPLY = 'total_supply' + +CONF_CURRENCY = 'currency' + +DEFAULT_CURRENCY = 'bitcoin' + +ICON = 'mdi:currency-usd' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the CoinMarketCap sensor.""" + currency = config.get(CONF_CURRENCY) + + try: + CoinMarketCapData(currency).update() + except HTTPError: + _LOGGER.warning('Currency "%s" is not available. Using "bitcoin"', + currency) + currency = DEFAULT_CURRENCY + + add_devices([CoinMarketCapSensor(CoinMarketCapData(currency))]) + + +# pylint: disable=too-few-public-methods +class CoinMarketCapSensor(Entity): + """Representation of a CoinMarketCap sensor.""" + + def __init__(self, data): + """Initialize the sensor.""" + self.data = data + self._ticker = None + self._unit_of_measurement = 'USD' + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._ticker.get('name') + + @property + def state(self): + """Return the state of the sensor.""" + return self._ticker.get('price_usd') + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + @property + def state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_24H_VOLUME_USD: self._ticker.get('24h_volume_usd'), + ATTR_AVAILABLE_SUPPLY: self._ticker.get('available_supply'), + ATTR_MARKET_CAP: self._ticker.get('market_cap_usd'), + ATTR_PERCENT_CHANGE_24H: self._ticker.get('percent_change_24h'), + ATTR_PERCENT_CHANGE_7D: self._ticker.get('percent_change_7d'), + ATTR_SYMBOL: self._ticker.get('symbol'), + ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), + } + + # pylint: disable=too-many-branches + def update(self): + """Get the latest data and updates the states.""" + self.data.update() + self._ticker = json.loads( + self.data.ticker.decode('utf-8').strip('\n '))[0] + + +class CoinMarketCapData(object): + """Get the latest data and update the states.""" + + def __init__(self, currency): + """Initialize the data object.""" + self.currency = currency + self.ticker = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from blockchain.info.""" + from coinmarketcap import Market + + self.ticker = Market().ticker(self.currency) diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index eb1fb4603e2..f26d2680a26 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -8,35 +8,41 @@ import logging import subprocess from datetime import timedelta -from homeassistant.const import CONF_VALUE_TEMPLATE +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_COMMAND) from homeassistant.helpers.entity import Entity from homeassistant.helpers import template from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Command Sensor" +DEFAULT_NAME = 'Command Sensor' -# Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COMMAND): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Command Sensor.""" - if config.get('command') is None: - _LOGGER.error('Missing required variable: "command"') - return False + name = config.get(CONF_NAME) + command = config.get(CONF_COMMAND) + unit = config.get(CONF_UNIT_OF_MEASUREMENT) + value_template = config.get(CONF_VALUE_TEMPLATE) - data = CommandSensorData(config.get('command')) + data = CommandSensorData(command) - add_devices_callback([CommandSensor( - hass, - data, - config.get('name', DEFAULT_NAME), - config.get('unit_of_measurement'), - config.get(CONF_VALUE_TEMPLATE) - )]) + add_devices([CommandSensor(hass, data, name, unit, value_template)]) # pylint: disable=too-many-arguments diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index 109e539c599..461c2fb1eeb 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -7,7 +7,12 @@ https://home-assistant.io/components/sensor.dht/ import logging from datetime import timedelta -from homeassistant.const import TEMP_FAHRENHEIT +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit @@ -18,18 +23,29 @@ REQUIREMENTS = ['http://github.com/adafruit/Adafruit_Python_DHT/archive/' '#Adafruit_DHT==1.3.0'] _LOGGER = logging.getLogger(__name__) + CONF_PIN = 'pin' CONF_SENSOR = 'sensor' + +DEFAULT_NAME = 'DHT Sensor' + +# DHT11 is able to deliver data once per second, DHT22 once every two +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + SENSOR_TEMPERATURE = 'temperature' SENSOR_HUMIDITY = 'humidity' SENSOR_TYPES = { SENSOR_TEMPERATURE: ['Temperature', None], SENSOR_HUMIDITY: ['Humidity', '%'] } -DEFAULT_NAME = "DHT Sensor" -# Return cached results if last scan was less then this time ago -# DHT11 is able to deliver data once per second, DHT22 once every two -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SENSOR): cv.string, + vol.Required(CONF_PIN): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -46,23 +62,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensor = available_sensors.get(config.get(CONF_SENSOR)) pin = config.get(CONF_PIN) - if not sensor or not pin: - _LOGGER.error( - "Config error " - "Please check your settings for DHT, sensor not supported.") - return None + if not sensor: + _LOGGER.error("DHT sensor type is not supported") + return False data = DHTClient(Adafruit_DHT, sensor, pin) dev = [] - name = config.get('name', DEFAULT_NAME) + name = config.get(CONF_NAME) try: - for variable in config['monitored_conditions']: - if variable not in SENSOR_TYPES: - _LOGGER.error('Sensor type: "%s" does not exist', variable) - else: - dev.append( - DHTSensor(data, variable, SENSOR_TYPES[variable][1], name)) + for variable in config[CONF_MONITORED_CONDITIONS]: + dev.append(DHTSensor( + data, variable, SENSOR_TYPES[variable][1], name)) except KeyError: pass @@ -109,8 +120,7 @@ class DHTSensor(Entity): if (temperature >= -20) and (temperature < 80): self._state = temperature if self.temp_unit == TEMP_FAHRENHEIT: - self._state = round(celsius_to_fahrenheit(temperature), - 1) + self._state = round(celsius_to_fahrenheit(temperature), 1) elif self.type == SENSOR_HUMIDITY: humidity = round(data[SENSOR_HUMIDITY], 1) if (humidity >= 0) and (humidity <= 100): diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 0a82f5d587c..421940b9c7d 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -7,18 +7,33 @@ https://home-assistant.io/components/sensor.eliqonline/ import logging from urllib.error import URLError -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['eliqonline==1.0.12'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['eliqonline==1.0.12'] -DEFAULT_NAME = "ELIQ Online" -UNIT_OF_MEASUREMENT = "W" -ICON = "mdi:speedometer" -CONF_CHANNEL_ID = "channel_id" +CONF_CHANNEL_ID = 'channel_id' + +DEFAULT_NAME = 'ELIQ Online' + +ICON = 'mdi:speedometer' + SCAN_INTERVAL = 60 +UNIT_OF_MEASUREMENT = 'W' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_CHANNEL_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the ELIQ Online sensor.""" @@ -28,13 +43,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME, DEFAULT_NAME) channel_id = config.get(CONF_CHANNEL_ID) - if access_token is None: - _LOGGER.error( - "Configuration Error: " - "Please make sure you have configured your access token " - "that can be aquired from https://my.eliq.se/user/settings/api") - return False - api = eliqonline.API(access_token) try: diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py index 95d91d42efc..ad6aa2ca630 100644 --- a/homeassistant/components/sensor/fastdotcom.py +++ b/homeassistant/components/sensor/fastdotcom.py @@ -14,8 +14,7 @@ from homeassistant.components.sensor import (DOMAIN, PLATFORM_SCHEMA) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_change -REQUIREMENTS = ['https://github.com/nkgilley/fast.com/archive/' - 'master.zip#fastdotcom==0.0.1'] +REQUIREMENTS = ['fastdotcom==0.0.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index eb87527a546..b99a4f320c9 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -16,7 +16,7 @@ from homeassistant.loader import get_component from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["fitbit==0.2.2"] +REQUIREMENTS = ["fitbit==0.2.3"] DEPENDENCIES = ["http"] ICON = "mdi:walk" diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index 4f3b2cd17c7..213760fee0d 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -6,20 +6,28 @@ https://home-assistant.io/components/sensor.forecast/ """ import logging from datetime import timedelta + import voluptuous as vol from requests.exceptions import ConnectionError as ConnectError, \ HTTPError, Timeout -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_API_KEY, CONF_NAME, - CONF_MONITORED_CONDITIONS) +from homeassistant.const import ( + CONF_API_KEY, CONF_NAME, CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['python-forecastio==1.3.4'] + _LOGGER = logging.getLogger(__name__) +CONF_UNITS = 'units' + +DEFAULT_NAME = 'Forecast.io' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) + # Sensor types are defined like so: # Name, si unit, us unit, ca unit, uk unit, uk2 unit SENSOR_TYPES = { @@ -57,22 +65,16 @@ SENSOR_TYPES = { 'precip_intensity_max': ['Daily Max Precip Intensity', 'mm', 'in', 'mm', 'mm', 'mm'], } -DEFAULT_NAME = "Forecast.io" -CONF_UNITS = 'units' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']) }) -# Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) - - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Forecast.io sensor.""" # Validate the configuration @@ -100,7 +102,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) - # Initialize and add all of the sensors. sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: sensors.append(ForeCastSensor(forecast_data, variable, name)) @@ -249,10 +250,8 @@ class ForeCastData(object): import forecastio try: - self.data = forecastio.load_forecast(self._api_key, - self.latitude, - self.longitude, - units=self.units) + self.data = forecastio.load_forecast( + self._api_key, self.latitude, self.longitude, units=self.units) except (ConnectError, HTTPError, Timeout, ValueError) as error: raise ValueError("Unable to init Forecast.io. - %s", error) self.unit_system = self.data.json['flags']['units'] diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py index 7525e5fcc81..82f6ae839fb 100644 --- a/homeassistant/components/sensor/fritzbox_callmonitor.py +++ b/homeassistant/components/sensor/fritzbox_callmonitor.py @@ -1,35 +1,49 @@ """ A sensor to monitor incoming and outgoing phone calls on a Fritz!Box router. -To activate the call monitor on your Fritz!Box, dial #96*5* from any phone -connected to it. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.fritzbox_callmonitor/ """ import logging import socket import threading import datetime import time + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME) from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Phone' DEFAULT_HOST = '169.254.1.1' # IP valid for all Fritz!Box routers DEFAULT_PORT = 1012 -# sensor values -VALUE_DEFAULT = 'idle' # initial value + +VALUE_DEFAULT = 'idle' VALUE_RING = 'ringing' VALUE_CALL = 'dialing' VALUE_CONNECT = 'talking' VALUE_DISCONNECT = 'idle' + INTERVAL_RECONNECT = 60 +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Fritz!Box call monitor sensor platform.""" - host = config.get('host', DEFAULT_HOST) - port = config.get('port', DEFAULT_PORT) + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) - sensor = FritzBoxCallSensor(name=config.get('name', DEFAULT_NAME)) + sensor = FritzBoxCallSensor(name=name) add_devices([sensor]) diff --git a/homeassistant/components/sensor/gpsd.py b/homeassistant/components/sensor/gpsd.py index a9f8245b738..0fb24c96283 100644 --- a/homeassistant/components/sensor/gpsd.py +++ b/homeassistant/components/sensor/gpsd.py @@ -15,7 +15,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['gps3==0.33.2'] +REQUIREMENTS = ['gps3==0.33.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index ad954899e6d..a9c6f36bf54 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -9,29 +9,47 @@ import logging import datetime import threading -from homeassistant.helpers.entity import Entity +import voluptuous as vol -_LOGGER = logging.getLogger(__name__) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ["https://github.com/robbiet480/pygtfs/archive/" "00546724e4bbcb3053110d844ca44e2246267dd8.zip#" "pygtfs==0.1.3"] -ICON = "mdi:train" +_LOGGER = logging.getLogger(__name__) + +CONF_DATA = 'data' +CONF_DESTINATION = 'destination' +CONF_ORIGIN = 'origin' + +DEFAULT_NAME = 'GTFS Sensor' +DEFAULT_PATH = 'gtfs' + +ICON = 'mdi:train' + +TIME_FORMAT = '%Y-%m-%d %H:%M:%S' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ORIGIN): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_DATA): cv.isfile, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) -TIME_FORMAT = "%Y-%m-%d %H:%M:%S" # pylint: disable=too-many-locals - - def get_next_departure(sched, start_station_id, end_station_id): """Get the next departure for the given sched.""" origin_station = sched.stops_by_id(start_station_id)[0] destination_station = sched.stops_by_id(end_station_id)[0] now = datetime.datetime.now() - day_name = now.strftime("%A").lower() - now_str = now.strftime("%H:%M:%S") + day_name = now.strftime('%A').lower() + now_str = now.strftime('%H:%M:%S') from sqlalchemy.sql import text @@ -78,9 +96,9 @@ def get_next_departure(sched, start_station_id, end_station_id): for row in result: item = row - today = datetime.datetime.today().strftime("%Y-%m-%d") - departure_time_string = "{} {}".format(today, item[2]) - arrival_time_string = "{} {}".format(today, item[3]) + today = datetime.datetime.today().strftime('%Y-%m-%d') + departure_time_string = '{} {}'.format(today, item[2]) + arrival_time_string = '{} {}'.format(today, item[3]) departure_time = datetime.datetime.strptime(departure_time_string, TIME_FORMAT) arrival_time = datetime.datetime.strptime(arrival_time_string, @@ -91,72 +109,61 @@ def get_next_departure(sched, start_station_id, end_station_id): route = sched.routes_by_id(item[1])[0] - origin_stoptime_arrival_time = "{} {}".format(today, item[4]) - - origin_stoptime_departure_time = "{} {}".format(today, item[5]) - - dest_stoptime_arrival_time = "{} {}".format(today, item[11]) - - dest_stoptime_depart_time = "{} {}".format(today, item[12]) + origin_stoptime_arrival_time = '{} {}'.format(today, item[4]) + origin_stoptime_departure_time = '{} {}'.format(today, item[5]) + dest_stoptime_arrival_time = '{} {}'.format(today, item[11]) + dest_stoptime_depart_time = '{} {}'.format(today, item[12]) origin_stop_time_dict = { - "Arrival Time": origin_stoptime_arrival_time, - "Departure Time": origin_stoptime_departure_time, - "Drop Off Type": item[6], "Pickup Type": item[7], - "Shape Dist Traveled": item[8], "Headsign": item[9], - "Sequence": item[10] + 'Arrival Time': origin_stoptime_arrival_time, + 'Departure Time': origin_stoptime_departure_time, + 'Drop Off Type': item[6], 'Pickup Type': item[7], + 'Shape Dist Traveled': item[8], 'Headsign': item[9], + 'Sequence': item[10] } destination_stop_time_dict = { - "Arrival Time": dest_stoptime_arrival_time, - "Departure Time": dest_stoptime_depart_time, - "Drop Off Type": item[13], "Pickup Type": item[14], - "Shape Dist Traveled": item[15], "Headsign": item[16], - "Sequence": item[17] + 'Arrival Time': dest_stoptime_arrival_time, + 'Departure Time': dest_stoptime_depart_time, + 'Drop Off Type': item[13], 'Pickup Type': item[14], + 'Shape Dist Traveled': item[15], 'Headsign': item[16], + 'Sequence': item[17] } return { - "trip_id": item[0], - "trip": sched.trips_by_id(item[0])[0], - "route": route, - "agency": sched.agencies_by_id(route.agency_id)[0], - "origin_station": origin_station, - "departure_time": departure_time, - "destination_station": destination_station, - "arrival_time": arrival_time, - "seconds_until_departure": seconds_until, - "minutes_until_departure": minutes_until, - "origin_stop_time": origin_stop_time_dict, - "destination_stop_time": destination_stop_time_dict + 'trip_id': item[0], + 'trip': sched.trips_by_id(item[0])[0], + 'route': route, + 'agency': sched.agencies_by_id(route.agency_id)[0], + 'origin_station': origin_station, + 'departure_time': departure_time, + 'destination_station': destination_station, + 'arrival_time': arrival_time, + 'seconds_until_departure': seconds_until, + 'minutes_until_departure': minutes_until, + 'origin_stop_time': origin_stop_time_dict, + 'destination_stop_time': destination_stop_time_dict } def setup_platform(hass, config, add_devices, discovery_info=None): """Get the GTFS sensor.""" - if config.get("origin") is None: - _LOGGER.error("Origin must be set in the GTFS configuration!") - return False - - if config.get("destination") is None: - _LOGGER.error("Destination must be set in the GTFS configuration!") - return False - - if config.get("data") is None: - _LOGGER.error("Data must be set in the GTFS configuration!") - return False - - gtfs_dir = hass.config.path("gtfs") + gtfs_dir = hass.config.path(DEFAULT_PATH) + data = config.get(CONF_DATA) + origin = config.get(CONF_ORIGIN) + destination = config.get(CONF_DESTINATION) + name = config.get(CONF_NAME) if not os.path.exists(gtfs_dir): os.makedirs(gtfs_dir) - if not os.path.exists(os.path.join(gtfs_dir, config["data"])): + if not os.path.exists(os.path.join(gtfs_dir, data)): _LOGGER.error("The given GTFS data file/folder was not found!") return False import pygtfs - split_file_name = os.path.splitext(config["data"]) + split_file_name = os.path.splitext(data) sqlite_file = "{}.sqlite".format(split_file_name[0]) joined_path = os.path.join(gtfs_dir, sqlite_file) @@ -164,27 +171,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=no-member if len(gtfs.feeds) < 1: - pygtfs.append_feed(gtfs, os.path.join(gtfs_dir, - config["data"])) + pygtfs.append_feed(gtfs, os.path.join(gtfs_dir, data)) + + add_devices([GTFSDepartureSensor(gtfs, name, origin, destination)]) - dev = [] - dev.append(GTFSDepartureSensor(gtfs, config["origin"], - config["destination"])) - add_devices(dev) # pylint: disable=too-many-instance-attributes,too-few-public-methods - - class GTFSDepartureSensor(Entity): """Implementation of an GTFS departures sensor.""" - def __init__(self, pygtfs, origin, destination): + def __init__(self, pygtfs, name, origin, destination): """Initialize the sensor.""" self._pygtfs = pygtfs self.origin = origin self.destination = destination - self._name = "GTFS Sensor" - self._unit_of_measurement = "min" + self._name = name + self._unit_of_measurement = 'min' self._state = 0 self._attributes = {} self.lock = threading.Lock() @@ -220,23 +222,22 @@ class GTFSDepartureSensor(Entity): with self.lock: self._departure = get_next_departure(self._pygtfs, self.origin, self.destination) - self._state = self._departure["minutes_until_departure"] + self._state = self._departure['minutes_until_departure'] - origin_station = self._departure["origin_station"] - destination_station = self._departure["destination_station"] - origin_stop_time = self._departure["origin_stop_time"] - destination_stop_time = self._departure["destination_stop_time"] - agency = self._departure["agency"] - route = self._departure["route"] - trip = self._departure["trip"] + origin_station = self._departure['origin_station'] + destination_station = self._departure['destination_station'] + origin_stop_time = self._departure['origin_stop_time'] + destination_stop_time = self._departure['destination_stop_time'] + agency = self._departure['agency'] + route = self._departure['route'] + trip = self._departure['trip'] - name = "{} {} to {} next departure" + name = '{} {} to {} next departure' self._name = name.format(agency.agency_name, origin_station.stop_id, destination_station.stop_id) # Build attributes - self._attributes = {} def dict_for_table(resource): @@ -247,22 +248,22 @@ class GTFSDepartureSensor(Entity): def append_keys(resource, prefix=None): """Properly format key val pairs to append to attributes.""" for key, val in resource.items(): - if val == "" or val is None or key == "feed_id": + if val == "" or val is None or key == 'feed_id': continue - pretty_key = key.replace("_", " ") + pretty_key = key.replace('_', ' ') pretty_key = pretty_key.title() - pretty_key = pretty_key.replace("Id", "ID") - pretty_key = pretty_key.replace("Url", "URL") + pretty_key = pretty_key.replace('Id', 'ID') + pretty_key = pretty_key.replace('Url', 'URL') if prefix is not None and \ pretty_key.startswith(prefix) is False: - pretty_key = "{} {}".format(prefix, pretty_key) + pretty_key = '{} {}'.format(prefix, pretty_key) self._attributes[pretty_key] = val - append_keys(dict_for_table(agency), "Agency") - append_keys(dict_for_table(route), "Route") - append_keys(dict_for_table(trip), "Trip") - append_keys(dict_for_table(origin_station), "Origin Station") + append_keys(dict_for_table(agency), 'Agency') + append_keys(dict_for_table(route), 'Route') + append_keys(dict_for_table(trip), 'Trip') + append_keys(dict_for_table(origin_station), 'Origin Station') append_keys(dict_for_table(destination_station), - "Destination Station") - append_keys(origin_stop_time, "Origin Stop") - append_keys(destination_stop_time, "Destination Stop") + 'Destination Station') + append_keys(origin_stop_time, 'Origin Stop') + append_keys(destination_stop_time, 'Destination Stop') diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 35cc4aea42b..8857ee6d889 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -46,9 +46,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return - return homematic.setup_hmdevice_discovery_helper(HMSensor, - discovery_info, - add_callback_devices) + return homematic.setup_hmdevice_discovery_helper( + HMSensor, + discovery_info, + add_callback_devices + ) class HMSensor(homematic.HMDevice): @@ -76,45 +78,8 @@ class HMSensor(homematic.HMDevice): return HM_UNIT_HA_CAST.get(self._state, 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 HMSensor as pyHMSensor - - # 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 not isinstance(self._hmdevice, pyHMSensor): - _LOGGER.critical("This %s can't be use as sensor!", self._name) - return False - - # Does user defined value exist? - if self._state and self._state not in self._hmdevice.SENSORNODE: - # pylint: disable=logging-too-many-args - _LOGGER.critical("This %s have no sensor with %s! Values are", - self._name, self._state, - str(self._hmdevice.SENSORNODE.keys())) - return False - - # No param is set and more than 1 sensor nodes are present - if self._state is None and len(self._hmdevice.SENSORNODE) > 1: - _LOGGER.critical("This %s has multiple sensor nodes. " + - "Please us param. Values are: %s", self._name, - str(self._hmdevice.SENSORNODE.keys())) - return False - - _LOGGER.debug("%s is okay for linking", self._name) - return True - def _init_data_struct(self): """Generate a data dict (self._data) from hm metadata.""" - super()._init_data_struct() - - if self._state is None and len(self._hmdevice.SENSORNODE) == 1: - for value in self._hmdevice.SENSORNODE: - self._state = value - # Add state to data dict if self._state: _LOGGER.debug("%s init datadict with main node '%s'", self._name, diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index 3001171081e..2e493399d5b 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -5,12 +5,25 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.lastfm/ """ import re + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pylast==1.6.0'] + +CONF_USERS = 'users' ICON = 'mdi:lastfm' -REQUIREMENTS = ['pylast==1.6.0'] +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_USERS, default=[]): + vol.All(cv.ensure_list, [cv.string]), +}) # pylint: disable=unused-argument @@ -18,9 +31,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Last.fm platform.""" import pylast as lastfm network = lastfm.LastFMNetwork(api_key=config.get(CONF_API_KEY)) + add_devices( [LastfmSensor(username, - network) for username in config.get("users", [])]) + network) for username in config.get(CONF_USERS)]) class LastfmSensor(Entity): diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py new file mode 100644 index 00000000000..c1d145953e3 --- /dev/null +++ b/homeassistant/components/sensor/linux_battery.py @@ -0,0 +1,125 @@ +""" +Details about the built-in battery. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.linux_battery/ +""" +import logging +import os + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['batinfo==0.3'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_NAME = 'name' +ATTR_PATH = 'path' +ATTR_ALARM = 'alarm' +ATTR_CAPACITY = 'capacity' +ATTR_CAPACITY_LEVEL = 'capacity_level' +ATTR_CYCLE_COUNT = 'cycle_count' +ATTR_ENERGY_FULL = 'energy_full' +ATTR_ENERGY_FULL_DESIGN = 'energy_full_design' +ATTR_ENERGY_NOW = 'energy_now' +ATTR_MANUFACTURER = 'manufacturer' +ATTR_MODEL_NAME = 'model_name' +ATTR_POWER_NOW = 'power_now' +ATTR_SERIAL_NUMBER = 'serial_number' +ATTR_STATUS = 'status' +ATTR_VOLTAGE_MIN_DESIGN = 'voltage_min_design' +ATTR_VOLTAGE_NOW = 'voltage_now' + +CONF_BATTERY = 'battery' + +DEFAULT_BATTERY = 1 +DEFAULT_NAME = 'Battery' +DEFAULT_PATH = '/sys/class/power_supply' + +ICON = 'mdi:battery' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_BATTERY, default=DEFAULT_BATTERY): cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Linux Battery sensor.""" + name = config.get(CONF_NAME) + battery_id = config.get(CONF_BATTERY) + + try: + os.listdir(os.path.join(DEFAULT_PATH, 'BAT{}'.format(battery_id))) + except FileNotFoundError: + _LOGGER.error("No battery found") + return False + + add_devices([LinuxBatterySensor(name, battery_id)]) + + +# pylint: disable=too-few-public-methods +class LinuxBatterySensor(Entity): + """Representation of a Linux Battery sensor.""" + + def __init__(self, name, battery_id): + """Initialize the battery sensor.""" + import batinfo + self._battery = batinfo.Batteries() + + self._name = name + self._battery_stat = None + self._battery_id = battery_id - 1 + self._unit_of_measurement = '%' + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._battery_stat.capacity + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + @property + def state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_NAME: self._battery_stat.name, + ATTR_PATH: self._battery_stat.path, + ATTR_ALARM: self._battery_stat.alarm, + ATTR_CAPACITY_LEVEL: self._battery_stat.capacity_level, + ATTR_CYCLE_COUNT: self._battery_stat.cycle_count, + ATTR_ENERGY_FULL: self._battery_stat.energy_full, + ATTR_ENERGY_FULL_DESIGN: self._battery_stat.energy_full_design, + ATTR_ENERGY_NOW: self._battery_stat.energy_now, + ATTR_MANUFACTURER: self._battery_stat.manufacturer, + ATTR_MODEL_NAME: self._battery_stat.model_name, + ATTR_POWER_NOW: self._battery_stat.power_now, + ATTR_SERIAL_NUMBER: self._battery_stat.serial_number, + ATTR_STATUS: self._battery_stat.status, + ATTR_VOLTAGE_MIN_DESIGN: self._battery_stat.voltage_min_design, + ATTR_VOLTAGE_NOW: self._battery_stat.voltage_now, + } + + def update(self): + """Get the latest data and updates the states.""" + self._battery.update() + self._battery_stat = self._battery.stat[self._battery_id] diff --git a/homeassistant/components/sensor/mfi.py b/homeassistant/components/sensor/mfi.py index 90d07811304..0f06426a05b 100644 --- a/homeassistant/components/sensor/mfi.py +++ b/homeassistant/components/sensor/mfi.py @@ -7,23 +7,30 @@ https://home-assistant.io/components/sensor.mfi/ import logging import requests +import voluptuous as vol -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, STATE_ON, STATE_OFF, CONF_HOST) -from homeassistant.helpers import validate_config + CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, STATE_ON, STATE_OFF, CONF_HOST, + CONF_SSL, CONF_VERIFY_SSL, CONF_PORT) from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['mficlient==0.3.0'] _LOGGER = logging.getLogger(__name__) +DEFAULT_PORT = 6443 +DEFAULT_SSL = True +DEFAULT_VERIFY_SSL = True + DIGITS = { 'volts': 1, 'amps': 1, 'active_power': 0, 'temperature': 1, } + SENSOR_MODELS = [ 'Ubiquiti mFi-THS', 'Ubiquiti mFi-CS', @@ -31,28 +38,27 @@ SENSOR_MODELS = [ 'Input Analog', 'Input Digital', ] -CONF_TLS = 'use_tls' -CONF_VERIFY_TLS = 'verify_tls' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, +}) # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Setup mFi sensors.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_HOST, - CONF_USERNAME, - CONF_PASSWORD]}, - _LOGGER): - _LOGGER.error('A host, username, and password are required') - return False - host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - use_tls = bool(config.get(CONF_TLS, True)) - verify_tls = bool(config.get(CONF_VERIFY_TLS, True)) - default_port = use_tls and 6443 or 6080 - port = int(config.get('port', default_port)) + use_tls = config.get(CONF_SSL) + verify_tls = config.get(CONF_VERIFY_SSL) + default_port = use_tls and DEFAULT_PORT or 6080 + port = int(config.get(CONF_PORT, default_port)) from mficlient.client import FailedToLogin, MFiClient @@ -85,7 +91,13 @@ class MfiSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self._port.model == 'Input Digital': + try: + tag = self._port.tag + except ValueError: + tag = None + if tag is None: + return STATE_OFF + elif self._port.model == 'Input Digital': return self._port.value > 0 and STATE_ON or STATE_OFF else: digits = DIGITS.get(self._port.tag, 0) @@ -94,13 +106,18 @@ class MfiSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - if self._port.tag == 'temperature': + try: + tag = self._port.tag + except ValueError: + return 'State' + + if tag == 'temperature': return TEMP_CELSIUS - elif self._port.tag == 'active_pwr': + elif tag == 'active_pwr': return 'Watts' elif self._port.model == 'Input Digital': return 'State' - return self._port.tag + return tag def update(self): """Get the latest data.""" diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index d6c85993162..063c1dc8600 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -114,12 +114,11 @@ class ModbusSensor(Entity): def update(self): """Update the state of the sensor.""" if self._coil: - result = modbus.NETWORK.read_coils(self.register, 1) + result = modbus.HUB.read_coils(self.slave, self.register, 1) self._value = result.bits[0] else: - result = modbus.NETWORK.read_holding_registers( - unit=self.slave, address=self.register, - count=1) + result = modbus.HUB.read_holding_registers( + self.slave, self.register, 1) val = 0 for i, res in enumerate(result.registers): val += res * (2**(i*16)) diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py index 4e59cd2cd62..b8f635ec593 100644 --- a/homeassistant/components/sensor/mold_indicator.py +++ b/homeassistant/components/sensor/mold_indicator.py @@ -7,48 +7,51 @@ https://home-assistant.io/components/sensor.mold_indicator/ import logging import math +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.util as util from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_state_change -from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Mold Indicator" -CONF_INDOOR_TEMP = "indoor_temp_sensor" -CONF_OUTDOOR_TEMP = "outdoor_temp_sensor" -CONF_INDOOR_HUMIDITY = "indoor_humidity_sensor" -CONF_CALIBRATION_FACTOR = "calibration_factor" +DEFAULT_NAME = 'Mold Indicator' +CONF_INDOOR_TEMP = 'indoor_temp_sensor' +CONF_OUTDOOR_TEMP = 'outdoor_temp_sensor' +CONF_INDOOR_HUMIDITY = 'indoor_humidity_sensor' +CONF_CALIBRATION_FACTOR = 'calibration_factor' MAGNUS_K2 = 17.62 MAGNUS_K3 = 243.12 -ATTR_DEWPOINT = "Dewpoint" -ATTR_CRITICAL_TEMP = "Est. Crit. Temp" +ATTR_DEWPOINT = 'Dewpoint' +ATTR_CRITICAL_TEMP = 'Est. Crit. Temp' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_INDOOR_TEMP): cv.entity_id, + vol.Required(CONF_OUTDOOR_TEMP): cv.entity_id, + vol.Required(CONF_INDOOR_HUMIDITY): cv.entity_id, + vol.Optional(CONF_CALIBRATION_FACTOR): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup MoldIndicator sensor.""" - name = config.get('name', DEFAULT_NAME) + name = config.get(CONF_NAME, DEFAULT_NAME) indoor_temp_sensor = config.get(CONF_INDOOR_TEMP) outdoor_temp_sensor = config.get(CONF_OUTDOOR_TEMP) indoor_humidity_sensor = config.get(CONF_INDOOR_HUMIDITY) - calib_factor = util.convert(config.get(CONF_CALIBRATION_FACTOR), - float, None) + calib_factor = config.get(CONF_CALIBRATION_FACTOR) - if None in (indoor_temp_sensor, - outdoor_temp_sensor, indoor_humidity_sensor): - _LOGGER.error('Missing required key %s, %s or %s', - CONF_INDOOR_TEMP, CONF_OUTDOOR_TEMP, - CONF_INDOOR_HUMIDITY) - return False - - add_devices_callback([MoldIndicator( - hass, name, indoor_temp_sensor, - outdoor_temp_sensor, indoor_humidity_sensor, - calib_factor)]) + add_devices([MoldIndicator( + hass, name, indoor_temp_sensor, outdoor_temp_sensor, + indoor_humidity_sensor, calib_factor)]) # pylint: disable=too-many-instance-attributes @@ -83,16 +86,14 @@ class MoldIndicator(Entity): indoor_hum = hass.states.get(indoor_humidity_sensor) if indoor_temp: - self._indoor_temp = \ - MoldIndicator._update_temp_sensor(indoor_temp) + self._indoor_temp = MoldIndicator._update_temp_sensor(indoor_temp) if outdoor_temp: - self._outdoor_temp = \ - MoldIndicator._update_temp_sensor(outdoor_temp) + self._outdoor_temp = MoldIndicator._update_temp_sensor( + outdoor_temp) if indoor_hum: - self._indoor_hum = \ - MoldIndicator._update_hum_sensor(indoor_hum) + self._indoor_hum = MoldIndicator._update_hum_sensor(indoor_hum) self.update() @@ -130,19 +131,13 @@ class MoldIndicator(Entity): state.state) return None - # check unit - if unit != "%": - _LOGGER.error( - "Humidity sensor has unsupported unit: %s %s", - unit, - " (allowed: %)") + if unit != '%': + _LOGGER.error("Humidity sensor has unsupported unit: %s %s", + unit, " (allowed: %)") - # check range if hum > 100 or hum < 0: - _LOGGER.error( - "Humidity sensor out of range: %s %s", - hum, - " (allowed: 0-100%)") + _LOGGER.error("Humidity sensor out of range: %s %s", hum, + " (allowed: 0-100%)") return hum @@ -162,15 +157,10 @@ class MoldIndicator(Entity): return if entity_id == self._indoor_temp_sensor: - # update the indoor temp sensor self._indoor_temp = MoldIndicator._update_temp_sensor(new_state) - elif entity_id == self._outdoor_temp_sensor: - # update outdoor temp sensor self._outdoor_temp = MoldIndicator._update_temp_sensor(new_state) - elif entity_id == self._indoor_humidity_sensor: - # update humidity self._indoor_hum = MoldIndicator._update_hum_sensor(new_state) self.update() @@ -206,9 +196,8 @@ class MoldIndicator(Entity): self._outdoor_temp + (self._indoor_temp - self._outdoor_temp) / \ self._calib_factor - _LOGGER.debug( - "Estimated Critical Temperature: %f " + - TEMP_CELSIUS, self._crit_temp) + _LOGGER.debug("Estimated Critical Temperature: %f " + + TEMP_CELSIUS, self._crit_temp) # Then calculate the humidity at this point alpha = MAGNUS_K2 * self._crit_temp / (MAGNUS_K3 + self._crit_temp) @@ -242,7 +231,7 @@ class MoldIndicator(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return "%" + return '%' @property def state(self): @@ -260,9 +249,7 @@ class MoldIndicator(Entity): else: return { ATTR_DEWPOINT: - util.temperature.celsius_to_fahrenheit( - self._dewpoint), + util.temperature.celsius_to_fahrenheit(self._dewpoint), ATTR_CRITICAL_TEMP: - util.temperature.celsius_to_fahrenheit( - self._crit_temp), + util.temperature.celsius_to_fahrenheit(self._crit_temp), } diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index c3d4910b527..f12df688385 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -8,20 +8,19 @@ import logging import voluptuous as vol -import homeassistant.components.mqtt as mqtt +from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT) -from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers import template +from homeassistant.helpers.entity import Entity +import homeassistant.components.mqtt as mqtt +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = 'MQTT Sensor' DEPENDENCIES = ['mqtt'] -DEFAULT_NAME = "MQTT Sensor" - PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -33,9 +32,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup MQTT Sensor.""" add_devices([MqttSensor( hass, - config[CONF_NAME], - config[CONF_STATE_TOPIC], - config[CONF_QOS], + config.get(CONF_NAME), + config.get(CONF_STATE_TOPIC), + config.get(CONF_QOS), config.get(CONF_UNIT_OF_MEASUREMENT), config.get(CONF_VALUE_TEMPLATE), )]) diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index 6980b7e6f7b..a640d1e5268 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -11,7 +11,8 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, STATE_UNKNOWN +from homeassistant.const import ( + CONF_NAME, STATE_UNKNOWN, CONF_TIMEOUT) from homeassistant.components.mqtt import CONF_STATE_TOPIC import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -21,12 +22,18 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_DEVICE_ID = 'device_id' -CONF_TIMEOUT = 'timeout' +ATTR_DEVICE_ID = 'device_id' +ATTR_DISTANCE = 'distance' +ATTR_ID = 'id' +ATTR_ROOM = 'room' + +CONF_DEVICE_ID = 'device_id' +CONF_ROOM = 'room' -DEFAULT_TOPIC = 'room_presence' -DEFAULT_TIMEOUT = 5 DEFAULT_NAME = 'Room Sensor' +DEFAULT_TIMEOUT = 5 +DEFAULT_TOPIC = 'room_presence' +DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DEVICE_ID): cv.string, @@ -36,15 +43,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) MQTT_PAYLOAD = vol.Schema(vol.All(json.loads, vol.Schema({ - vol.Required('id'): cv.string, - vol.Required('distance'): vol.Coerce(float) + vol.Required(ATTR_ID): cv.string, + vol.Required(ATTR_DISTANCE): vol.Coerce(float), }, extra=vol.ALLOW_EXTRA))) # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup MQTT Sensor.""" - add_devices_callback([MQTTRoomSensor( + add_devices([MQTTRoomSensor( hass, config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -62,7 +69,7 @@ class MQTTRoomSensor(Entity): self._state = STATE_UNKNOWN self._hass = hass self._name = name - self._state_topic = state_topic + '/+' + self._state_topic = '{}{}'.format(state_topic, '/+') self._device_id = slugify(device_id).upper() self._timeout = timeout self._distance = None @@ -86,7 +93,7 @@ class MQTTRoomSensor(Entity): return device = _parse_update_data(topic, data) - if device.get('device_id') == self._device_id: + if device.get(CONF_DEVICE_ID) == self._device_id: if self._distance is None or self._updated is None: update_state(**device) else: @@ -95,8 +102,8 @@ class MQTTRoomSensor(Entity): # device is closer to another room OR # last update from other room was too long ago timediff = dt.utcnow() - self._updated - if device.get('room') == self._state \ - or device.get('distance') < self._distance \ + if device.get(ATTR_ROOM) == self._state \ + or device.get(ATTR_DISTANCE) < self._distance \ or timediff.seconds >= self._timeout: update_state(**device) @@ -116,7 +123,7 @@ class MQTTRoomSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - 'distance': self._distance + ATTR_DISTANCE: self._distance } @property @@ -129,11 +136,11 @@ def _parse_update_data(topic, data): """Parse the room presence update.""" parts = topic.split('/') room = parts[-1] - device_id = slugify(data.get('id')).upper() + device_id = slugify(data.get(ATTR_ID)).upper() distance = data.get('distance') parsed_data = { - 'device_id': device_id, - 'room': room, - 'distance': distance + ATTR_DEVICE_ID: device_id, + ATTR_ROOM: room, + ATTR_DISTANCE: distance } return parsed_data diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index f7e7fa30817..3b4635c829a 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -5,31 +5,44 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.octoprint/ """ import logging -import requests -from homeassistant.const import TEMP_CELSIUS, CONF_NAME +import requests +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity from homeassistant.loader import get_component +import homeassistant.helpers.config_validation as cv -DEPENDENCIES = ["octoprint"] + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['octoprint'] + +DEFAULT_NAME = 'OctoPrint' SENSOR_TYPES = { # API Endpoint, Group, Key, unit - "Temperatures": ["printer", "temperature", "*", TEMP_CELSIUS], - "Current State": ["printer", "state", "text", None], - "Job Percentage": ["job", "progress", "completion", "%"], + 'Temperatures': ['printer', 'temperature', '*', TEMP_CELSIUS], + 'Current State': ['printer', 'state', 'text', None], + 'Job Percentage': ['job', 'progress', 'completion', '%'], } -_LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the available OctoPrint sensors.""" octoprint = get_component('octoprint') - name = config.get(CONF_NAME, "OctoPrint") - monitored_conditions = config.get("monitored_conditions", - SENSOR_TYPES.keys()) + name = config.get(CONF_NAME) + monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) devices = [] types = ["actual", "target"] @@ -46,19 +59,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): SENSOR_TYPES[octo_type][1], tool) devices.append(new_sensor) - elif octo_type in SENSOR_TYPES: - new_sensor = OctoPrintSensor(octoprint.OCTOPRINT, - octo_type, - SENSOR_TYPES[octo_type][2], - name, - SENSOR_TYPES[octo_type][3], - SENSOR_TYPES[octo_type][0], - SENSOR_TYPES[octo_type][1]) - devices.append(new_sensor) else: _LOGGER.error("Unknown OctoPrint sensor type: %s", octo_type) - add_devices(devices) + new_sensor = OctoPrintSensor(octoprint.OCTOPRINT, + octo_type, + SENSOR_TYPES[octo_type][2], + name, + SENSOR_TYPES[octo_type][3], + SENSOR_TYPES[octo_type][0], + SENSOR_TYPES[octo_type][1]) + devices.append(new_sensor) + + add_devices(devices) # pylint: disable=too-many-instance-attributes @@ -66,14 +79,15 @@ class OctoPrintSensor(Entity): """Representation of an OctoPrint sensor.""" # pylint: disable=too-many-arguments - def __init__(self, api, condition, sensor_type, sensor_name, - unit, endpoint, group, tool=None): + def __init__(self, api, condition, sensor_type, sensor_name, unit, + endpoint, group, tool=None): """Initialize a new OctoPrint sensor.""" self.sensor_name = sensor_name if tool is None: - self._name = sensor_name + ' ' + condition + self._name = '{} {}'.format(sensor_name, condition) else: - self._name = sensor_name + ' ' + condition + ' ' + tool + ' temp' + self._name = '{} {} {} {}'.format( + sensor_name, condition, tool, ' temp') self.sensor_type = sensor_type self.api = api self._state = None diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index a2a3f0811f2..e7a78393b93 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -1,16 +1,28 @@ """ -Support for DS18B20 One Wire Sensors. +Support for 1-Wire temperature sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.onewire/ """ -import logging import os import time +import logging from glob import glob - -from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS +import voluptuous as vol from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.components.sensor import PLATFORM_SCHEMA + +CONF_MOUNT_DIR = 'mount_dir' +CONF_NAMES = 'names' +DEFAULT_MOUNT_DIR = '/sys/bus/w1/devices/' +DEVICE_FAMILIES = ('10', '22', '28', '3B', '42') + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAMES): {cv.string: cv.string}, + vol.Optional(CONF_MOUNT_DIR, default=DEFAULT_MOUNT_DIR): cv.string, +}) _LOGGER = logging.getLogger(__name__) @@ -18,22 +30,22 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the one wire Sensors.""" - base_dir = config.get('mount_dir', '/sys/bus/w1/devices/') - device_folders = glob(os.path.join(base_dir, '[10,22,28,3B,42]*')) + base_dir = config.get(CONF_MOUNT_DIR) sensor_ids = [] device_files = [] - for device_folder in device_folders: - sensor_ids.append(os.path.split(device_folder)[1]) - if base_dir.startswith('/sys/bus/w1/devices'): - device_files.append(os.path.join(device_folder, 'w1_slave')) - else: - device_files.append(os.path.join(device_folder, 'temperature')) + for device_family in DEVICE_FAMILIES: + for device_folder in glob(os.path.join(base_dir, device_family + + '[.-]*')): + sensor_ids.append(os.path.split(device_folder)[1]) + if base_dir == DEFAULT_MOUNT_DIR: + device_files.append(os.path.join(device_folder, 'w1_slave')) + else: + device_files.append(os.path.join(device_folder, 'temperature')) if device_files == []: - _LOGGER.error('No onewire sensor found.') - _LOGGER.error('Check if dtoverlay=w1-gpio,gpiopin=4.') - _LOGGER.error('is in your /boot/config.txt and') - _LOGGER.error('the correct gpiopin number is set.') + _LOGGER.error('No onewire sensor found. Check if ' + 'dtoverlay=w1-gpio is in your /boot/config.txt. ' + 'Check the mount_dir parameter if it\'s defined.') return devs = [] @@ -92,7 +104,7 @@ class OneWire(Entity): def update(self): """Get the latest data from the device.""" temp = -99 - if self._device_file.startswith('/sys/bus/w1/devices'): + if self._device_file.startswith(DEFAULT_MOUNT_DIR): lines = self._read_temp_raw() while lines[0].strip()[-3:] != 'YES': time.sleep(0.2) @@ -102,15 +114,18 @@ class OneWire(Entity): temp_string = lines[1][equals_pos+2:] temp = round(float(temp_string) / 1000.0, 1) else: - ds_device_file = open(self._device_file, 'r') - temp_read = ds_device_file.readlines() - ds_device_file.close() - if len(temp_read) == 1: - try: + try: + ds_device_file = open(self._device_file, 'r') + temp_read = ds_device_file.readlines() + ds_device_file.close() + if len(temp_read) == 1: temp = round(float(temp_read[0]), 1) - except ValueError: - _LOGGER.warning('Invalid temperature value read from ' + - self._device_file) + except ValueError: + _LOGGER.warning('Invalid temperature value read from ' + + self._device_file) + except FileNotFoundError: + _LOGGER.warning('Cannot read from sensor: ' + + self._device_file) if temp < -55 or temp > 125: return diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index efaa8d450b4..e7936cc0535 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -9,14 +9,24 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.const import (CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_PLATFORM, CONF_MONITORED_CONDITIONS) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_API_KEY, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_MONITORED_CONDITIONS) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pyowm==2.3.2'] +REQUIREMENTS = ['pyowm==2.4.0'] + _LOGGER = logging.getLogger(__name__) + +CONF_FORECAST = 'forecast' + +DEFAULT_NAME = 'OWM' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) + SENSOR_TYPES = { 'weather': ['Condition', None], 'temperature': ['Temperature', None], @@ -28,17 +38,14 @@ SENSOR_TYPES = { 'snow': ['Snow', 'mm'] } -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): 'openweathermap', - vol.Required(CONF_API_KEY): vol.Coerce(str), +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): - [vol.In(SENSOR_TYPES.keys())], - vol.Optional('forecast', default=False): cv.boolean + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_FORECAST, default=False): cv.boolean }) -# Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the OpenWeatherMap sensor.""" @@ -49,32 +56,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from pyowm import OWM SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit - forecast = config.get('forecast') - owm = OWM(config.get(CONF_API_KEY, None)) + + name = config.get(CONF_NAME) + forecast = config.get(CONF_FORECAST) + + owm = OWM(config.get(CONF_API_KEY)) if not owm: _LOGGER.error( "Connection error " - "Please check your settings for OpenWeatherMap.") + "Please check your settings for OpenWeatherMap") return False data = WeatherData(owm, forecast, hass.config.latitude, hass.config.longitude) dev = [] - try: - for variable in config['monitored_conditions']: - if variable not in SENSOR_TYPES: - _LOGGER.error('Sensor type: "%s" does not exist', variable) - else: - dev.append(OpenWeatherMapSensor(data, variable, - SENSOR_TYPES[variable][1])) - except KeyError: - pass + for variable in config[CONF_MONITORED_CONDITIONS]: + dev.append(OpenWeatherMapSensor( + name, data, variable, SENSOR_TYPES[variable][1])) if forecast: SENSOR_TYPES['forecast'] = ['Forecast', None] - dev.append(OpenWeatherMapSensor(data, 'forecast', - SENSOR_TYPES['temperature'][1])) + dev.append(OpenWeatherMapSensor( + name, data, 'forecast', SENSOR_TYPES['temperature'][1])) add_devices(dev) @@ -83,9 +87,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OpenWeatherMapSensor(Entity): """Implementation of an OpenWeatherMap sensor.""" - def __init__(self, weather_data, sensor_type, temp_unit): + def __init__(self, name, weather_data, sensor_type, temp_unit): """Initialize the sensor.""" - self.client_name = 'Weather' + self.client_name = name self._name = SENSOR_TYPES[sensor_type][0] self.owa_client = weather_data self.temp_unit = temp_unit diff --git a/homeassistant/components/sensor/pi_hole.py b/homeassistant/components/sensor/pi_hole.py new file mode 100644 index 00000000000..7cd3423bf65 --- /dev/null +++ b/homeassistant/components/sensor/pi_hole.py @@ -0,0 +1,101 @@ +""" +Support for getting statistical data from a Pi-Hole system. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.pi_hole/ +""" +import logging +import json + +import voluptuous as vol + +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor.rest import RestData +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_SSL, CONF_VERIFY_SSL) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) +_ENDPOINT = '/admin/api.php' + +ATTR_BLOCKED_DOMAINS = 'domains_blocked' +ATTR_PERCENTAGE_TODAY = 'percentage_today' +ATTR_QUERIES_TODAY = 'queries_today' + +DEFAULT_HOST = 'localhost' +DEFAULT_METHOD = 'GET' +DEFAULT_NAME = 'Pi-hole' +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Pi-Hole sensor.""" + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + method = 'GET' + payload = None + verify_ssl = config.get(CONF_VERIFY_SSL) + use_ssl = config.get(CONF_SSL) + + if use_ssl: + uri_scheme = 'https://' + else: + uri_scheme = 'http://' + + resource = "{}{}{}".format(uri_scheme, host, _ENDPOINT) + + rest = RestData(method, resource, payload, verify_ssl) + rest.update() + + if rest.data is None: + _LOGGER.error('Unable to fetch REST data') + return False + + add_devices([PiHoleSensor(hass, rest, name)]) + + +class PiHoleSensor(Entity): + """Representation of a Pi-Hole sensor.""" + + def __init__(self, hass, rest, name): + """Initialize a Pi-Hole sensor.""" + self._hass = hass + self.rest = rest + self._name = name + self._state = False + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + # pylint: disable=no-member + @property + def state(self): + """Return the state of the device.""" + return self._state.get('ads_blocked_today') + + # pylint: disable=no-member + @property + def state_attributes(self): + """Return the state attributes of the GPS.""" + return { + ATTR_BLOCKED_DOMAINS: self._state.get('domains_being_blocked'), + ATTR_PERCENTAGE_TODAY: self._state.get('ads_percentage_today'), + ATTR_QUERIES_TODAY: self._state.get('dns_queries_today'), + } + + def update(self): + """Get the latest data from REST API and updates the state.""" + self.rest.update() + self._state = json.loads(self.rest.data) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 022477d77a9..def47c79f4d 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -12,13 +12,16 @@ import requests from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, - CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN) + CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, CONF_VERIFY_SSL) from homeassistant.helpers.entity import Entity from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +_LOGGER = logging.getLogger(__name__) + DEFAULT_METHOD = 'GET' DEFAULT_NAME = 'REST Sensor' +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, @@ -27,10 +30,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, }) -_LOGGER = logging.getLogger(__name__) - # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): @@ -39,7 +41,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): resource = config.get(CONF_RESOURCE) method = config.get(CONF_METHOD) payload = config.get(CONF_PAYLOAD) - verify_ssl = config.get('verify_ssl', True) + verify_ssl = config.get(CONF_VERIFY_SSL) unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -58,7 +60,7 @@ class RestSensor(Entity): """Implementation of a REST sensor.""" def __init__(self, hass, rest, name, unit_of_measurement, value_template): - """Initialize the sensor.""" + """Initialize the REST sensor.""" self._hass = hass self.rest = rest self._name = name diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index f9f7270c8e3..60afd80997d 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -9,6 +9,7 @@ import voluptuous as vol import homeassistant.components.rfxtrx as rfxtrx import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PLATFORM from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.components.rfxtrx import ( @@ -20,7 +21,7 @@ DEPENDENCIES = ['rfxtrx'] _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.Schema({ - vol.Required("platform"): rfxtrx.DOMAIN, + vol.Required(CONF_PLATFORM): rfxtrx.DOMAIN, vol.Optional(CONF_DEVICES, default={}): vol.All(dict, rfxtrx.valid_sensor), vol.Optional(ATTR_AUTOMATIC_ADD, default=False): cv.boolean, }, extra=vol.ALLOW_EXTRA) @@ -31,7 +32,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # pylint: disable=too-many-locals from RFXtrx import SensorEvent sensors = [] - for packet_id, entity_info in config['devices'].items(): + for packet_id, entity_info in config[CONF_DEVICES].items(): event = rfxtrx.get_rfx_object(packet_id) device_id = "sensor_" + slugify(event.device.id_string.lower()) if device_id in rfxtrx.RFX_DEVICES: @@ -41,7 +42,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): sub_sensors = {} data_types = entity_info[ATTR_DATA_TYPE] if len(data_types) == 0: - data_types = ["Unknown"] + data_types = [''] for data_type in DATA_TYPES: if data_type in event.values: data_types = [data_type] @@ -52,7 +53,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): sensors.append(new_sensor) sub_sensors[_data_type] = new_sensor rfxtrx.RFX_DEVICES[device_id] = sub_sensors - add_devices_callback(sensors) def sensor_update(event): @@ -75,7 +75,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): sensors[key].entity_id, } ) - return # Add entity if not exist and the automatic_add is True @@ -86,7 +85,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): _LOGGER.info("Automatic add rfxtrx.sensor: %s", pkt_id) - data_type = "Unknown" + data_type = '' for _data_type in DATA_TYPES: if _data_type in event.values: data_type = _data_type @@ -119,9 +118,9 @@ class RfxtrxSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.event and self.data_type in self.event.values: - return self.event.values[self.data_type] - return None + if not self.event: + return None + return self.event.values.get(self.data_type) @property def name(self): @@ -131,8 +130,9 @@ class RfxtrxSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - if self.event: - return self.event.values + if not self.event: + return None + return self.event.values @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index a11d65d22bf..0f33a39bbcc 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -11,7 +11,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES) + CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES, + CONF_SSL) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -25,6 +26,7 @@ _THROTTLED_REFRESH = None DEFAULT_NAME = 'SABnzbd' DEFAULT_PORT = 8080 +DEFAULT_SSL = False MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) @@ -44,10 +46,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, }) -# pylint: disable=unused-argument +# pylint: disable=unused-argument, too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the SABnzbd sensors.""" from pysabnzbd import SabnzbdApi, SabnzbdApiException @@ -57,14 +60,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) api_key = config.get(CONF_API_KEY) monitored_types = config.get(CONF_MONITORED_VARIABLES) - base_url = "http://{}:{}/".format(host, port) + use_ssl = config.get(CONF_SSL) + + if use_ssl: + uri_scheme = 'https://' + else: + uri_scheme = 'http://' + + base_url = "{}{}:{}/".format(uri_scheme, host, port) sab_api = SabnzbdApi(base_url, api_key) try: sab_api.check_available() except SabnzbdApiException: - _LOGGER.exception("Connection to SABnzbd API failed") + _LOGGER.error("Connection to SABnzbd API failed") return False # pylint: disable=global-statement diff --git a/homeassistant/components/sensor/steam_online.py b/homeassistant/components/sensor/steam_online.py index a94eed9702e..ed12d4f7844 100644 --- a/homeassistant/components/sensor/steam_online.py +++ b/homeassistant/components/sensor/steam_online.py @@ -4,12 +4,24 @@ Sensor for Steam account status. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.steam_online/ """ +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['steamodd==4.21'] + +CONF_ACCOUNTS = 'accounts' ICON = 'mdi:steam' -REQUIREMENTS = ['steamodd==4.21'] +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_ACCOUNTS, default=[]): + vol.All(cv.ensure_list, [cv.string]), +}) # pylint: disable=unused-argument @@ -19,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): steamod.api.key.set(config.get(CONF_API_KEY)) add_devices( [SteamSensor(account, - steamod) for account in config.get('accounts', [])]) + steamod) for account in config.get(CONF_ACCOUNTS)]) class SteamSensor(Entity): diff --git a/homeassistant/components/sensor/supervisord.py b/homeassistant/components/sensor/supervisord.py index cebdfb83f14..22c1285a547 100644 --- a/homeassistant/components/sensor/supervisord.py +++ b/homeassistant/components/sensor/supervisord.py @@ -7,28 +7,41 @@ https://home-assistant.io/components/sensor.supervisord/ import logging import xmlrpc.client +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_URL from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DEFAULT_URL = 'http://localhost:9001/RPC2' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url, +}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Supervisord platform.""" + url = config.get(CONF_URL) try: - supervisor_server = xmlrpc.client.ServerProxy( - config.get('url', 'http://localhost:9001/RPC2')) + supervisor_server = xmlrpc.client.ServerProxy(url) except ConnectionRefusedError: _LOGGER.error('Could not connect to Supervisord') - return + return False + processes = supervisor_server.supervisor.getAllProcessInfo() + add_devices( [SupervisorProcessSensor(info, supervisor_server) for info in processes]) class SupervisorProcessSensor(Entity): - """Represent a supervisor-monitored process.""" + """Representation of a supervisor-monitored process.""" # pylint: disable=abstract-method def __init__(self, info, server): diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 893ec8154c4..125a2871f28 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -8,14 +8,13 @@ import logging import voluptuous as vol -import homeassistant.util.dt as dt_util - -from homeassistant.const import (CONF_RESOURCES, STATE_OFF, STATE_ON) from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_RESOURCES, STATE_OFF, STATE_ON) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==4.3.0'] +REQUIREMENTS = ['psutil==4.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py index fe5ebb17982..b7fcdd1b015 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -5,39 +5,66 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.temper/ """ import logging +import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME, TEMP_FAHRENHEIT from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/rkabadi/temper-python/archive/' - '3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip' - '#temperusb==1.2.3'] +REQUIREMENTS = ['temperusb==1.5.1'] + +CONF_SCALE = 'scale' +CONF_OFFSET = 'offset' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), + vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), + vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float) +}) # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Temper sensors.""" from temperusb.temper import TemperHandler temp_unit = hass.config.units.temperature_unit - name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME) + name = config.get(CONF_NAME) + scaling = { + 'scale': config.get(CONF_SCALE), + 'offset': config.get(CONF_OFFSET) + } temper_devices = TemperHandler().get_devices() - add_devices_callback([TemperSensor(dev, temp_unit, name + '_' + str(idx)) - for idx, dev in enumerate(temper_devices)]) + devices = [] + + for idx, dev in enumerate(temper_devices): + if idx != 0: + name = name + '_' + str(idx) + devices.append(TemperSensor(dev, temp_unit, name, scaling)) + + add_devices(devices) class TemperSensor(Entity): """Representation of a Temper temperature sensor.""" - def __init__(self, temper_device, temp_unit, name): + def __init__(self, temper_device, temp_unit, name, scaling): """Initialize the sensor.""" self.temper_device = temper_device self.temp_unit = temp_unit + self.scale = scaling['scale'] + self.offset = scaling['offset'] self.current_value = None self._name = name + # set calibration data + self.temper_device.set_calibration_data( + scale=self.scale, + offset=self.offset + ) + @property def name(self): """Return the name of the temperature sensor.""" @@ -58,7 +85,8 @@ class TemperSensor(Entity): try: format_str = ('fahrenheit' if self.temp_unit == TEMP_FAHRENHEIT else 'celsius') - self.current_value = self.temper_device.get_temperature(format_str) + sensor_value = self.temper_device.get_temperature(format_str) + self.current_value = round(sensor_value, 1) except IOError: _LOGGER.error('Failed to get temperature due to insufficient ' 'permissions. Try running with "sudo"') diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 961b6f39c17..743a1909ea5 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -5,20 +5,20 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.template/ """ import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, - ATTR_ENTITY_ID, MATCH_ALL) + ATTR_ENTITY_ID, MATCH_ALL, CONF_SENSORS) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.helpers import template +from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.helpers.event import track_state_change +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_SENSORS = 'sensors' SENSOR_SCHEMA = vol.Schema({ vol.Required(CONF_VALUE_TEMPLATE): cv.template, @@ -80,8 +80,7 @@ class SensorTemplate(Entity): """Called when the target device changes state.""" self.update_ha_state(True) - track_state_change(hass, entity_ids, - template_sensor_state_listener) + track_state_change(hass, entity_ids, template_sensor_state_listener) @property def name(self): diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py index 55c6aef31d0..c05217692ac 100644 --- a/homeassistant/components/sensor/torque.py +++ b/homeassistant/components/sensor/torque.py @@ -4,19 +4,28 @@ Support for the Torque OBD application. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.torque/ """ - +import logging import re -from homeassistant.helpers.entity import Entity -from homeassistant.components.http import HomeAssistantView +import voluptuous as vol -DOMAIN = 'torque' -DEPENDENCIES = ['http'] -SENSOR_EMAIL_FIELD = 'eml' -DEFAULT_NAME = 'vehicle' -ENTITY_NAME_FORMAT = '{0} {1}' +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_EMAIL, CONF_NAME) +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) API_PATH = '/api/torque' + +DEFAULT_NAME = 'vehicle' +DEPENDENCIES = ['http'] +DOMAIN = 'torque' + +ENTITY_NAME_FORMAT = '{0} {1}' + +SENSOR_EMAIL_FIELD = 'eml' SENSOR_NAME_KEY = r'userFullName(\w+)' SENSOR_UNIT_KEY = r'userUnit(\w+)' SENSOR_VALUE_KEY = r'k(\w+)' @@ -25,6 +34,11 @@ NAME_KEY = re.compile(SENSOR_NAME_KEY) UNIT_KEY = re.compile(SENSOR_UNIT_KEY) VALUE_KEY = re.compile(SENSOR_VALUE_KEY) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_EMAIL): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + def decode(value): """Double-decode required.""" @@ -39,12 +53,12 @@ def convert_pid(value): # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Torque platform.""" - vehicle = config.get('name', DEFAULT_NAME) - email = config.get('email', None) + vehicle = config.get(CONF_NAME) + email = config.get(CONF_EMAIL) sensors = {} - hass.wsgi.register_view(TorqueReceiveDataView(hass, email, vehicle, - sensors, add_devices)) + hass.wsgi.register_view(TorqueReceiveDataView( + hass, email, vehicle, sensors, add_devices)) return True diff --git a/homeassistant/components/sensor/uber.py b/homeassistant/components/sensor/uber.py index a27f8ca4def..7f250431984 100644 --- a/homeassistant/components/sensor/uber.py +++ b/homeassistant/components/sensor/uber.py @@ -7,50 +7,61 @@ https://home-assistant.io/components/sensor.uber/ import logging from datetime import timedelta +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['uber_rides==0.2.5'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["uber_rides==0.2.4"] -ICON = "mdi:taxi" +CONF_END_LATITUDE = 'end_latitude' +CONF_END_LONGITUDE = 'end_longitude' +CONF_PRODUCT_IDS = 'product_ids' +CONF_SERVER_TOKEN = 'server_token' +CONF_START_LATITUDE = 'start_latitude' +CONF_START_LONGITUDE = 'start_longitude' + +ICON = 'mdi:taxi' -# Return cached results if last scan was less then this time ago. MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SERVER_TOKEN): cv.string, + vol.Required(CONF_START_LATITUDE): cv.latitude, + vol.Required(CONF_START_LONGITUDE): cv.longitude, + vol.Optional(CONF_END_LATITUDE): cv.latitude, + vol.Optional(CONF_END_LONGITUDE): cv.longitude, + vol.Optional(CONF_PRODUCT_IDS, default=[]): + vol.All(cv.ensure_list, [cv.string]), +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Uber sensor.""" - if None in (config.get("start_latitude"), config.get("start_longitude")): - _LOGGER.error( - "You must set start latitude and longitude to use the Uber sensor!" - ) - return False - - if config.get("server_token") is None: - _LOGGER.error("You must set a server_token to use the Uber sensor!") - return False - from uber_rides.session import Session - session = Session(server_token=config.get("server_token")) + session = Session(server_token=config.get(CONF_SERVER_TOKEN)) - wanted_product_ids = config.get("product_ids") + wanted_product_ids = config.get(CONF_PRODUCT_IDS) dev = [] - timeandpriceest = UberEstimate(session, config["start_latitude"], - config["start_longitude"], - config.get("end_latitude"), - config.get("end_longitude")) + timeandpriceest = UberEstimate(session, config[CONF_START_LATITUDE], + config[CONF_START_LONGITUDE], + config.get(CONF_END_LATITUDE), + config.get(CONF_END_LONGITUDE)) for product_id, product in timeandpriceest.products.items(): if (wanted_product_ids is not None) and \ (product_id not in wanted_product_ids): continue - dev.append(UberSensor("time", timeandpriceest, product_id, product)) - if (product.get("price_details") is not None) and \ - product["price_details"]["estimate"] is not "Metered": - dev.append(UberSensor("price", timeandpriceest, - product_id, product)) + dev.append(UberSensor('time', timeandpriceest, product_id, product)) + if (product.get('price_details') is not None) and \ + product['price_details']['estimate'] is not 'Metered': + dev.append(UberSensor( + 'price', timeandpriceest, product_id, product)) add_devices(dev) @@ -64,20 +75,20 @@ class UberSensor(Entity): self._product_id = product_id self._product = product self._sensortype = sensorType - self._name = "{} {}".format(self._product["display_name"], + self._name = '{} {}'.format(self._product['display_name'], self._sensortype) - if self._sensortype == "time": - self._unit_of_measurement = "min" - time_estimate = self._product.get("time_estimate_seconds", 0) + if self._sensortype == 'time': + self._unit_of_measurement = 'min' + time_estimate = self._product.get('time_estimate_seconds', 0) self._state = int(time_estimate / 60) - elif self._sensortype == "price": - if self._product.get("price_details") is not None: - price_details = self._product["price_details"] - self._unit_of_measurement = price_details.get("currency_code") - if price_details.get("low_estimate") is not None: - statekey = "minimum" + elif self._sensortype == 'price': + if self._product.get('price_details') is not None: + price_details = self._product['price_details'] + self._unit_of_measurement = price_details.get('currency_code') + if price_details.get('low_estimate') is not None: + statekey = 'minimum' else: - statekey = "low_estimate" + statekey = 'low_estimate' self._state = int(price_details.get(statekey, 0)) else: self._state = 0 @@ -86,8 +97,8 @@ class UberSensor(Entity): @property def name(self): """Return the name of the sensor.""" - if "uber" not in self._name.lower(): - self._name = "Uber{}".format(self._name) + if 'uber' not in self._name.lower(): + self._name = 'Uber{}'.format(self._name) return self._name @property @@ -105,35 +116,35 @@ class UberSensor(Entity): """Return the state attributes.""" time_estimate = self._product.get("time_estimate_seconds") params = { - "Product ID": self._product["product_id"], - "Product short description": self._product["short_description"], - "Product display name": self._product["display_name"], - "Product description": self._product["description"], - "Pickup time estimate (in seconds)": time_estimate, - "Trip duration (in seconds)": self._product.get("duration"), - "Vehicle Capacity": self._product["capacity"] + 'Product ID': self._product['product_id'], + 'Product short description': self._product['short_description'], + 'Product display name': self._product['display_name'], + 'Product description': self._product['description'], + 'Pickup time estimate (in seconds)': time_estimate, + 'Trip duration (in seconds)': self._product.get('duration'), + 'Vehicle Capacity': self._product['capacity'] } - if self._product.get("price_details") is not None: - price_details = self._product["price_details"] - dunit = price_details.get("distance_unit") - distance_key = "Trip distance (in {}s)".format(dunit) - distance_val = self._product.get("distance") - params["Cost per minute"] = price_details.get("cost_per_minute") - params["Distance units"] = price_details.get("distance_unit") - params["Cancellation fee"] = price_details.get("cancellation_fee") - cpd = price_details.get("cost_per_distance") - params["Cost per distance"] = cpd - params["Base price"] = price_details.get("base") - params["Minimum price"] = price_details.get("minimum") - params["Price estimate"] = price_details.get("estimate") - params["Price currency code"] = price_details.get("currency_code") - params["High price estimate"] = price_details.get("high_estimate") - params["Low price estimate"] = price_details.get("low_estimate") - params["Surge multiplier"] = price_details.get("surge_multiplier") + if self._product.get('price_details') is not None: + price_details = self._product['price_details'] + dunit = price_details.get('distance_unit') + distance_key = 'Trip distance (in {}s)'.format(dunit) + distance_val = self._product.get('distance') + params['Cost per minute'] = price_details.get('cost_per_minute') + params['Distance units'] = price_details.get('distance_unit') + params['Cancellation fee'] = price_details.get('cancellation_fee') + cpd = price_details.get('cost_per_distance') + params['Cost per distance'] = cpd + params['Base price'] = price_details.get('base') + params['Minimum price'] = price_details.get('minimum') + params['Price estimate'] = price_details.get('estimate') + params['Price currency code'] = price_details.get('currency_code') + params['High price estimate'] = price_details.get('high_estimate') + params['Low price estimate'] = price_details.get('low_estimate') + params['Surge multiplier'] = price_details.get('surge_multiplier') else: - distance_key = "Trip distance (in miles)" - distance_val = self._product.get("distance") + distance_key = 'Trip distance (in miles)' + distance_val = self._product.get('distance') params[distance_key] = distance_val @@ -149,14 +160,14 @@ class UberSensor(Entity): """Get the latest data from the Uber API and update the states.""" self.data.update() self._product = self.data.products[self._product_id] - if self._sensortype == "time": - time_estimate = self._product.get("time_estimate_seconds", 0) + if self._sensortype == 'time': + time_estimate = self._product.get('time_estimate_seconds', 0) self._state = int(time_estimate / 60) - elif self._sensortype == "price": - price_details = self._product.get("price_details") + elif self._sensortype == 'price': + price_details = self._product.get('price_details') if price_details is not None: - min_price = price_details.get("minimum") - self._state = int(price_details.get("low_estimate", min_price)) + min_price = price_details.get('minimum') + self._state = int(price_details.get('low_estimate', min_price)) else: self._state = 0 @@ -188,39 +199,39 @@ class UberEstimate(object): products_response = client.get_products( self.start_latitude, self.start_longitude) - products = products_response.json.get("products") + products = products_response.json.get('products') for product in products: - self.products[product["product_id"]] = product + self.products[product['product_id']] = product if self.end_latitude is not None and self.end_longitude is not None: price_response = client.get_price_estimates( self.start_latitude, self.start_longitude, self.end_latitude, self.end_longitude) - prices = price_response.json.get("prices", []) + prices = price_response.json.get('prices', []) for price in prices: - product = self.products[price["product_id"]] - product["duration"] = price.get("duration", "0") - product["distance"] = price.get("distance", "0") - price_details = product.get("price_details") - if product.get("price_details") is None: + product = self.products[price['product_id']] + product['duration'] = price.get('duration', '0') + product['distance'] = price.get('distance', '0') + price_details = product.get('price_details') + if product.get('price_details') is None: price_details = {} - price_details["estimate"] = price.get("estimate", "0") - price_details["high_estimate"] = price.get("high_estimate", - "0") - price_details["low_estimate"] = price.get("low_estimate", "0") - price_details["currency_code"] = price.get("currency_code") - surge_multiplier = price.get("surge_multiplier", "0") - price_details["surge_multiplier"] = surge_multiplier - product["price_details"] = price_details + price_details['estimate'] = price.get('estimate', '0') + price_details['high_estimate'] = price.get('high_estimate', + '0') + price_details['low_estimate'] = price.get('low_estimate', '0') + price_details['currency_code'] = price.get('currency_code') + surge_multiplier = price.get('surge_multiplier', '0') + price_details['surge_multiplier'] = surge_multiplier + product['price_details'] = price_details estimate_response = client.get_pickup_time_estimates( self.start_latitude, self.start_longitude) - estimates = estimate_response.json.get("times") + estimates = estimate_response.json.get('times') for estimate in estimates: - self.products[estimate["product_id"]][ - "time_estimate_seconds"] = estimate.get("estimate", "0") + self.products[estimate['product_id']][ + 'time_estimate_seconds'] = estimate.get('estimate', '0') diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py index 4252c9d8b33..932da40bc9f 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/sensor/verisure.py @@ -2,11 +2,13 @@ Interfaces with Verisure sensors. For more details about this platform, please refer to the documentation at -documentation at https://home-assistant.io/components/verisure/ +https://home-assistant.io/components/sensor.verisure/ """ import logging from homeassistant.components.verisure import HUB as hub +from homeassistant.components.verisure import ( + CONF_THERMOMETERS, CONF_HYDROMETERS, CONF_MOUSE) from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity @@ -17,7 +19,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Verisure platform.""" sensors = [] - if int(hub.config.get('thermometers', '1')): + if int(hub.config.get(CONF_THERMOMETERS, 1)): hub.update_climate() sensors.extend([ VerisureThermometer(value.id) @@ -25,7 +27,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if hasattr(value, 'temperature') and value.temperature ]) - if int(hub.config.get('hygrometers', '1')): + if int(hub.config.get(CONF_HYDROMETERS, 1)): hub.update_climate() sensors.extend([ VerisureHygrometer(value.id) @@ -33,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if hasattr(value, 'humidity') and value.humidity ]) - if int(hub.config.get('mouse', '1')): + if int(hub.config.get(CONF_MOUSE, 1)): hub.update_mousedetection() sensors.extend([ VerisureMouseDetection(value.deviceLabel) @@ -56,8 +58,7 @@ class VerisureThermometer(Entity): def name(self): """Return the name of the device.""" return '{} {}'.format( - hub.climate_status[self._id].location, - "Temperature") + hub.climate_status[self._id].location, 'Temperature') @property def state(self): @@ -91,8 +92,7 @@ class VerisureHygrometer(Entity): def name(self): """Return the name of the sensor.""" return '{} {}'.format( - hub.climate_status[self._id].location, - "Humidity") + hub.climate_status[self._id].location, 'Humidity') @property def state(self): @@ -126,8 +126,7 @@ class VerisureMouseDetection(Entity): def name(self): """Return the name of the sensor.""" return '{} {}'.format( - hub.mouse_status[self._id].location, - "Mouse") + hub.mouse_status[self._id].location, 'Mouse') @property def state(self): diff --git a/homeassistant/components/sensor/xbox_live.py b/homeassistant/components/sensor/xbox_live.py new file mode 100644 index 00000000000..90983e1df83 --- /dev/null +++ b/homeassistant/components/sensor/xbox_live.py @@ -0,0 +1,112 @@ +""" +Sensor for Xbox Live account status. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.xbox_live/ +""" +import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_API_KEY, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +ICON = 'mdi:xbox' + +REQUIREMENTS = ['xboxapi==0.1.1'] + +CONF_XUID = 'xuid' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_XUID): vol.All(cv.ensure_list, [cv.string]) +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Xbox platform.""" + from xboxapi import xbox_api + api = xbox_api.XboxApi(config.get(CONF_API_KEY)) + devices = [] + + for xuid in config.get(CONF_XUID): + new_device = XboxSensor(hass, api, xuid) + if new_device.success_init: + devices.append(new_device) + + if len(devices) > 0: + add_devices(devices) + else: + return False + + +# pylint: disable=too-many-instance-attributes +class XboxSensor(Entity): + """A class for the Xbox account.""" + + def __init__(self, hass, api, xuid): + """Initialize the sensor.""" + self._hass = hass + self._state = STATE_UNKNOWN + self._presence = {} + self._xuid = xuid + self._api = api + + # get profile info + profile = self._api.get_user_profile(self._xuid) + + if profile.get('success', True) \ + and profile.get('code', 0) != 28: + self.success_init = True + self._gamertag = profile.get('Gamertag') + self._picture = profile.get('GameDisplayPicRaw') + else: + self.success_init = False + + @property + def name(self): + """Return the name of the sensor.""" + return self._gamertag + + @property + def entity_id(self): + """Return the entity ID.""" + return 'sensor.xbox_' + self._gamertag + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attributes = {} + for device in self._presence: + for title in device.get('titles'): + attributes[ + '{} {}'.format(device.get('type'), + title.get('placement')) + ] = title.get('name') + + return attributes + + @property + def entity_picture(self): + """Avatar of the account.""" + return self._picture + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return ICON + + def update(self): + """Update state data from Xbox API.""" + presence = self._api.get_user_presence(self._xuid) + self._state = presence.get('state', STATE_UNKNOWN) + self._presence = presence.get('devices', {}) diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/sensor/zigbee.py index 2692bcf9715..6b455230aa6 100644 --- a/homeassistant/components/sensor/zigbee.py +++ b/homeassistant/components/sensor/zigbee.py @@ -7,32 +7,45 @@ https://home-assistant.io/components/sensor.zigbee/ import logging from binascii import hexlify +import voluptuous as vol + from homeassistant.components import zigbee +from homeassistant.components.zigbee import PLATFORM_SCHEMA from homeassistant.const import TEMP_CELSIUS from homeassistant.core import JobPriority from homeassistant.helpers.entity import Entity -DEPENDENCIES = ["zigbee"] _LOGGER = logging.getLogger(__name__) +CONF_TYPE = 'type' +CONF_MAX_VOLTS = 'max_volts' -def setup_platform(hass, config, add_entities, discovery_info=None): - """Setup the Z-Wave platform. +DEFAULT_VOLTS = 1.2 +DEPENDENCIES = ['zigbee'] + +TYPES = ['analog', 'temperature'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TYPE): vol.In(TYPES), + vol.Optional(CONF_MAX_VOLTS, default=DEFAULT_VOLTS): vol.Coerce(float), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the ZigBee platform. Uses the 'type' config value to work out which type of ZigBee sensor we're dealing with and instantiates the relevant classes to handle it. """ - typ = config.get("type", "").lower() - if not typ: - _LOGGER.exception( - "Must include 'type' when configuring a ZigBee sensor.") - return + typ = config.get(CONF_TYPE) + try: sensor_class, config_class = TYPE_CLASSES[typ] except KeyError: _LOGGER.exception("Unknown ZigBee sensor type: %s", typ) return - add_entities([sensor_class(hass, config_class(config))]) + + add_devices([sensor_class(hass, config_class(config))]) class ZigBeeTemperatureSensor(Entity): diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index ac6d9829fc5..4f79a2ee627 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -39,6 +39,11 @@ foursquare: description: Vertical accuracy of the user's location, in meters. example: 1 +group: + reload: + description: "Reload group configuration." + fields: + persistent_notification: create: description: Show a notification in the frontend diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py index b0a6a93cb4d..5845e611c31 100644 --- a/homeassistant/components/switch/acer_projector.py +++ b/homeassistant/components/switch/acer_projector.py @@ -1,21 +1,40 @@ """ -Use serial protocol of acer projector to obtain state of the projector. +Use serial protocol of Acer projector to obtain state of the projector. -This component allows to control almost all projectors from acer using -their RS232 serial communication protocol. +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/switch.acer_projector/ """ import logging import re -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN, - CONF_NAME, CONF_FILENAME) +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_NAME, CONF_FILENAME) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyserial==3.1.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_TIMEOUT = 'timeout' +CONF_WRITE_TIMEOUT = 'write_timeout' + +DEFAULT_NAME = 'Acer Projector' +DEFAULT_TIMEOUT = 1 +DEFAULT_WRITE_TIMEOUT = 1 -LAMP_HOURS = 'Lamp Hours' -INPUT_SOURCE = 'Input Source' ECO_MODE = 'ECO Mode' -MODEL = 'Model' + +ICON = 'mdi:projector' + +INPUT_SOURCE = 'Input Source' + LAMP = 'Lamp' +LAMP_HOURS = 'Lamp Hours' + +MODEL = 'Model' # Commands known to the projector CMD_DICT = {LAMP: '* 0 Lamp ?\r', @@ -26,38 +45,34 @@ CMD_DICT = {LAMP: '* 0 Lamp ?\r', STATE_ON: '* 0 IR 001\r', STATE_OFF: '* 0 IR 002\r'} -_LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyserial<=3.1'] - -ICON = 'mdi:projector' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FILENAME): cv.isfile, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT): + cv.positive_int, +}) -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Connect with serial port and return Acer Projector.""" - serial_port = config.get(CONF_FILENAME, None) - name = config.get(CONF_NAME, 'Projector') - timeout = config.get('timeout', 1) - write_timeout = config.get('write_timeout', 1) + serial_port = config.get(CONF_FILENAME) + name = config.get(CONF_NAME) + timeout = config.get(CONF_TIMEOUT) + write_timeout = config.get(CONF_WRITE_TIMEOUT) - if not serial_port: - _LOGGER.error('Missing path of serial device') - return - - devices = [] - devices.append(AcerSwitch(serial_port, name, timeout, write_timeout)) - add_devices_callback(devices) + add_devices([AcerSwitch(serial_port, name, timeout, write_timeout)]) class AcerSwitch(SwitchDevice): """Represents an Acer Projector as an switch.""" - def __init__(self, serial_port, name='Projector', - timeout=1, write_timeout=1, **kwargs): + def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): """Init of the Acer projector.""" import serial - self.ser = serial.Serial(port=serial_port, timeout=timeout, - write_timeout=write_timeout, **kwargs) + self.ser = serial.Serial( + port=serial_port, timeout=timeout, write_timeout=write_timeout, + **kwargs) self._serial_port = serial_port self._name = name self._state = False @@ -73,18 +88,17 @@ class AcerSwitch(SwitchDevice): """Write to the projector and read the return.""" import serial ret = "" - # Sometimes the projector won't answer for no reason, - # or the projector was disconnected during runtime. - # Thisway the projector can be reconnected and will still - # work + # Sometimes the projector won't answer for no reason or the projector + # was disconnected during runtime. + # This way the projector can be reconnected and will still work try: if not self.ser.is_open: self.ser.open() msg = msg.encode('utf-8') self.ser.write(msg) - # size is an experience value there is no real limit. - # AFAIK there is no limit and no end character so - # we will usually need to wait for timeout + # Size is an experience value there is no real limit. + # AFAIK there is no limit and no end character so we will usually + # need to wait for timeout ret = self.ser.read_until(size=20).decode('utf-8') except serial.SerialException: _LOGGER.error('Problem comunicating with %s', self._serial_port) diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index 40b83371f9a..e20a47cf084 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -7,30 +7,53 @@ https://home-assistant.io/components/switch.command_line/ import logging import subprocess -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_VALUE_TEMPLATE +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_FRIENDLY_NAME, CONF_SWITCHES, CONF_VALUE_TEMPLATE, CONF_COMMAND_OFF, + CONF_COMMAND_ON, CONF_COMMAND_STATE) from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +SWITCH_SCHEMA = vol.Schema({ + vol.Optional(CONF_COMMAND_OFF, default='true'): cv.string, + vol.Optional(CONF_COMMAND_ON, default='true'): cv.string, + vol.Optional(CONF_COMMAND_STATE): cv.string, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}), +}) + # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return switches controlled by shell commands.""" - switches = config.get('switches', {}) - devices = [] + devices = config.get(CONF_SWITCHES, {}) + switches = [] - for dev_name, properties in switches.items(): - devices.append( + for device_name, device_config in devices.items(): + switches.append( CommandSwitch( hass, - properties.get('name', dev_name), - properties.get('oncmd', 'true'), - properties.get('offcmd', 'true'), - properties.get('statecmd', False), - properties.get(CONF_VALUE_TEMPLATE, False))) + device_config.get(CONF_FRIENDLY_NAME, device_name), + device_config.get(CONF_COMMAND_ON), + device_config.get(CONF_COMMAND_OFF), + device_config.get(CONF_COMMAND_STATE), + device_config.get(CONF_VALUE_TEMPLATE) + ) + ) - add_devices_callback(devices) + if not switches: + _LOGGER.error("No switches added") + return False + + add_devices(switches) # pylint: disable=too-many-instance-attributes diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py index b65c521bad5..377826695a3 100644 --- a/homeassistant/components/switch/dlink.py +++ b/homeassistant/components/switch/dlink.py @@ -12,20 +12,27 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) import homeassistant.helpers.config_validation as cv +from homeassistant.const import TEMP_CELSIUS REQUIREMENTS = ['https://github.com/LinuxChristian/pyW215/archive/' - 'v0.1.1.zip#pyW215==0.1.1'] + 'v0.3.4.zip#pyW215==0.3.4'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'D-link Smart Plug W215' DEFAULT_PASSWORD = '' DEFAULT_USERNAME = 'admin' +CONF_USE_LEGACY_PROTOCOL = 'use_legacy_protocol' + +ATTR_CURRENT_CONSUMPTION = 'Current Consumption' +ATTR_TOTAL_CONSUMPTION = 'Total Consumption' +ATTR_TEMPERATURE = 'Temperature' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Required(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_USE_LEGACY_PROTOCOL, default=False): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -38,16 +45,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None): host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) + use_legacy_protocol = config.get(CONF_USE_LEGACY_PROTOCOL) name = config.get(CONF_NAME) - add_devices([SmartPlugSwitch(SmartPlug(host, password, username), name)]) + add_devices([SmartPlugSwitch(hass, SmartPlug(host, + password, + username, + use_legacy_protocol), + name)]) class SmartPlugSwitch(SwitchDevice): """Representation of a D-link Smart Plug switch.""" - def __init__(self, smartplug, name): + def __init__(self, hass, smartplug, name): """Initialize the switch.""" + self.units = hass.config.units self.smartplug = smartplug self._name = name @@ -56,6 +69,23 @@ class SmartPlugSwitch(SwitchDevice): """Return the name of the Smart Plug, if any.""" return self._name + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + ui_temp = self.units.temperature(int(self.smartplug.temperature), + TEMP_CELSIUS) + temperature = "{} {}".format(ui_temp, self.units.temperature_unit) + current_consumption = "{} W".format(self.smartplug.current_consumption) + total_consumption = "{} W".format(self.smartplug.total_consumption) + + attrs = { + ATTR_CURRENT_CONSUMPTION: current_consumption, + ATTR_TOTAL_CONSUMPTION: total_consumption, + ATTR_TEMPERATURE: temperature + } + + return attrs + @property def current_power_watt(self): """Return the current power usage in Watt.""" diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 8240be692ba..41746f9a0ef 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -6,39 +6,40 @@ https://home-assistant.io/components/switch.edimax/ """ import logging -from homeassistant.components.switch import DOMAIN, SwitchDevice +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) -from homeassistant.helpers import validate_config +import homeassistant.helpers.config_validation as cv -# constants -DEFAULT_USERNAME = 'admin' -DEFAULT_PASSWORD = '1234' -DEVICE_DEFAULT_NAME = 'Edimax Smart Plug' REQUIREMENTS = ['https://github.com/rkabadi/pyedimax/archive/' '365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1'] _LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = 'Edimax Smart Plug' +DEFAULT_PASSWORD = '1234' +DEFAULT_USERNAME = 'admin' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, +}) + # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return Edimax Smart Plugs.""" from pyedimax.smartplug import SmartPlug - # pylint: disable=global-statement - # check for required values in configuration file - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_HOST]}, - _LOGGER): - return False - host = config.get(CONF_HOST) - auth = (config.get(CONF_USERNAME, DEFAULT_USERNAME), - config.get(CONF_PASSWORD, DEFAULT_PASSWORD)) - name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME) + auth = (config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) + name = config.get(CONF_NAME) - add_devices_callback([SmartPlugSwitch(SmartPlug(host, auth), name)]) + add_devices([SmartPlugSwitch(SmartPlug(host, auth), name)]) class SmartPlugSwitch(SwitchDevice): diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 61a40315620..a0c982952e2 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.light import is_on, turn_on from homeassistant.components.sun import next_setting, next_rising from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.const import CONF_NAME, CONF_PLATFORM, EVENT_TIME_CHANGED +from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers.event import track_utc_time_change from homeassistant.util.color import color_temperature_to_rgb as temp_to_rgb from homeassistant.util.color import color_RGB_to_xy @@ -124,7 +124,7 @@ class FluxSwitch(SwitchDevice): self._stop_colortemp = stop_colortemp self._brightness = brightness self._mode = mode - self.tracker = None + self.unsub_tracker = None @property def name(self): @@ -139,15 +139,17 @@ class FluxSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn on flux.""" self._state = True - self.tracker = track_utc_time_change(self.hass, - self.flux_update, - second=[0, 30]) + self.unsub_tracker = track_utc_time_change(self.hass, self.flux_update, + second=[0, 30]) self.update_ha_state() def turn_off(self, **kwargs): """Turn off flux.""" + if self.unsub_tracker is not None: + self.unsub_tracker() + self.unsub_tracker = None + self._state = False - self.hass.bus.remove_listener(EVENT_TIME_CHANGED, self.tracker) self.update_ha_state() # pylint: disable=too-many-locals diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py index 40874138e53..5a911ee3d74 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -6,31 +6,50 @@ https://home-assistant.io/components/switch.hikvision/ """ import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_OFF, STATE_ON) + CONF_NAME, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, STATE_OFF, + STATE_ON) from homeassistant.helpers.entity import ToggleEntity +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['hikvision==0.4'] _LOGGING = logging.getLogger(__name__) -REQUIREMENTS = ['hikvision==0.4'] + +DEFAULT_NAME = 'Hikvision Camera Motion Detection' +DEFAULT_PASSWORD = '12345' +DEFAULT_PORT = 80 +DEFAULT_USERNAME = 'admin' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, +}) + + # pylint: disable=too-many-arguments # pylint: disable=too-many-instance-attributes - - -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Hikvision camera.""" import hikvision.api from hikvision.error import HikvisionError, MissingParamError - host = config.get(CONF_HOST, None) - port = config.get('port', "80") - name = config.get('name', "Hikvision Camera Motion Detection") - username = config.get(CONF_USERNAME, "admin") - password = config.get(CONF_PASSWORD, "12345") + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) try: hikvision_cam = hikvision.api.CreateDevice( - host, port=port, username=username, - password=password, is_https=False) + host, port=port, username=username, password=password, + is_https=False) except MissingParamError as param_err: _LOGGING.error("Missing required param: %s", param_err) return False @@ -38,9 +57,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): _LOGGING.error("Unable to connect: %s", conn_err) return False - add_devices_callback([ - HikvisionMotionSwitch(name, hikvision_cam) - ]) + add_devices([HikvisionMotionSwitch(name, hikvision_cam)]) class HikvisionMotionSwitch(ToggleEntity): @@ -85,6 +102,6 @@ class HikvisionMotionSwitch(ToggleEntity): def update(self): """Update Motion Detection state.""" enabled = self._hikvision_cam.is_motion_detection_enabled() - _LOGGING.info('enabled: %s', enabled) + _LOGGING.info("enabled: %s", enabled) self._state = STATE_ON if enabled else STATE_OFF diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index e9f103b95fa..e13027780c6 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -19,9 +19,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): if discovery_info is None: return - return homematic.setup_hmdevice_discovery_helper(HMSwitch, - discovery_info, - add_callback_devices) + return homematic.setup_hmdevice_discovery_helper( + HMSwitch, + discovery_info, + add_callback_devices + ) class HMSwitch(homematic.HMDevice, SwitchDevice): @@ -56,47 +58,12 @@ class HMSwitch(homematic.HMDevice, SwitchDevice): 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 switch", 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, SwitchPowermeter - - super()._init_data_struct() - # Use STATE - if isinstance(self._hmdevice, Switch): - self._state = "STATE" - - # Use LEVEL - if isinstance(self._hmdevice, Dimmer): - self._state = "LEVEL" + self._state = "STATE" + self._data.update({self._state: STATE_UNKNOWN}) # Need sensor values for SwitchPowermeter - if isinstance(self._hmdevice, SwitchPowermeter): - for node in self._hmdevice.SENSORNODE: - self._data.update({node: STATE_UNKNOWN}) - - # Add state to data dict - if self._state: - _LOGGER.debug("%s init data dict 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) + for node in self._hmdevice.SENSORNODE: + self._data.update({node: STATE_UNKNOWN}) diff --git a/homeassistant/components/switch/mfi.py b/homeassistant/components/switch/mfi.py index cca59111495..48e4741e770 100644 --- a/homeassistant/components/switch/mfi.py +++ b/homeassistant/components/switch/mfi.py @@ -7,43 +7,49 @@ https://home-assistant.io/components/switch.mfi/ import logging import requests +import voluptuous as vol -from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME, CONF_SSL, + CONF_VERIFY_SSL) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['mficlient==0.3.0'] _LOGGER = logging.getLogger(__name__) +DEFAULT_PORT = 6443 +DEFAULT_SSL = True +DEFAULT_VERIFY_SSL = True + SWITCH_MODELS = [ 'Outlet', 'Output 5v', 'Output 12v', 'Output 24v', ] -CONF_TLS = 'use_tls' -CONF_VERIFY_TLS = 'verify_tls' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, +}) # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Setup mFi sensors.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: ['host', - CONF_USERNAME, - CONF_PASSWORD]}, - _LOGGER): - _LOGGER.error('A host, username, and password are required') - return False - - host = config.get('host') - username = config.get('username') - password = config.get('password') - use_tls = bool(config.get(CONF_TLS, True)) - verify_tls = bool(config.get(CONF_VERIFY_TLS, True)) - default_port = use_tls and 6443 or 6080 - port = int(config.get('port', default_port)) + host = config.get(CONF_HOST) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + use_tls = config.get(CONF_SSL) + verify_tls = config.get(CONF_VERIFY_SSL) + default_port = use_tls and DEFAULT_PORT or 6080 + port = int(config.get(CONF_PORT, default_port)) from mficlient.client import FailedToLogin, MFiClient diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py index 971947a6ed3..2ae0c74991d 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -90,12 +90,10 @@ class ModbusSwitch(ToggleEntity): self.update() if self._coil: - modbus.NETWORK.write_coil(self.register, True) + modbus.HUB.write_coil(self.slave, self.register, True) else: val = self.register_value | (0x0001 << self.bit) - modbus.NETWORK.write_register(unit=self.slave, - address=self.register, - value=val) + modbus.HUB.write_register(self.slave, self.register, val) def turn_off(self, **kwargs): """Set switch off.""" @@ -103,23 +101,22 @@ class ModbusSwitch(ToggleEntity): self.update() if self._coil: - modbus.NETWORK.write_coil(self.register, False) + modbus.HUB.write_coil(self.slave, self.register, False) else: val = self.register_value & ~(0x0001 << self.bit) - modbus.NETWORK.write_register(unit=self.slave, - address=self.register, - value=val) + modbus.HUB.write_register(self.slave, self.register, val) def update(self): """Update the state of the switch.""" if self._coil: - result = modbus.NETWORK.read_coils(self.register, 1) + result = modbus.HUB.read_coils(self.slave, self.register, 1) self.register_value = result.bits[0] self._is_on = self.register_value else: - result = modbus.NETWORK.read_holding_registers( - unit=self.slave, address=self.register, - count=1) + result = modbus.HUB.read_holding_registers( + self.slave, + self.register, + 1) val = 0 for i, res in enumerate(result.registers): val += res * (2**(i*16)) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 2a2b2aed547..d17ea82cd32 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -8,24 +8,23 @@ import logging import voluptuous as vol -import homeassistant.components.mqtt as mqtt -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) -import homeassistant.helpers.config_validation as cv +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import ( + CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON) from homeassistant.helpers import template +import homeassistant.components.mqtt as mqtt +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_PAYLOAD_ON = 'payload_on' -CONF_PAYLOAD_OFF = 'payload_off' - -DEFAULT_NAME = "MQTT Switch" -DEFAULT_PAYLOAD_ON = "ON" -DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_NAME = 'MQTT Switch' +DEFAULT_PAYLOAD_ON = 'ON' +DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ @@ -37,19 +36,20 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Add MQTT switch.""" - add_devices_callback([MqttSwitch( +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the MQTT switch.""" + add_devices([MqttSwitch( hass, - config[CONF_NAME], + config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), - config[CONF_COMMAND_TOPIC], - config[CONF_QOS], - config[CONF_RETAIN], - config[CONF_PAYLOAD_ON], - config[CONF_PAYLOAD_OFF], - config[CONF_OPTIMISTIC], - config.get(CONF_VALUE_TEMPLATE))]) + config.get(CONF_COMMAND_TOPIC), + config.get(CONF_QOS), + config.get(CONF_RETAIN), + config.get(CONF_PAYLOAD_ON), + config.get(CONF_PAYLOAD_OFF), + config.get(CONF_OPTIMISTIC), + config.get(CONF_VALUE_TEMPLATE) + )]) # pylint: disable=too-many-arguments, too-many-instance-attributes @@ -86,8 +86,8 @@ class MqttSwitch(SwitchDevice): # Force into optimistic mode. self._optimistic = True else: - mqtt.subscribe(hass, self._state_topic, message_received, - self._qos) + mqtt.subscribe( + hass, self._state_topic, message_received, self._qos) @property def should_poll(self): diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py index b33e71df49d..7d30990e823 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -1,63 +1,9 @@ """ -Netio switch component. - -The Netio platform allows you to control your [Netio] -(http://www.netio-products.com/en/overview/) Netio4, Netio4 All and Netio 230B. -These are smart outlets controllable through ethernet and/or WiFi that reports -consumptions (Netio4all). - -To use these devices in your installation, add the following to your -configuration.yaml file: -``` -switch: - - platform: netio - host: netio-living - outlets: - 1: "AppleTV" - 2: "Htpc" - 3: "Lampe Gauche" - 4: "Lampe Droite" - - platform: netio - host: 192.168.1.43 - port: 1234 - username: user - password: pwd - outlets: - 1: "Nothing..." - 4: "Lampe du fer" -``` - -To get pushed updates from the netio devices, one can add this lua code in the -device interface as an action triggered on "Netio" "System variables updated" -with an 'Always' schedule: - -`` --- this will send socket and consumption status updates via CGI --- to given address. Associate with 'System variables update' event --- to get consumption updates when they show up - -local address='ha:8123' -local path = '/api/netio/' - - -local output = {} -for i = 1, 4 do for _, what in pairs({'state', 'consumption', - 'cumulatedConsumption', 'consumptionStart'}) do - local varname = string.format('output%d_%s', i, what) - table.insert(output, - varname..'='..tostring(devices.system[varname]):gsub(" ","|")) -end end - -local qs = table.concat(output, '&') -local url = string.format('http://%s%s?%s', address, path, qs) -devices.system.CustomCGI{url=url} -``` - +The Netio switch component. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.netio/ """ - import logging from collections import namedtuple from datetime import timedelta diff --git a/homeassistant/components/switch/orvibo.py b/homeassistant/components/switch/orvibo.py index b2b8ed41abe..0ce1426dd1f 100644 --- a/homeassistant/components/switch/orvibo.py +++ b/homeassistant/components/switch/orvibo.py @@ -6,40 +6,60 @@ https://home-assistant.io/components/switch.orvibo/ """ import logging -from homeassistant.components.switch import SwitchDevice +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_SWITCHES, CONF_MAC, CONF_DISCOVERY) +import homeassistant.helpers.config_validation as cv -DEFAULT_NAME = "Orvibo S20 Switch" REQUIREMENTS = ['orvibo==1.1.1'] + _LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = 'Orvibo S20 Switch' +DEFAULT_DISCOVERY = True + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SWITCHES, default=[]): + vol.All(cv.ensure_list, [{ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_MAC): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string + }]), + vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, +}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Find and return S20 switches.""" - from orvibo.s20 import S20, S20Exception + """Setup S20 switches.""" + from orvibo.s20 import discover, S20, S20Exception + switch_data = {} switches = [] - switch_conf = config.get('switches', [config]) + switch_conf = config.get(CONF_SWITCHES, [config]) + + if config.get(CONF_DISCOVERY): + _LOGGER.info("Discovering S20 switches ...") + switch_data.update(discover()) for switch in switch_conf: - if switch.get('host') is None: - _LOGGER.error("Missing required variable: host") - continue - host = switch.get('host') - mac = switch.get('mac') + switch_data[switch.get(CONF_HOST)] = switch + + for host, data in switch_data.items(): try: - switches.append(S20Switch(switch.get('name', DEFAULT_NAME), - S20(host, mac=mac))) + switches.append(S20Switch(data.get(CONF_NAME), + S20(host, mac=data.get(CONF_MAC)))) _LOGGER.info("Initialized S20 at %s", host) except S20Exception: - _LOGGER.exception("S20 at %s couldn't be initialized", - host) + _LOGGER.error("S20 at %s couldn't be initialized", host) add_devices_callback(switches) class S20Switch(SwitchDevice): - """Representsation of an S20 switch.""" + """Representation of an S20 switch.""" def __init__(self, name, s20): """Initialize the S20 device.""" diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py index b9175bee9b7..c9ee19aa0e3 100644 --- a/homeassistant/components/switch/pulseaudio_loopback.py +++ b/homeassistant/components/switch/pulseaudio_loopback.py @@ -9,69 +9,75 @@ import re import socket from datetime import timedelta +import voluptuous as vol + import homeassistant.util as util -from homeassistant.components.switch import SwitchDevice -from homeassistant.util import convert +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _PULSEAUDIO_SERVERS = {} -DEFAULT_NAME = "paloopback" -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 4712 -DEFAULT_BUFFER_SIZE = 1024 -DEFAULT_TCP_TIMEOUT = 3 -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) +CONF_BUFFER_SIZE = 'buffer_size' +CONF_SINK_NAME = 'sink_name' +CONF_SOURCE_NAME = 'source_name' +CONF_TCP_TIMEOUT = 'tcp_timeout' -LOAD_CMD = "load-module module-loopback sink={0} source={1}" -UNLOAD_CMD = "unload-module {0}" -MOD_REGEX = r"index: ([0-9]+)\s+name: " \ - r"\s+argument: (?=<.*sink={0}.*>)(?=<.*source={1}.*>)" +DEFAULT_BUFFER_SIZE = 1024 +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'paloopback' +DEFAULT_PORT = 4712 +DEFAULT_TCP_TIMEOUT = 3 IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring." +LOAD_CMD = "load-module module-loopback sink={0} source={1}" + +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +MOD_REGEX = r"index: ([0-9]+)\s+name: " \ + r"\s+argument: (?=<.*sink={0}.*>)(?=<.*source={1}.*>)" + +UNLOAD_CMD = "unload-module {0}" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SINK_NAME): cv.string, + vol.Required(CONF_SOURCE_NAME): cv.string, + vol.Optional(CONF_BUFFER_SIZE, default=DEFAULT_BUFFER_SIZE): + cv.positive_int, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_TCP_TIMEOUT, default=DEFAULT_TCP_TIMEOUT): + cv.positive_int, +}) + # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Read in all of our configuration, and initialize the loopback switch.""" - if config.get('sink_name') is None: - _LOGGER.error("Missing required variable: sink_name") - return False - - if config.get('source_name') is None: - _LOGGER.error("Missing required variable: source_name") - return False - - name = convert(config.get('name'), str, DEFAULT_NAME) - sink_name = config.get('sink_name') - source_name = config.get('source_name') - host = convert(config.get('host'), str, DEFAULT_HOST) - port = convert(config.get('port'), int, DEFAULT_PORT) - buffer_size = convert(config.get('buffer_size'), int, DEFAULT_BUFFER_SIZE) - tcp_timeout = convert(config.get('tcp_timeout'), int, DEFAULT_TCP_TIMEOUT) + name = config.get(CONF_NAME) + sink_name = config.get(CONF_SINK_NAME) + source_name = config.get(CONF_SOURCE_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + buffer_size = config.get(CONF_BUFFER_SIZE) + tcp_timeout = config.get(CONF_TCP_TIMEOUT) server_id = str.format("{0}:{1}", host, port) if server_id in _PULSEAUDIO_SERVERS: server = _PULSEAUDIO_SERVERS[server_id] - else: server = PAServer(host, port, buffer_size, tcp_timeout) - _PULSEAUDIO_SERVERS[server_id] = server - add_devices_callback([PALoopbackSwitch( - hass, - name, - server, - sink_name, - source_name - )]) + add_devices([PALoopbackSwitch(hass, name, server, sink_name, source_name)]) class PAServer(): - """Represents a pulseaudio server.""" + """Representation of a Pulseaudio server.""" _current_module_state = "" @@ -88,11 +94,11 @@ class PAServer(): sock.settimeout(self._tcp_timeout) try: sock.connect((self._pa_host, self._pa_port)) - _LOGGER.info("Calling pulseaudio:" + cmd) + _LOGGER.info("Calling pulseaudio: %s", cmd) sock.send((cmd + "\n").encode("utf-8")) if response_expected: return_data = self._get_full_response(sock) - _LOGGER.debug("Data received from pulseaudio: " + return_data) + _LOGGER.debug("Data received from pulseaudio: %s", return_data) else: return_data = "" finally: @@ -103,11 +109,11 @@ class PAServer(): """Helper method to get the full response back from pulseaudio.""" result = "" rcv_buffer = sock.recv(self._buffer_size) - result += rcv_buffer.decode("utf-8") + result += rcv_buffer.decode('utf-8') while len(rcv_buffer) == self._buffer_size: rcv_buffer = sock.recv(self._buffer_size) - result += rcv_buffer.decode("utf-8") + result += rcv_buffer.decode('utf-8') return result @@ -118,10 +124,7 @@ class PAServer(): def turn_on(self, sink_name, source_name): """Send a command to pulseaudio to turn on the loopback.""" - self._send_command(str.format(LOAD_CMD, - sink_name, - source_name), - False) + self._send_command(str.format(LOAD_CMD, sink_name, source_name), False) def turn_off(self, module_idx): """Send a command to pulseaudio to turn off the loopback.""" @@ -129,8 +132,7 @@ class PAServer(): def get_module_idx(self, sink_name, source_name): """For a sink/source, return it's module id in our cache, if found.""" - result = re.search(str.format(MOD_REGEX, - re.escape(sink_name), + result = re.search(str.format(MOD_REGEX, re.escape(sink_name), re.escape(source_name)), self._current_module_state) if result and result.group(1).isdigit(): @@ -141,11 +143,10 @@ class PAServer(): # pylint: disable=too-many-arguments class PALoopbackSwitch(SwitchDevice): - """Represents the presence or absence of a pa loopback module.""" + """Representation the presence or absence of a PA loopback module.""" - def __init__(self, hass, name, pa_server, - sink_name, source_name): - """Initialize the switch.""" + def __init__(self, hass, name, pa_server, sink_name, source_name): + """Initialize the Pulseaudio switch.""" self._module_idx = -1 self._hass = hass self._name = name @@ -168,8 +169,8 @@ class PALoopbackSwitch(SwitchDevice): if not self.is_on: self._pa_svr.turn_on(self._sink_name, self._source_name) self._pa_svr.update_module_state(no_throttle=True) - self._module_idx = self._pa_svr.get_module_idx(self._sink_name, - self._source_name) + self._module_idx = self._pa_svr.get_module_idx( + self._sink_name, self._source_name) self.update_ha_state() else: _LOGGER.warning(IGNORED_SWITCH_WARN) @@ -179,8 +180,8 @@ class PALoopbackSwitch(SwitchDevice): if self.is_on: self._pa_svr.turn_off(self._module_idx) self._pa_svr.update_module_state(no_throttle=True) - self._module_idx = self._pa_svr.get_module_idx(self._sink_name, - self._source_name) + self._module_idx = self._pa_svr.get_module_idx( + self._sink_name, self._source_name) self.update_ha_state() else: _LOGGER.warning(IGNORED_SWITCH_WARN) @@ -188,5 +189,5 @@ class PALoopbackSwitch(SwitchDevice): def update(self): """Refresh state in case an alternate process modified this data.""" self._pa_svr.update_module_state() - self._module_idx = self._pa_svr.get_module_idx(self._sink_name, - self._source_name) + self._module_idx = self._pa_svr.get_module_idx( + self._sink_name, self._source_name) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 6778315843e..2b043c110a4 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -5,28 +5,27 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.template/ """ import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.switch import ( ENTITY_ID_FORMAT, SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, STATE_OFF, STATE_ON, - ATTR_ENTITY_ID, MATCH_ALL) + ATTR_ENTITY_ID, MATCH_ALL, CONF_SWITCHES) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity import generate_entity_id -from homeassistant.helpers.script import Script from homeassistant.helpers import template +from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_state_change - -CONF_SWITCHES = 'switches' - -ON_ACTION = 'turn_on' -OFF_ACTION = 'turn_off' +from homeassistant.helpers.script import Script +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_ON, STATE_OFF, 'true', 'false'] +ON_ACTION = 'turn_on' +OFF_ACTION = 'turn_off' + SWITCH_SCHEMA = vol.Schema({ vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA, @@ -91,8 +90,7 @@ class SwitchTemplate(SwitchDevice): """Called when the target device changes state.""" self.update_ha_state(True) - track_state_change(hass, entity_ids, - template_switch_state_listener) + track_state_change(hass, entity_ids, template_switch_state_listener) @property def name(self): diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index a1de3621b9a..ddb10e74c37 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -4,25 +4,35 @@ Support for TPLink HS100/HS110 smart switch. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.tplink/ """ -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ( - CONF_HOST, CONF_NAME) +import logging + +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_HOST, CONF_NAME) +import homeassistant.helpers.config_validation as cv -# constants -DEVICE_DEFAULT_NAME = 'HS100' REQUIREMENTS = ['https://github.com/gadgetreactor/pyHS100/archive/' 'master.zip#pyHS100==0.1.2'] +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'TPLink Switch HS100' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the TPLink switch platform.""" from pyHS100.pyHS100 import SmartPlug host = config.get(CONF_HOST) - name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME) + name = config.get(CONF_NAME) - add_devices_callback([SmartPlugSwitch(SmartPlug(host), - name)]) + add_devices([SmartPlugSwitch(SmartPlug(host), name)]) class SmartPlugSwitch(SwitchDevice): diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py index 1bd0a46fb78..d7974335811 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/switch/verisure.py @@ -2,11 +2,12 @@ Support for Verisure Smartplugs. For more details about this platform, please refer to the documentation at -documentation at https://home-assistant.io/components/verisure/ +https://home-assistant.io/components/switch.verisure/ """ import logging from homeassistant.components.verisure import HUB as hub +from homeassistant.components.verisure import CONF_SMARTPLUGS from homeassistant.components.switch import SwitchDevice _LOGGER = logging.getLogger(__name__) @@ -14,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Verisure switch platform.""" - if not int(hub.config.get('smartplugs', '1')): + if not int(hub.config.get(CONF_SMARTPLUGS, 1)): return False hub.update_smartplugs() diff --git a/homeassistant/components/switch/zigbee.py b/homeassistant/components/switch/zigbee.py index 4588be139a2..7a58b0867c1 100644 --- a/homeassistant/components/switch/zigbee.py +++ b/homeassistant/components/switch/zigbee.py @@ -4,18 +4,29 @@ Contains functionality to use a ZigBee device as a switch. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.zigbee/ """ +import voluptuous as vol + from homeassistant.components.switch import SwitchDevice from homeassistant.components.zigbee import ( - ZigBeeDigitalOut, ZigBeeDigitalOutConfig) + ZigBeeDigitalOut, ZigBeeDigitalOutConfig, PLATFORM_SCHEMA) -DEPENDENCIES = ["zigbee"] +DEPENDENCIES = ['zigbee'] + +CONF_ON_STATE = 'on_state' + +DEFAULT_ON_STATE = 'high' +DEPENDENCIES = ['zigbee'] + +STATES = ['high', 'low'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ON_STATE): vol.In(STATES), +}) -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the ZigBee switch platform.""" - add_entities([ - ZigBeeSwitch(hass, ZigBeeDigitalOutConfig(config)) - ]) + add_devices([ZigBeeSwitch(hass, ZigBeeDigitalOutConfig(config))]) class ZigBeeSwitch(ZigBeeDigitalOut, SwitchDevice): diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py deleted file mode 100644 index 73901ab61df..00000000000 --- a/homeassistant/components/thermostat/homematic.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Support for Homematic thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/thermostat.homematic/ -""" -import logging -import homeassistant.components.homematic as homematic -from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.util.temperature import convert -from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN - -DEPENDENCIES = ['homematic'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_callback_devices, discovery_info=None): - """Setup the Homematic thermostat platform.""" - if discovery_info is None: - return - - return homematic.setup_hmdevice_discovery_helper(HMThermostat, - discovery_info, - add_callback_devices) - - -# pylint: disable=abstract-method -class HMThermostat(homematic.HMDevice, ThermostatDevice): - """Representation of a Homematic thermostat.""" - - @property - def unit_of_measurement(self): - """Return the unit of measurement that is used.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - if not self.available: - return None - return self._data["ACTUAL_TEMPERATURE"] - - @property - def target_temperature(self): - """Return the target temperature.""" - if not self.available: - return None - return self._data["SET_TEMPERATURE"] - - def set_temperature(self, temperature): - """Set new target temperature.""" - if not self.available: - return None - self._hmdevice.set_temperature(temperature) - - @property - def min_temp(self): - """Return the minimum temperature - 4.5 means off.""" - return convert(4.5, TEMP_CELSIUS, self.unit_of_measurement) - - @property - def max_temp(self): - """Return the maximum temperature - 30.5 means on.""" - return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement) - - def _check_hm_to_ha_object(self): - """Check if possible to use the Homematic object as this HA type.""" - from pyhomematic.devicetypes.thermostats import HMThermostat\ - as pyHMThermostat - - # 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 isinstance(self._hmdevice, pyHMThermostat): - return True - - _LOGGER.critical("This %s can't be use as thermostat", self._name) - return False - - def _init_data_struct(self): - """Generate a data dict (self._data) from the Homematic metadata.""" - super()._init_data_struct() - - # Add state to data dict - self._data.update({"CONTROL_MODE": STATE_UNKNOWN, - "SET_TEMPERATURE": STATE_UNKNOWN, - "ACTUAL_TEMPERATURE": STATE_UNKNOWN}) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 1231a4128fa..8634184fe57 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -9,26 +9,46 @@ import threading import time from datetime import timedelta -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config, discovery -from homeassistant.util import Throttle +import voluptuous as vol -DOMAIN = "verisure" +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import discovery +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['vsure==0.10.2'] _LOGGER = logging.getLogger(__name__) +CONF_ALARM = 'alarm' +CONF_CODE_DIGITS = 'code_digits' +CONF_HYDROMETERS = 'hygrometers' +CONF_LOCKS = 'locks' +CONF_MOUSE = 'mouse' +CONF_SMARTPLUGS = 'smartplugs' +CONF_THERMOMETERS = 'thermometers' + +DOMAIN = 'verisure' + HUB = None +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_ALARM, default=True): cv.boolean, + vol.Optional(CONF_CODE_DIGITS, default=4): cv.positive_int, + vol.Optional(CONF_HYDROMETERS, default=True): cv.boolean, + vol.Optional(CONF_LOCKS, default=True): cv.boolean, + vol.Optional(CONF_MOUSE, default=True): cv.boolean, + vol.Optional(CONF_SMARTPLUGS, default=True): cv.boolean, + vol.Optional(CONF_THERMOMETERS, default=True): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + def setup(hass, config): """Setup the Verisure component.""" - if not validate_config(config, - {DOMAIN: [CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): - return False - import verisure global HUB HUB = VerisureHub(config[DOMAIN], verisure) diff --git a/homeassistant/components/weblink.py b/homeassistant/components/weblink.py index 08ba7eb036e..df9dcef9ac1 100644 --- a/homeassistant/components/weblink.py +++ b/homeassistant/components/weblink.py @@ -6,30 +6,39 @@ https://home-assistant.io/components/weblink/ """ import logging +import voluptuous as vol + +from homeassistant.const import (CONF_NAME, CONF_ICON, CONF_URL) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify - -DOMAIN = "weblink" -DEPENDENCIES = [] - -ATTR_NAME = 'name' -ATTR_URL = 'url' -ATTR_ICON = 'icon' +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +CONF_ENTITIES = 'entities' + +DOMAIN = 'weblink' + +ENTITIES_SCHEMA = vol.Schema({ + vol.Required(CONF_URL): cv.url, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ENTITIES): [ENTITIES_SCHEMA], + }), +}, extra=vol.ALLOW_EXTRA) + def setup(hass, config): """Setup weblink component.""" links = config.get(DOMAIN) - for link in links.get('entities'): - if ATTR_NAME not in link or ATTR_URL not in link: - _LOGGER.error("You need to set both %s and %s to add a %s", - ATTR_NAME, ATTR_URL, DOMAIN) - continue - Link(hass, link.get(ATTR_NAME), link.get(ATTR_URL), - link.get(ATTR_ICON)) + for link in links.get(CONF_ENTITIES): + Link(hass, link.get(CONF_NAME), link.get(CONF_URL), + link.get(CONF_URL)) return True diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 96f40c8d1f7..6d6e09b1918 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -41,8 +41,8 @@ def setup(hass, config): ('binary_sensor', pywink.get_sensors), ('sensor', lambda: pywink.get_sensors or pywink.get_eggtrays), ('lock', pywink.get_locks), - ('rollershutter', pywink.get_shades), - ('garage_door', pywink.get_garage_doors)): + ('cover', pywink.get_shades), + ('cover', pywink.get_garage_doors)): if func_exists(): discovery.load_platform(hass, component_name, DOMAIN, {}, config) diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 84770390ad9..4b4da350199 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -9,19 +9,26 @@ import pickle from binascii import hexlify, unhexlify from base64 import b64encode, b64decode -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +import voluptuous as vol + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, CONF_DEVICE, CONF_NAME, CONF_PIN) from homeassistant.core import JobPriority from homeassistant.helpers.entity import Entity +from homeassistant.helpers import config_validation as cv -DOMAIN = "zigbee" -REQUIREMENTS = ("xbee-helper==0.0.7",) +REQUIREMENTS = ['xbee-helper==0.0.7'] -EVENT_ZIGBEE_FRAME_RECEIVED = "zigbee_frame_received" +_LOGGER = logging.getLogger(__name__) -CONF_DEVICE = "device" -CONF_BAUD = "baud" +DOMAIN = 'zigbee' -DEFAULT_DEVICE = "/dev/ttyUSB0" +EVENT_ZIGBEE_FRAME_RECEIVED = 'zigbee_frame_received' + +CONF_ADDRESS = 'address' +CONF_BAUD = 'baud' + +DEFAULT_DEVICE = '/dev/ttyUSB0' DEFAULT_BAUD = 9600 DEFAULT_ADC_MAX_VOLTS = 1.2 @@ -35,11 +42,22 @@ CONVERT_ADC = None ZIGBEE_EXCEPTION = None ZIGBEE_TX_FAILURE = None -ATTR_FRAME = "frame" +ATTR_FRAME = 'frame' DEVICE = None -_LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string, + vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_PIN): cv.positive_int, + vol.Optional(CONF_ADDRESS): cv.string, +}, extra=vol.ALLOW_EXTRA) def setup(hass, config): @@ -101,9 +119,9 @@ def close_serial_port(*args): def frame_is_relevant(entity, frame): """Test whether the frame is relevant to the entity.""" - if frame.get("source_addr_long") != entity.config.address: + if frame.get('source_addr_long') != entity.config.address: return False - if "samples" not in frame: + if 'samples' not in frame: return False return True @@ -279,7 +297,7 @@ class ZigBeeDigitalIn(Entity): """ if not frame_is_relevant(self, frame): return - sample = frame["samples"].pop() + sample = frame['samples'].pop() pin_name = DIGITAL_PINS[self._config.pin] if pin_name not in sample: # Doesn't contain information about our pin @@ -402,7 +420,7 @@ class ZigBeeAnalogIn(Entity): """ if not frame_is_relevant(self, frame): return - sample = frame["samples"].pop() + sample = frame['samples'].pop() pin_name = ANALOG_PINS[self._config.pin] if pin_name not in sample: # Doesn't contain information about our pin diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index db57b387c9f..a7841578e2b 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -27,6 +27,9 @@ ATTR_PASSIVE = 'passive' DEFAULT_PASSIVE = False ICON_HOME = 'mdi:home' +ICON_IMPORT = 'mdi:import' + +_LOGGER = logging.getLogger(__name__) def active_zone(hass, latitude, longitude, radius=0): @@ -71,7 +74,6 @@ def in_zone(zone, latitude, longitude, radius=0): def setup(hass, config): """Setup zone.""" entities = set() - for key in extract_domain_configs(config, DOMAIN): entries = config[key] if not isinstance(entries, list): @@ -90,26 +92,48 @@ def setup(hass, config): 'Each zone needs a latitude and longitude.') continue - zone = Zone(hass, name, latitude, longitude, radius, icon, passive) - zone.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, - entities) - zone.update_ha_state() + zone = Zone(hass, name, latitude, longitude, radius, + icon, passive, False) + add_zone(hass, name, zone, entities) entities.add(zone.entity_id) if ENTITY_ID_HOME not in entities: - zone = Zone(hass, hass.config.location_name, hass.config.latitude, - hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False) + zone = Zone(hass, hass.config.location_name, + hass.config.latitude, hass.config.longitude, + DEFAULT_RADIUS, ICON_HOME, False, False) + add_zone(hass, hass.config.location_name, zone, entities) zone.entity_id = ENTITY_ID_HOME zone.update_ha_state() return True +# Add a zone to the existing set +def add_zone(hass, name, zone, entities=None): + """Add a zone from other components.""" + _LOGGER.info("Adding new zone %s", name) + if entities is None: + _entities = set() + else: + _entities = entities + zone.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, + _entities) + zone_exists = hass.states.get(zone.entity_id) + if zone_exists is None: + zone.update_ha_state() + _entities.add(zone.entity_id) + return zone + else: + _LOGGER.info("Zone already exists") + return zone_exists + + class Zone(Entity): """Representation of a Zone.""" # pylint: disable=too-many-arguments, too-many-instance-attributes - def __init__(self, hass, name, latitude, longitude, radius, icon, passive): + def __init__(self, hass, name, latitude, longitude, radius, icon, passive, + imported): """Initialize the zone.""" self.hass = hass self._name = name @@ -118,6 +142,7 @@ class Zone(Entity): self._radius = radius self._icon = icon self._passive = passive + self._imported = imported @property def name(self): diff --git a/homeassistant/const.py b/homeassistant/const.py index f6f0cd7855d..eb8b65df998 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,9 +1,43 @@ # coding: utf-8 """Constants used by Home Assistant components.""" - -__version__ = '0.27.2' +MAJOR_VERSION = 0 +MINOR_VERSION = 28 +PATCH_VERSION = '0' +__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) +__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4) +PROJECT_NAME = 'Home Assistant' +PROJECT_PACKAGE_NAME = 'homeassistant' +PROJECT_LICENSE = 'MIT License' +PROJECT_AUTHOR = 'The Home Assistant Authors' +PROJECT_COPYRIGHT = ' 2016, {}'.format(PROJECT_AUTHOR) +PROJECT_URL = 'https://home-assistant.io/' +PROJECT_EMAIL = 'hello@home-assistant.io' +PROJECT_DESCRIPTION = ('Open-source home automation platform ' + 'running on Python 3.') +PROJECT_LONG_DESCRIPTION = ('Home Assistant is an open-source ' + 'home automation platform running on Python 3. ' + 'Track and control all devices at home and ' + 'automate control. ' + 'Installation in less than a minute.') +PROJECT_CLASSIFIERS = [ + 'Intended Audience :: End Users/Desktop', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.4', + 'Topic :: Home Automation' +] + +PROJECT_GITHUB_USERNAME = 'home-assistant' +PROJECT_GITHUB_REPOSITORY = 'home-assistant' + +PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME) +GITHUB_PATH = '{}/{}'.format(PROJECT_GITHUB_USERNAME, + PROJECT_GITHUB_REPOSITORY) +GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) + PLATFORM_FORMAT = '{}.{}' # Can be used to specify a catch all when registering state or event listeners. @@ -27,24 +61,38 @@ CONF_AUTHENTICATION = 'authentication' CONF_BEFORE = 'before' CONF_BELOW = 'below' CONF_BLACKLIST = 'blacklist' +CONF_BRIGHTNESS = 'brightness' CONF_CODE = 'code' +CONF_COMMAND = 'command' +CONF_COMMAND_CLOSE = 'command_close' +CONF_COMMAND_OFF = 'command_off' +CONF_COMMAND_ON = 'command_on' +CONF_COMMAND_OPEN = 'command_open' +CONF_COMMAND_STATE = 'command_state' +CONF_COMMAND_STOP = 'command_stop' CONF_CONDITION = 'condition' +CONF_COVERS = 'covers' CONF_CUSTOMIZE = 'customize' CONF_DEVICE = 'device' +CONF_DEVICES = 'devices' CONF_DISARM_AFTER_TRIGGER = 'disarm_after_trigger' +CONF_DISCOVERY = 'discovery' CONF_DISPLAY_OPTIONS = 'display_options' CONF_ELEVATION = 'elevation' +CONF_EMAIL = 'email' CONF_ENTITY_ID = 'entity_id' CONF_ENTITY_NAMESPACE = 'entity_namespace' CONF_EVENT = 'event' CONF_FILE_PATH = 'file_path' CONF_FILENAME = 'filename' +CONF_FRIENDLY_NAME = 'friendly_name' CONF_HOST = 'host' CONF_HOSTS = 'hosts' CONF_ICON = 'icon' CONF_ID = 'id' CONF_LATITUDE = 'latitude' CONF_LONGITUDE = 'longitude' +CONF_MAC = 'mac' CONF_METHOD = 'method' CONF_MONITORED_CONDITIONS = 'monitored_conditions' CONF_MONITORED_VARIABLES = 'monitored_variables' @@ -53,19 +101,28 @@ CONF_OFFSET = 'offset' CONF_OPTIMISTIC = 'optimistic' CONF_PASSWORD = 'password' CONF_PAYLOAD = 'payload' +CONF_PAYLOAD_OFF = 'payload_off' +CONF_PAYLOAD_ON = 'payload_on' CONF_PENDING_TIME = 'pending_time' +CONF_PIN = 'pin' CONF_PLATFORM = 'platform' CONF_PORT = 'port' CONF_PREFIX = 'prefix' +CONF_RECIPIENT = 'recipient' CONF_RESOURCE = 'resource' CONF_RESOURCES = 'resources' +CONF_RGB = 'rgb' CONF_SCAN_INTERVAL = 'scan_interval' +CONF_SENDER = 'sender' CONF_SENSOR_CLASS = 'sensor_class' +CONF_SENSORS = 'sensors' CONF_SSL = 'ssl' CONF_STATE = 'state' CONF_STRUCTURE = 'structure' +CONF_SWITCHES = 'switches' CONF_TEMPERATURE_UNIT = 'temperature_unit' CONF_TIME_ZONE = 'time_zone' +CONF_TIMEOUT = 'timeout' CONF_TOKEN = 'token' CONF_TRIGGER_TIME = 'trigger_time' CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' diff --git a/homeassistant/core.py b/homeassistant/core.py index b77d8356a35..03f9658325f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -297,13 +297,19 @@ class EventBus(object): else: self._listeners[event_type] = [listener] + def remove_listener(): + """Remove the listener.""" + self._remove_listener(event_type, listener) + + return remove_listener + def listen_once(self, event_type, listener): """Listen once for event of a specific type. To listen to all events specify the constant ``MATCH_ALL`` as event_type. - Returns registered listener that can be used with remove_listener. + Returns function to unsubscribe the listener. """ @ft.wraps(listener) def onetime_listener(event): @@ -317,15 +323,21 @@ class EventBus(object): # This will make sure the second time it does nothing. setattr(onetime_listener, 'run', True) - self.remove_listener(event_type, onetime_listener) + remove_listener() listener(event) - self.listen(event_type, onetime_listener) + remove_listener = self.listen(event_type, onetime_listener) - return onetime_listener + return remove_listener def remove_listener(self, event_type, listener): + """Remove a listener of a specific event_type. (DEPRECATED 0.28).""" + _LOGGER.warning('bus.remove_listener has been deprecated. Please use ' + 'the function returned from calling listen.') + self._remove_listener(event_type, listener) + + def _remove_listener(self, event_type, listener): """Remove a listener of a specific event_type.""" with self._lock: try: @@ -338,7 +350,8 @@ class EventBus(object): except (KeyError, ValueError): # KeyError is key event_type listener did not exist # ValueError if listener did not exist within event_type - pass + _LOGGER.warning('Unable to remove unknown listener %s', + listener) class State(object): @@ -568,6 +581,7 @@ class Service(object): try: if self.schema: call.data = self.schema(call.data) + call.data = MappingProxyType(call.data) self.func(call) except vol.MultipleInvalid as ex: @@ -682,14 +696,13 @@ class ServiceRegistry(object): if call.data[ATTR_SERVICE_CALL_ID] == call_id: executed_event.set() - self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed) + unsub = self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed) self._bus.fire(EVENT_CALL_SERVICE, event_data) if blocking: success = executed_event.wait(SERVICE_CALL_LIMIT) - self._bus.remove_listener( - EVENT_SERVICE_EXECUTED, service_executed) + unsub() return success def _event_to_service_call(self, event): diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index d9c761832dc..1be157c789d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,5 +1,6 @@ """Helpers for config validation using voluptuous.""" from datetime import timedelta +import os from urllib.parse import urlparse from typing import Any, Union, TypeVar, Callable, Sequence, List, Dict @@ -65,9 +66,17 @@ def boolean(value: Any) -> bool: return bool(value) -def isfile(value): +def isfile(value: Any) -> str: """Validate that the value is an existing file.""" - return vol.IsFile('not a file')(value) + if value is None: + raise vol.Invalid('None is not file') + file_in = os.path.expanduser(str(value)) + + if not os.path.isfile(file_in): + raise vol.Invalid('not a file') + if not os.access(file_in, os.R_OK): + raise vol.Invalid('file not readable') + return file_in def ensure_list(value: Union[T, Sequence[T]]) -> List[T]: @@ -235,6 +244,20 @@ def template(value): raise vol.Invalid('invalid template ({})'.format(ex)) +def template_complex(value): + """Validate a complex jinja2 template.""" + if isinstance(value, list): + for idx, element in enumerate(value): + value[idx] = template_complex(element) + return value + if isinstance(value, dict): + for key, element in value.items(): + value[key] = template_complex(element) + return value + + return template(value) + + def time(value): """Validate time.""" time_val = dt_util.parse_time(value) @@ -301,7 +324,7 @@ SERVICE_SCHEMA = vol.All(vol.Schema({ vol.Exclusive('service', 'service name'): service, vol.Exclusive('service_template', 'service name'): template, vol.Optional('data'): dict, - vol.Optional('data_template'): {match_all: template}, + vol.Optional('data_template'): {match_all: template_complex}, vol.Optional(CONF_ENTITY_ID): entity_ids, }), has_at_least_one_key('service', 'service_template')) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 61cda43d431..0b4768b809d 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -195,6 +195,10 @@ class Entity(object): return self.hass.states.set( self.entity_id, state, attr, self.force_update) + def remove(self) -> None: + """Remove entitiy from HASS.""" + self.hass.states.remove(self.entity_id) + def _attr_setter(self, name, typ, attr, attrs): """Helper method to populate attributes based on properties.""" if attr in attrs: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 898a445c788..3146d703d19 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -1,11 +1,14 @@ """Helpers for components that manage entities.""" from threading import Lock -from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.components import group +from homeassistant import config as conf_util +from homeassistant.bootstrap import (prepare_setup_platform, + prepare_setup_component) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, DEVICE_DEFAULT_NAME) +from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import get_component from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_utc_time_change @@ -32,13 +35,14 @@ class EntityComponent(object): self.entities = {} self.group = None - self.is_polling = False self.config = None self.lock = Lock() - self.add_entities = EntityPlatform(self, self.scan_interval, - None).add_entities + self._platforms = { + 'core': EntityPlatform(self, self.scan_interval, None), + } + self.add_entities = self._platforms['core'].add_entities def setup(self, config): """Set up a full entity component. @@ -85,17 +89,22 @@ class EntityComponent(object): return # Config > Platform > Component - scan_interval = platform_config.get( - CONF_SCAN_INTERVAL, - getattr(platform, 'SCAN_INTERVAL', self.scan_interval)) + scan_interval = (platform_config.get(CONF_SCAN_INTERVAL) or + getattr(platform, 'SCAN_INTERVAL', None) or + self.scan_interval) entity_namespace = platform_config.get(CONF_ENTITY_NAMESPACE) + key = (platform_type, scan_interval, entity_namespace) + + if key not in self._platforms: + self._platforms[key] = EntityPlatform(self, scan_interval, + entity_namespace) + entity_platform = self._platforms[key] + try: - platform.setup_platform( - self.hass, platform_config, - EntityPlatform(self, scan_interval, - entity_namespace).add_entities, - discovery_info) + platform.setup_platform(self.hass, platform_config, + entity_platform.add_entities, + discovery_info) self.hass.config.components.append( '{}.{}'.format(self.domain, platform_type)) @@ -129,12 +138,46 @@ class EntityComponent(object): def update_group(self): """Set up and/or update component group.""" if self.group is None and self.group_name is not None: + group = get_component('group') self.group = group.Group(self.hass, self.group_name, user_defined=False) if self.group is not None: self.group.update_tracked_entity_ids(self.entities.keys()) + def reset(self): + """Remove entities and reset the entity component to initial values.""" + with self.lock: + for platform in self._platforms.values(): + platform.reset() + + self._platforms = { + 'core': self._platforms['core'] + } + self.entities = {} + self.config = None + + if self.group is not None: + self.group.stop() + self.group = None + + def prepare_reload(self): + """Prepare reloading this entity component.""" + try: + path = conf_util.find_config_file(self.hass.config.config_dir) + conf = conf_util.load_yaml_config_file(path) + except HomeAssistantError as err: + self.logger.error(err) + return None + + conf = prepare_setup_component(self.hass, conf, self.domain) + + if conf is None: + return None + + self.reset() + return conf + class EntityPlatform(object): """Keep track of entities for a single platform.""" @@ -146,7 +189,7 @@ class EntityPlatform(object): self.scan_interval = scan_interval self.entity_namespace = entity_namespace self.platform_entities = [] - self.is_polling = False + self._unsub_polling = None def add_entities(self, new_entities): """Add entities for a single platform.""" @@ -157,17 +200,23 @@ class EntityPlatform(object): self.component.update_group() - if self.is_polling or \ + if self._unsub_polling is not None or \ not any(entity.should_poll for entity in self.platform_entities): return - self.is_polling = True - - track_utc_time_change( + self._unsub_polling = track_utc_time_change( self.component.hass, self._update_entity_states, second=range(0, 60, self.scan_interval)) + def reset(self): + """Remove all entities and reset data.""" + for entity in self.platform_entities: + entity.remove() + if self._unsub_polling is not None: + self._unsub_polling() + self._unsub_polling = None + def _update_entity_states(self, now): """Update the states of all the polling entities.""" with self.component.lock: diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 9bc6910c685..512b173a249 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -14,8 +14,7 @@ def track_state_change(hass, entity_ids, action, from_state=None, entity_ids, from_state and to_state can be string or list. Use list to match multiple. - Returns the listener that listens on the bus for EVENT_STATE_CHANGED. - Pass the return value into hass.bus.remove_listener to remove it. + Returns a function that can be called to remove the listener. """ from_state = _process_state_match(from_state) to_state = _process_state_match(to_state) @@ -50,9 +49,7 @@ def track_state_change(hass, entity_ids, action, from_state=None, event.data.get('old_state'), event.data.get('new_state')) - hass.bus.listen(EVENT_STATE_CHANGED, state_change_listener) - - return state_change_listener + return hass.bus.listen(EVENT_STATE_CHANGED, state_change_listener) def track_point_in_time(hass, action, point_in_time): @@ -77,23 +74,20 @@ def track_point_in_utc_time(hass, action, point_in_time): """Listen for matching time_changed events.""" now = event.data[ATTR_NOW] - if now >= point_in_time and \ - not hasattr(point_in_time_listener, 'run'): + if now < point_in_time or hasattr(point_in_time_listener, 'run'): + return - # Set variable so that we will never run twice. - # Because the event bus might have to wait till a thread comes - # available to execute this listener it might occur that the - # listener gets lined up twice to be executed. This will make - # sure the second time it does nothing. - point_in_time_listener.run = True + # Set variable so that we will never run twice. + # Because the event bus might have to wait till a thread comes + # available to execute this listener it might occur that the + # listener gets lined up twice to be executed. This will make + # sure the second time it does nothing. + point_in_time_listener.run = True + remove() + action(now) - hass.bus.remove_listener(EVENT_TIME_CHANGED, - point_in_time_listener) - - action(now) - - hass.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener) - return point_in_time_listener + remove = hass.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener) + return remove def track_sunrise(hass, action, offset=None): @@ -112,10 +106,19 @@ def track_sunrise(hass, action, offset=None): def sunrise_automation_listener(now): """Called when it's time for action.""" - track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + nonlocal remove + remove = track_point_in_utc_time(hass, sunrise_automation_listener, + next_rise()) action() - track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + remove = track_point_in_utc_time(hass, sunrise_automation_listener, + next_rise()) + + def remove_listener(): + """Remove sunrise listener.""" + remove() + + return remove_listener def track_sunset(hass, action, offset=None): @@ -134,10 +137,19 @@ def track_sunset(hass, action, offset=None): def sunset_automation_listener(now): """Called when it's time for action.""" - track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + nonlocal remove + remove = track_point_in_utc_time(hass, sunset_automation_listener, + next_set()) action() - track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + remove = track_point_in_utc_time(hass, sunset_automation_listener, + next_set()) + + def remove_listener(): + """Remove sunset listener.""" + remove() + + return remove_listener # pylint: disable=too-many-arguments @@ -152,8 +164,7 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None, """Fire every time event that comes in.""" action(event.data[ATTR_NOW]) - hass.bus.listen(EVENT_TIME_CHANGED, time_change_listener) - return time_change_listener + return hass.bus.listen(EVENT_TIME_CHANGED, time_change_listener) pmp = _process_time_match year, month, day = pmp(year), pmp(month), pmp(day) @@ -178,8 +189,7 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None, action(now) - hass.bus.listen(EVENT_TIME_CHANGED, pattern_time_change_listener) - return pattern_time_change_listener + return hass.bus.listen(EVENT_TIME_CHANGED, pattern_time_change_listener) # pylint: disable=too-many-arguments diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 008fdb9374d..73ef08ce1ff 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -7,7 +7,7 @@ from typing import Optional, Sequence import voluptuous as vol from homeassistant.core import HomeAssistant -from homeassistant.const import EVENT_TIME_CHANGED, CONF_CONDITION +from homeassistant.const import CONF_CONDITION from homeassistant.helpers import ( service, condition, template, config_validation as cv) from homeassistant.helpers.event import track_point_in_utc_time @@ -47,7 +47,7 @@ class Script(): self.can_cancel = any(CONF_DELAY in action for action in self.sequence) self._lock = threading.Lock() - self._delay_listener = None + self._unsub_delay_listener = None @property def is_running(self) -> bool: @@ -72,7 +72,7 @@ class Script(): # Call ourselves in the future to continue work def script_delay(now): """Called after delay is done.""" - self._delay_listener = None + self._unsub_delay_listener = None self.run(variables) delay = action[CONF_DELAY] @@ -83,7 +83,7 @@ class Script(): cv.positive_timedelta)( template.render(self.hass, delay)) - self._delay_listener = track_point_in_utc_time( + self._unsub_delay_listener = track_point_in_utc_time( self.hass, script_delay, date_util.utcnow() + delay) self._cur = cur + 1 @@ -139,10 +139,9 @@ class Script(): def _remove_listener(self): """Remove point in time listener, if any.""" - if self._delay_listener: - self.hass.bus.remove_listener(EVENT_TIME_CHANGED, - self._delay_listener) - self._delay_listener = None + if self._unsub_delay_listener: + self._unsub_delay_listener() + self._unsub_delay_listener = None def _log(self, msg): """Logger helper.""" diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index b594889fd77..21cfb0aab54 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -63,9 +63,21 @@ def call_from_config(hass, config, blocking=False, variables=None, domain, service_name = domain_service.split('.', 1) service_data = dict(config.get(CONF_SERVICE_DATA, {})) + def _data_template_creator(value): + """Recursive template creator helper function.""" + if isinstance(value, list): + for idx, element in enumerate(value): + value[idx] = _data_template_creator(element) + return value + if isinstance(value, dict): + for key, element in value.items(): + value[key] = _data_template_creator(element) + return value + return template.render(hass, value, variables) + if CONF_SERVICE_DATA_TEMPLATE in config: for key, value in config[CONF_SERVICE_DATA_TEMPLATE].items(): - service_data[key] = template.render(hass, value, variables) + service_data[key] = _data_template_creator(value) if CONF_SERVICE_ENTITY_ID in config: service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index fab081cc5c5..e083534f828 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -6,11 +6,11 @@ import logging import jinja2 from jinja2.sandbox import ImmutableSandboxedEnvironment -from homeassistant.components import group from homeassistant.const import STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import State from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper +from homeassistant.loader import get_component from homeassistant.util import convert, dt as dt_util, location as loc_util _LOGGER = logging.getLogger(__name__) @@ -169,6 +169,8 @@ class LocationMethods(object): else: gr_entity_id = str(entities) + group = get_component('group') + states = [self._hass.states.get(entity_id) for entity_id in group.expand_entity_ids(self._hass, [gr_entity_id])] @@ -250,6 +252,20 @@ def multiply(value, amount): return value +def timestamp_custom(value, date_format=DATE_STR_FORMAT, local=True): + """Filter to convert given timestamp to format.""" + try: + date = dt_util.utc_from_timestamp(value) + + if local: + date = dt_util.as_local(date) + + return date.strftime(date_format) + except (ValueError, TypeError): + # If timestamp can't be converted + return value + + def timestamp_local(value): """Filter to convert given timestamp to local date/time.""" try: @@ -261,7 +277,7 @@ def timestamp_local(value): def timestamp_utc(value): - """Filter to convert gibrn timestamp to UTC date/time.""" + """Filter to convert given timestamp to UTC date/time.""" try: return dt_util.utc_from_timestamp(value).strftime(DATE_STR_FORMAT) except (ValueError, TypeError): @@ -287,5 +303,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): ENV = TemplateEnvironment() ENV.filters['round'] = forgiving_round ENV.filters['multiply'] = multiply +ENV.filters['timestamp_custom'] = timestamp_custom ENV.filters['timestamp_local'] = timestamp_local ENV.filters['timestamp_utc'] = timestamp_utc diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 8e62cdd044a..4564878a5ad 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -211,6 +211,7 @@ class EventForwarder(object): self._targets = {} self._lock = threading.Lock() + self._unsub_listener = None def connect(self, api): """Attach to a Home Assistant instance and forward events. @@ -218,9 +219,9 @@ class EventForwarder(object): Will overwrite old target if one exists with same host/port. """ with self._lock: - if len(self._targets) == 0: - # First target we get, setup listener for events - self.hass.bus.listen(ha.MATCH_ALL, self._event_listener) + if self._unsub_listener is None: + self._unsub_listener = self.hass.bus.listen( + ha.MATCH_ALL, self._event_listener) key = (api.host, api.port) @@ -235,8 +236,8 @@ class EventForwarder(object): if len(self._targets) == 0: # Remove event listener if no forwarding targets present - self.hass.bus.remove_listener(ha.MATCH_ALL, - self._event_listener) + self._unsub_listener() + self._unsub_listener = None return did_remove diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 624452b0592..d1bf12187e8 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -221,14 +221,18 @@ def check(config_path): try: bootstrap.from_config_file(config_path, skip_pip=True) - res['secret_cache'] = yaml.__SECRET_CACHE - return res + res['secret_cache'] = dict(yaml.__SECRET_CACHE) + except Exception as err: # pylint: disable=broad-except + print(color('red', 'Fatal error while loading config:'), str(err)) finally: # Stop all patches for pat in PATCHES.values(): pat.stop() # Ensure !secrets point to the original function yaml.yaml.SafeLoader.add_constructor('!secret', yaml._secret_yaml) + bootstrap.clear_secret_cache() + + return res def dump_dict(layer, indent_count=1, listi=False, **kwargs): diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index b834ac8048c..035a96b657e 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -121,6 +121,16 @@ def _ordered_dict(loader: SafeLineLoader, line = getattr(node, '__line__', 'unknown') if line != 'unknown' and (min_line is None or line < min_line): min_line = line + + try: + hash(key) + except TypeError: + fname = getattr(loader.stream, 'name', '') + raise yaml.MarkedYAMLError( + context="invalid key: \"{}\"".format(key), + context_mark=yaml.Mark(fname, 0, min_line, -1, None, None) + ) + if key in seen: fname = getattr(loader.stream, 'name', '') first_mark = yaml.Mark(fname, 0, seen[key], -1, None, None) diff --git a/requirements_all.txt b/requirements_all.txt index 56d445daf54..c0ef98ffdc1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -26,7 +26,7 @@ SoCo==0.11.1 TwitterAPI==2.4.2 # homeassistant.components.http -Werkzeug==0.11.10 +Werkzeug==0.11.11 # homeassistant.components.apcupsd apcaccess==0.0.4 @@ -34,6 +34,9 @@ apcaccess==0.0.4 # homeassistant.components.sun astral==1.2 +# homeassistant.components.sensor.linux_battery +batinfo==0.3 + # homeassistant.components.light.blinksticklight blinkstick==1.1.8 @@ -52,6 +55,9 @@ boto3==1.3.1 # homeassistant.components.http cherrypy==7.1.0 +# homeassistant.components.sensor.coinmarketcap +coinmarketcap==2.0.1 + # homeassistant.scripts.check_config colorlog>2.1,<3 @@ -75,11 +81,14 @@ enocean==0.31 # homeassistant.components.thermostat.honeywell evohomeclient==0.2.5 +# homeassistant.components.sensor.fastdotcom +fastdotcom==0.0.1 + # homeassistant.components.feedreader feedparser==5.2.1 # homeassistant.components.sensor.fitbit -fitbit==0.2.2 +fitbit==0.2.3 # homeassistant.components.sensor.fixer fixerio==0.1.1 @@ -100,11 +109,11 @@ gntp==1.0.3 googlemaps==2.4.4 # homeassistant.components.sensor.gpsd -gps3==0.33.2 +gps3==0.33.3 # homeassistant.components.binary_sensor.ffmpeg # homeassistant.components.camera.ffmpeg -ha-ffmpeg==0.9 +ha-ffmpeg==0.10 # homeassistant.components.mqtt.server hbmqtt==0.7.1 @@ -123,7 +132,7 @@ hikvision==0.4 https://github.com/Danielhiversen/flux_led/archive/0.6.zip#flux_led==0.6 # homeassistant.components.switch.dlink -https://github.com/LinuxChristian/pyW215/archive/v0.1.1.zip#pyW215==0.1.1 +https://github.com/LinuxChristian/pyW215/archive/v0.3.4.zip#pyW215==0.3.4 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv @@ -137,7 +146,7 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 # homeassistant.components.media_player.braviatv -https://github.com/aparraga/braviarc/archive/0.3.3.zip#braviarc==0.3.3 +https://github.com/aparraga/braviarc/archive/0.3.5.zip#braviarc==0.3.5 # homeassistant.components.media_player.roku https://github.com/bah2830/python-roku/archive/3.1.2.zip#roku==3.1.2 @@ -155,7 +164,7 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9. https://github.com/gadgetreactor/pyHS100/archive/master.zip#pyHS100==0.1.2 # homeassistant.components.netatmo -https://github.com/jabesq/netatmo-api-python/archive/v0.5.0.zip#lnetatmo==0.5.0 +https://github.com/jabesq/netatmo-api-python/archive/master.zip#lnetatmo==0.5.0 # homeassistant.components.sensor.sabnzbd https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1 @@ -166,9 +175,6 @@ https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip#pyqwikswitch==0.4 # homeassistant.components.media_player.russound_rnet https://github.com/laf/russound/archive/0.1.6.zip#russound==0.1.6 -# homeassistant.components.sensor.fastdotcom -https://github.com/nkgilley/fast.com/archive/master.zip#fastdotcom==0.0.1 - # homeassistant.components.ecobee https://github.com/nkgilley/python-ecobee-api/archive/4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6 @@ -179,9 +185,6 @@ https://github.com/nkgilley/python-join-api/archive/3e1e849f1af0b4080f551b62270c # homeassistant.components.switch.edimax https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1 -# homeassistant.components.sensor.temper -https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc704537dd2d09b.zip#temperusb==1.2.3 - # homeassistant.components.sensor.gtfs https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3 @@ -270,7 +273,7 @@ pmsensor==0.3 proliphix==0.3.1 # homeassistant.components.sensor.systemmonitor -psutil==4.3.0 +psutil==4.3.1 # homeassistant.components.wink # homeassistant.components.binary_sensor.wink @@ -343,10 +346,10 @@ pynetio==0.1.6 pynx584==0.2 # homeassistant.components.sensor.openweathermap -pyowm==2.3.2 +pyowm==2.4.0 # homeassistant.components.switch.acer_projector -pyserial<=3.1 +pyserial==3.1.1 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp @@ -394,7 +397,7 @@ python-twitch==1.3.0 python-wink==0.7.13 # homeassistant.components.keyboard -pyuserinput==0.1.9 +pyuserinput==0.1.11 # homeassistant.components.vera pyvera==0.2.15 @@ -422,16 +425,16 @@ schiene==0.17 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==3.2.10 +sendgrid==3.4.0 # homeassistant.components.notify.slack -slacker==0.9.24 +slacker==0.9.25 # homeassistant.components.notify.xmpp sleekxmpp==1.3.1 # homeassistant.components.media_player.snapcast -snapcast==1.2.1 +snapcast==1.2.2 # homeassistant.components.climate.honeywell # homeassistant.components.thermostat.honeywell @@ -460,6 +463,9 @@ tellcore-py==1.1.2 # homeassistant.components.tellduslive tellive-py==0.5.2 +# homeassistant.components.sensor.temper +temperusb==1.5.1 + # homeassistant.components.sensor.transmission # homeassistant.components.switch.transmission transmissionrpc==0.11 @@ -468,7 +474,7 @@ transmissionrpc==0.11 twilio==5.4.0 # homeassistant.components.sensor.uber -uber_rides==0.2.4 +uber_rides==0.2.5 # homeassistant.components.device_tracker.unifi unifi==1.2.5 @@ -491,6 +497,9 @@ websocket-client==0.37.0 # homeassistant.components.zigbee xbee-helper==0.0.7 +# homeassistant.components.sensor.xbox_live +xboxapi==0.1.1 + # homeassistant.components.sensor.swiss_hydrological_data # homeassistant.components.sensor.yr xmltodict==0.10.2 diff --git a/requirements_docs.txt b/requirements_docs.txt new file mode 100644 index 00000000000..df88ba8fb58 --- /dev/null +++ b/requirements_docs.txt @@ -0,0 +1,3 @@ +Sphinx==1.4.6 +sphinx-autodoc-typehints==1.1.0 +sphinx-autodoc-annotation==1.0.post1 diff --git a/setup.cfg b/setup.cfg index 98a4f54d55d..6d952083a31 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,6 @@ norecursedirs = .git testing_config [flake8] exclude = .venv,.git,.tox,docs,www_static,venv,bin,lib,deps,build + +[pydocstyle] +match_dir = ^((?!\.|www_static).)*$ diff --git a/setup.py b/setup.py index caa5b177b5c..67366bd7d83 100755 --- a/setup.py +++ b/setup.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 import os from setuptools import setup, find_packages -from homeassistant.const import __version__ +from homeassistant.const import (__version__, PROJECT_PACKAGE_NAME, + PROJECT_LICENSE, PROJECT_URL, + PROJECT_EMAIL, PROJECT_DESCRIPTION, + PROJECT_CLASSIFIERS, GITHUB_URL, + PROJECT_AUTHOR) -PACKAGE_NAME = 'homeassistant' HERE = os.path.abspath(os.path.dirname(__file__)) -DOWNLOAD_URL = ('https://github.com/home-assistant/home-assistant/archive/' - '{}.zip'.format(__version__)) +DOWNLOAD_URL = ('{}/archive/' + '{}.zip'.format(GITHUB_URL, __version__)) PACKAGES = find_packages(exclude=['tests', 'tests.*']) @@ -21,14 +24,14 @@ REQUIRES = [ ] setup( - name=PACKAGE_NAME, + name=PROJECT_PACKAGE_NAME, version=__version__, - license='MIT License', - url='https://home-assistant.io/', + license=PROJECT_LICENSE, + url=PROJECT_URL, download_url=DOWNLOAD_URL, - author='Home Assistant', - author_email='hello@home-assistant.io', - description='Open-source home automation platform running on Python 3.', + author=PROJECT_AUTHOR, + author_email=PROJECT_EMAIL, + description=PROJECT_DESCRIPTION, packages=PACKAGES, include_package_data=True, zip_safe=False, @@ -41,12 +44,5 @@ setup( 'hass = homeassistant.__main__:main' ] }, - classifiers=[ - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.4', - 'Topic :: Home Automation' - ], + classifiers=PROJECT_CLASSIFIERS, ) diff --git a/tests/common.py b/tests/common.py index e51e4ba048a..3c6815ece02 100644 --- a/tests/common.py +++ b/tests/common.py @@ -44,6 +44,7 @@ def get_test_home_assistant(num_threads=None): hass.config.elevation = 0 hass.config.time_zone = date_util.get_time_zone('US/Pacific') hass.config.units = METRIC_SYSTEM + hass.config.skip_pip = True if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: loader.prepare(hass) @@ -246,20 +247,23 @@ def patch_yaml_files(files_dict, endswith=True): """Patch load_yaml with a dictionary of yaml files.""" # match using endswith, start search with longest string matchlist = sorted(list(files_dict.keys()), key=len) if endswith else [] - # matchlist.sort(key=len) def mock_open_f(fname, **_): """Mock open() in the yaml module, used by load_yaml.""" # Return the mocked file on full match if fname in files_dict: _LOGGER.debug('patch_yaml_files match %s', fname) - return StringIO(files_dict[fname]) + res = StringIO(files_dict[fname]) + setattr(res, 'name', fname) + return res # Match using endswith for ends in matchlist: if fname.endswith(ends): _LOGGER.debug('patch_yaml_files end match %s: %s', ends, fname) - return StringIO(files_dict[ends]) + res = StringIO(files_dict[ends]) + setattr(res, 'name', fname) + return res # Fallback for hass.components (i.e. services.yaml) if 'homeassistant/components' in fname: @@ -267,6 +271,6 @@ def patch_yaml_files(files_dict, endswith=True): return open(fname, encoding='utf-8') # Not found - raise IOError('File not found: {}'.format(fname)) + raise FileNotFoundError('File not found: {}'.format(fname)) return patch.object(yaml, 'open', mock_open_f, create=True) diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index ef5d380075b..80b1f507651 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -44,6 +44,13 @@ class TestAutomationEvent(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + automation.turn_off(self.hass) + self.hass.pool.block_till_done() + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fires_on_event_with_data(self): """Test the firing of events with data.""" assert _setup_component(self.hass, automation.DOMAIN, { diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index e90ffe8d765..3d69cca2d32 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,9 +1,12 @@ """The tests for the automation component.""" import unittest +from unittest.mock import patch from homeassistant.bootstrap import _setup_component import homeassistant.components.automation as automation from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import HomeAssistantError +import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant @@ -45,6 +48,7 @@ class TestAutomation(unittest.TestCase): """Test service data.""" assert _setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { + 'alias': 'hello', 'trigger': { 'platform': 'event', 'event_type': 'test_event', @@ -59,10 +63,17 @@ class TestAutomation(unittest.TestCase): } }) - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - self.assertEqual('event - test_event', self.calls[0].data['some']) + time = dt_util.utcnow() + + with patch('homeassistant.components.automation.utcnow', + return_value=time): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + assert len(self.calls) == 1 + assert 'event - test_event' == self.calls[0].data['some'] + state = self.hass.states.get('automation.hello') + assert state is not None + assert state.attributes.get('last_triggered') == time def test_service_specify_entity_id(self): """Test service data.""" @@ -347,3 +358,195 @@ class TestAutomation(unittest.TestCase): assert len(self.calls) == 2 assert self.calls[0].data['position'] == 0 assert self.calls[1].data['position'] == 1 + + def test_services(self): + """Test the automation services for turning entities on/off.""" + entity_id = 'automation.hello' + + assert self.hass.states.get(entity_id) is None + assert not automation.is_on(self.hass, entity_id) + + assert _setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'alias': 'hello', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'service': 'test.automation', + } + } + }) + + assert self.hass.states.get(entity_id) is not None + assert automation.is_on(self.hass, entity_id) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + assert len(self.calls) == 1 + + automation.turn_off(self.hass, entity_id) + self.hass.pool.block_till_done() + + assert not automation.is_on(self.hass, entity_id) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + assert len(self.calls) == 1 + + automation.toggle(self.hass, entity_id) + self.hass.pool.block_till_done() + + assert automation.is_on(self.hass, entity_id) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + assert len(self.calls) == 2 + + automation.trigger(self.hass, entity_id) + self.hass.pool.block_till_done() + assert len(self.calls) == 3 + + automation.turn_off(self.hass, entity_id) + self.hass.pool.block_till_done() + automation.trigger(self.hass, entity_id) + self.hass.pool.block_till_done() + assert len(self.calls) == 4 + + automation.turn_on(self.hass, entity_id) + self.hass.pool.block_till_done() + assert automation.is_on(self.hass, entity_id) + + @patch('homeassistant.config.load_yaml_config_file', return_value={ + automation.DOMAIN: { + 'alias': 'bye', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event2', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'event': '{{ trigger.event.event_type }}' + } + } + } + }) + def test_reload_config_service(self, mock_load_yaml): + """Test the reload config service.""" + assert _setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'alias': 'hello', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'event': '{{ trigger.event.event_type }}' + } + } + } + }) + assert self.hass.states.get('automation.hello') is not None + assert self.hass.states.get('automation.bye') is None + listeners = self.hass.bus.listeners + assert listeners.get('test_event') == 1 + assert listeners.get('test_event2') is None + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + assert len(self.calls) == 1 + assert self.calls[0].data.get('event') == 'test_event' + + automation.reload(self.hass) + self.hass.pool.block_till_done() + + assert self.hass.states.get('automation.hello') is None + assert self.hass.states.get('automation.bye') is not None + listeners = self.hass.bus.listeners + assert listeners.get('test_event') is None + assert listeners.get('test_event2') == 1 + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + assert len(self.calls) == 1 + + self.hass.bus.fire('test_event2') + self.hass.pool.block_till_done() + assert len(self.calls) == 2 + assert self.calls[1].data.get('event') == 'test_event2' + + @patch('homeassistant.config.load_yaml_config_file', return_value={ + automation.DOMAIN: 'not valid', + }) + def test_reload_config_when_invalid_config(self, mock_load_yaml): + """Test the reload config service handling invalid config.""" + assert _setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'alias': 'hello', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'event': '{{ trigger.event.event_type }}' + } + } + } + }) + assert self.hass.states.get('automation.hello') is not None + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + assert len(self.calls) == 1 + assert self.calls[0].data.get('event') == 'test_event' + + automation.reload(self.hass) + self.hass.pool.block_till_done() + + assert self.hass.states.get('automation.hello') is not None + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + assert len(self.calls) == 2 + + def test_reload_config_handles_load_fails(self): + """Test the reload config service.""" + assert _setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'alias': 'hello', + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'action': { + 'service': 'test.automation', + 'data_template': { + 'event': '{{ trigger.event.event_type }}' + } + } + } + }) + assert self.hass.states.get('automation.hello') is not None + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + assert len(self.calls) == 1 + assert self.calls[0].data.get('event') == 'test_event' + + with patch('homeassistant.config.load_yaml_config_file', + side_effect=HomeAssistantError('bla')): + automation.reload(self.hass) + self.hass.pool.block_till_done() + + assert self.hass.states.get('automation.hello') is not None + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + assert len(self.calls) == 2 diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 29d55b424f2..9bd22d0675c 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -50,6 +50,12 @@ class TestAutomationMQTT(unittest.TestCase): self.assertEqual('mqtt - test-topic - test_payload', self.calls[0].data['some']) + automation.turn_off(self.hass) + self.hass.pool.block_till_done() + fire_mqtt_message(self.hass, 'test-topic', 'test_payload') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fires_on_topic_and_payload_match(self): """Test if message is fired on topic and payload match.""" assert _setup_component(self.hass, automation.DOMAIN, { diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index f7d1447632f..9ee8514052c 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -45,6 +45,14 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + # Set above 12 so the automation will fire again + self.hass.states.set('test.entity', 12) + automation.turn_off(self.hass) + self.hass.pool.block_till_done() + self.hass.states.set('test.entity', 9) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fires_on_entity_change_over_to_below(self): """"Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 4a6971124b6..0b715cb365c 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -59,6 +59,12 @@ class TestAutomationState(unittest.TestCase): 'state - test.entity - hello - world - None', self.calls[0].data['some']) + automation.turn_off(self.hass) + self.hass.pool.block_till_done() + self.hass.states.set('test.entity', 'planet') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fires_on_entity_change_with_from_filter(self): """Test for firing on entity change with filter.""" assert _setup_component(self.hass, automation.DOMAIN, { diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 745e7c060ca..d3bbd254e1b 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -54,6 +54,18 @@ class TestAutomationSun(unittest.TestCase): } }) + automation.turn_off(self.hass) + self.hass.pool.block_till_done() + + fire_time_changed(self.hass, trigger_time) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + with patch('homeassistant.util.dt.utcnow', + return_value=now): + automation.turn_on(self.hass) + self.hass.pool.block_till_done() + fire_time_changed(self.hass, trigger_time) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index a643b731492..a33da951cc8 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -45,6 +45,13 @@ class TestAutomationTemplate(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + automation.turn_off(self.hass) + self.hass.pool.block_till_done() + + self.hass.states.set('test.entity', 'planet') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fires_on_change_str(self): """Test for firing on change.""" assert _setup_component(self.hass, automation.DOMAIN, { @@ -149,6 +156,9 @@ class TestAutomationTemplate(unittest.TestCase): } }) + self.hass.pool.block_till_done() + self.calls = [] + self.hass.states.set('test.entity', 'hello') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) @@ -209,9 +219,12 @@ class TestAutomationTemplate(unittest.TestCase): } }) + self.hass.pool.block_till_done() + self.calls = [] + self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) + assert len(self.calls) == 0 def test_if_fires_on_change_with_template_advanced(self): """Test for firing on change with template advanced.""" @@ -237,6 +250,9 @@ class TestAutomationTemplate(unittest.TestCase): } }) + self.hass.pool.block_till_done() + self.calls = [] + self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) @@ -287,29 +303,32 @@ class TestAutomationTemplate(unittest.TestCase): } }) + self.hass.pool.block_till_done() + self.calls = [] + self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) + assert len(self.calls) == 0 self.hass.states.set('test.entity', 'home') self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) + assert len(self.calls) == 1 self.hass.states.set('test.entity', 'work') self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) + assert len(self.calls) == 1 self.hass.states.set('test.entity', 'not_home') self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) + assert len(self.calls) == 1 self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) + assert len(self.calls) == 1 self.hass.states.set('test.entity', 'home') self.hass.pool.block_till_done() - self.assertEqual(2, len(self.calls)) + assert len(self.calls) == 2 def test_if_action(self): """Test for firing if action.""" diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index b36ce8c92b5..3c195f2eb38 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -43,7 +43,13 @@ class TestAutomationTime(unittest.TestCase): }) fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0)) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + automation.turn_off(self.hass) + self.hass.pool.block_till_done() + + fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0)) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index 24980b466bf..9d4161547ef 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -74,6 +74,24 @@ class TestAutomationZone(unittest.TestCase): 'zone - test.entity - hello - hello - test', self.calls[0].data['some']) + # Set out of zone again so we can trigger call + self.hass.states.set('test.entity', 'hello', { + 'latitude': 32.881011, + 'longitude': -117.234758 + }) + self.hass.pool.block_till_done() + + automation.turn_off(self.hass) + self.hass.pool.block_till_done() + + self.hass.states.set('test.entity', 'hello', { + 'latitude': 32.880586, + 'longitude': -117.237564 + }) + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + def test_if_not_fires_for_enter_on_zone_leave(self): """Test for not firing on zone leave.""" self.hass.states.set('test.entity', 'hello', { diff --git a/tests/components/binary_sensor/test_command_line.py b/tests/components/binary_sensor/test_command_line.py index 758911db353..62b856bbc23 100644 --- a/tests/components/binary_sensor/test_command_line.py +++ b/tests/components/binary_sensor/test_command_line.py @@ -3,6 +3,7 @@ import unittest from homeassistant.const import (STATE_ON, STATE_OFF) from homeassistant.components.binary_sensor import command_line +from homeassistant import bootstrap from tests.common import get_test_home_assistant @@ -24,6 +25,7 @@ class TestCommandSensorBinarySensor(unittest.TestCase): 'command': 'echo 1', 'payload_on': '1', 'payload_off': '0'} + devices = [] def add_dev_callback(devs): @@ -31,8 +33,7 @@ class TestCommandSensorBinarySensor(unittest.TestCase): for dev in devs: devices.append(dev) - command_line.setup_platform( - self.hass, config, add_dev_callback) + command_line.setup_platform(self.hass, config, add_dev_callback) self.assertEqual(1, len(devices)) entity = devices[0] @@ -41,19 +42,13 @@ class TestCommandSensorBinarySensor(unittest.TestCase): def test_setup_bad_config(self): """Test the setup with a bad configuration.""" - config = {} + config = {'name': 'test', + 'platform': 'not_command_line', + } - devices = [] - - def add_dev_callback(devs): - """Add callback to add devices.""" - for dev in devs: - devices.append(dev) - - self.assertFalse(command_line.setup_platform( - self.hass, config, add_dev_callback)) - - self.assertEqual(0, len(devices)) + self.assertFalse(bootstrap.setup_component(self.hass, 'test', { + 'command_line': config, + })) def test_template(self): """Test setting the state with a template.""" diff --git a/tests/components/binary_sensor/test_trend.py b/tests/components/binary_sensor/test_trend.py new file mode 100644 index 00000000000..beb8683e97f --- /dev/null +++ b/tests/components/binary_sensor/test_trend.py @@ -0,0 +1,229 @@ +"""The test for the Trend sensor platform.""" +import homeassistant.bootstrap as bootstrap + +from tests.common import get_test_home_assistant + + +class TestTrendBinarySensor: + """Test the Trend sensor.""" + + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_up(self): + """Test up trend.""" + assert bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'trend', + 'sensors': { + 'test_trend_sensor': { + 'entity_id': + "sensor.test_state" + } + } + } + }) + + self.hass.states.set('sensor.test_state', '1') + self.hass.pool.block_till_done() + self.hass.states.set('sensor.test_state', '2') + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test_trend_sensor') + assert state.state == 'on' + + def test_down(self): + """Test down trend.""" + assert bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'trend', + 'sensors': { + 'test_trend_sensor': { + 'entity_id': + "sensor.test_state" + } + } + } + }) + + self.hass.states.set('sensor.test_state', '2') + self.hass.pool.block_till_done() + self.hass.states.set('sensor.test_state', '1') + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test_trend_sensor') + assert state.state == 'off' + + def test__invert_up(self): + """Test up trend with custom message.""" + assert bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'trend', + 'sensors': { + 'test_trend_sensor': { + 'entity_id': + "sensor.test_state", + 'invert': "Yes" + } + } + } + }) + + self.hass.states.set('sensor.test_state', '1') + self.hass.pool.block_till_done() + self.hass.states.set('sensor.test_state', '2') + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test_trend_sensor') + assert state.state == 'off' + + def test_invert_down(self): + """Test down trend with custom message.""" + assert bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'trend', + 'sensors': { + 'test_trend_sensor': { + 'entity_id': + "sensor.test_state", + 'invert': "Yes" + } + } + } + }) + + self.hass.states.set('sensor.test_state', '2') + self.hass.pool.block_till_done() + self.hass.states.set('sensor.test_state', '1') + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test_trend_sensor') + assert state.state == 'on' + + def test_attribute_up(self): + """Test attribute up trend.""" + assert bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'trend', + 'sensors': { + 'test_trend_sensor': { + 'entity_id': + "sensor.test_state", + 'attribute': 'attr' + } + } + } + }) + self.hass.states.set('sensor.test_state', 'State', {'attr': '1'}) + self.hass.pool.block_till_done() + self.hass.states.set('sensor.test_state', 'State', {'attr': '2'}) + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test_trend_sensor') + assert state.state == 'on' + + def test_attribute_down(self): + """Test attribute down trend.""" + assert bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'trend', + 'sensors': { + 'test_trend_sensor': { + 'entity_id': + "sensor.test_state", + 'attribute': 'attr' + } + } + } + }) + + self.hass.states.set('sensor.test_state', 'State', {'attr': '2'}) + self.hass.pool.block_till_done() + self.hass.states.set('sensor.test_state', 'State', {'attr': '1'}) + + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test_trend_sensor') + assert state.state == 'off' + + def test_non_numeric(self): + """Test up trend.""" + assert bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'trend', + 'sensors': { + 'test_trend_sensor': { + 'entity_id': + "sensor.test_state" + } + } + } + }) + + self.hass.states.set('sensor.test_state', 'Non') + self.hass.pool.block_till_done() + self.hass.states.set('sensor.test_state', 'Numeric') + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test_trend_sensor') + assert state.state == 'off' + + def test_missing_attribute(self): + """Test attribute down trend.""" + assert bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'trend', + 'sensors': { + 'test_trend_sensor': { + 'entity_id': + "sensor.test_state", + 'attribute': 'missing' + } + } + } + }) + + self.hass.states.set('sensor.test_state', 'State', {'attr': '2'}) + self.hass.pool.block_till_done() + self.hass.states.set('sensor.test_state', 'State', {'attr': '1'}) + + self.hass.pool.block_till_done() + state = self.hass.states.get('binary_sensor.test_trend_sensor') + assert state.state == 'off' + + def test_invalid_name_does_not_create(self): + """Test invalid name.""" + assert not bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'template', + 'sensors': { + 'test INVALID sensor': { + 'entity_id': + "sensor.test_state" + } + } + } + }) + assert self.hass.states.all() == [] + + def test_invalid_sensor_does_not_create(self): + """Test invalid sensor.""" + assert not bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'template', + 'sensors': { + 'test_trend_sensor': { + 'not_entity_id': + "sensor.test_state" + } + } + } + }) + assert self.hass.states.all() == [] + + def test_no_sensors_does_not_create(self): + """Test no sensors.""" + assert not bootstrap.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'trend' + } + }) + assert self.hass.states.all() == [] diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index c30f59968e8..546152b0d8a 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -59,7 +59,7 @@ class TestLocalCamera(unittest.TestCase): fp.flush() with mock.patch('os.access', return_value=False): - assert setup_component(self.hass, 'camera', { + assert not setup_component(self.hass, 'camera', { 'camera': { 'name': 'config_test', 'platform': 'local_file', diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 4b3d4fcc64a..dbb9f8a192e 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -10,6 +10,7 @@ from tests.common import get_test_home_assistant ENTITY_CLIMATE = 'climate.hvac' +ENTITY_ECOBEE = 'climate.ecobee' class TestDemoClimate(unittest.TestCase): @@ -37,7 +38,7 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(67, state.attributes.get('humidity')) self.assertEqual(54, state.attributes.get('current_humidity')) self.assertEqual("Off", state.attributes.get('swing_mode')) - self.assertEqual("Cool", state.attributes.get('operation_mode')) + self.assertEqual("cool", state.attributes.get('operation_mode')) self.assertEqual('off', state.attributes.get('aux_heat')) def test_default_setup_params(self): @@ -48,7 +49,7 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(30, state.attributes.get('min_humidity')) self.assertEqual(99, state.attributes.get('max_humidity')) - def test_set_target_temp_bad_attr(self): + def test_set_only_target_temp_bad_attr(self): """Test setting the target temperature without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) @@ -56,23 +57,55 @@ class TestDemoClimate(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(21, state.attributes.get('temperature')) - def test_set_target_temp(self): + def test_set_only_target_temp(self): """Test the setting of the target temperature.""" + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual(21, state.attributes.get('temperature')) climate.set_temperature(self.hass, 30, ENTITY_CLIMATE) self.hass.pool.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(30.0, state.attributes.get('temperature')) + def test_set_target_temp_range(self): + """Test the setting of the target temperature with range.""" + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual(23.0, state.attributes.get('temperature')) + self.assertEqual(21.0, state.attributes.get('target_temp_low')) + self.assertEqual(24.0, state.attributes.get('target_temp_high')) + climate.set_temperature(self.hass, 30, ENTITY_ECOBEE, 25, 20) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual(30.0, state.attributes.get('temperature')) + self.assertEqual(20.0, state.attributes.get('target_temp_low')) + self.assertEqual(25.0, state.attributes.get('target_temp_high')) + + def test_set_target_temp_range_bad_attr(self): + """Test setting the target temperature range without required + attribute.""" + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual(23, state.attributes.get('temperature')) + self.assertEqual(21.0, state.attributes.get('target_temp_low')) + self.assertEqual(24.0, state.attributes.get('target_temp_high')) + climate.set_temperature(self.hass, None, ENTITY_ECOBEE, None, None) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_ECOBEE) + self.assertEqual(23, state.attributes.get('temperature')) + self.assertEqual(21.0, state.attributes.get('target_temp_low')) + self.assertEqual(24.0, state.attributes.get('target_temp_high')) + def test_set_target_humidity_bad_attr(self): """Test setting the target humidity without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(67, state.attributes.get('humidity')) climate.set_humidity(self.hass, None, ENTITY_CLIMATE) self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(67, state.attributes.get('humidity')) def test_set_target_humidity(self): """Test the setting of the target humidity.""" + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual(67, state.attributes.get('humidity')) climate.set_humidity(self.hass, 64, ENTITY_CLIMATE) self.hass.pool.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) @@ -84,10 +117,13 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual("On High", state.attributes.get('fan_mode')) climate.set_fan_mode(self.hass, None, ENTITY_CLIMATE) self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("On High", state.attributes.get('fan_mode')) def test_set_fan_mode(self): """Test setting of new fan mode.""" + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("On High", state.attributes.get('fan_mode')) climate.set_fan_mode(self.hass, "On Low", ENTITY_CLIMATE) self.hass.pool.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) @@ -99,30 +135,40 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual("Off", state.attributes.get('swing_mode')) climate.set_swing_mode(self.hass, None, ENTITY_CLIMATE) self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Off", state.attributes.get('swing_mode')) def test_set_swing(self): """Test setting of new swing mode.""" + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("Off", state.attributes.get('swing_mode')) climate.set_swing_mode(self.hass, "Auto", ENTITY_CLIMATE) self.hass.pool.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Auto", state.attributes.get('swing_mode')) - def test_set_operation_bad_attr(self): - """Test setting operation mode without required attribute.""" + def test_set_operation_bad_attr_and_state(self): + """Test setting operation mode without required attribute, and + check the state.""" state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("Cool", state.attributes.get('operation_mode')) + self.assertEqual("cool", state.attributes.get('operation_mode')) + self.assertEqual("cool", state.state) climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE) self.hass.pool.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("Cool", state.attributes.get('operation_mode')) + self.assertEqual("cool", state.attributes.get('operation_mode')) + self.assertEqual("cool", state.state) def test_set_operation(self): """Test setting of new operation mode.""" - climate.set_operation_mode(self.hass, "Heat", ENTITY_CLIMATE) + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("cool", state.attributes.get('operation_mode')) + self.assertEqual("cool", state.state) + climate.set_operation_mode(self.hass, "heat", ENTITY_CLIMATE) self.hass.pool.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) - self.assertEqual("Heat", state.attributes.get('operation_mode')) + self.assertEqual("heat", state.attributes.get('operation_mode')) + self.assertEqual("heat", state.state) def test_set_away_mode_bad_attr(self): """Test setting the away mode without required attribute.""" diff --git a/tests/components/climate/test_honeywell.py b/tests/components/climate/test_honeywell.py index 6c97b65dea7..75a4d1081f3 100644 --- a/tests/components/climate/test_honeywell.py +++ b/tests/components/climate/test_honeywell.py @@ -274,7 +274,7 @@ class TestHoneywellRound(unittest.TestCase): def test_set_temperature(self): """Test setting the temperature.""" - self.round1.set_temperature(25) + self.round1.set_temperature(temperature=25) self.device.set_temperature.assert_called_once_with('House', 25) def test_set_operation_mode(self: unittest.TestCase) -> None: @@ -327,13 +327,13 @@ class TestHoneywellUS(unittest.TestCase): def test_set_temp(self): """Test setting the temperature.""" - self.honeywell.set_temperature(70) + self.honeywell.set_temperature(temperature=70) self.assertEqual(70, self.device.setpoint_heat) self.assertEqual(70, self.honeywell.target_temperature) self.device.system_mode = 'cool' self.assertEqual(78, self.honeywell.target_temperature) - self.honeywell.set_temperature(74) + self.honeywell.set_temperature(temperature=74) self.assertEqual(74, self.device.setpoint_cool) self.assertEqual(74, self.honeywell.target_temperature) @@ -351,7 +351,7 @@ class TestHoneywellUS(unittest.TestCase): """Test if setting the temperature fails.""" self.device.setpoint_heat = mock.MagicMock( side_effect=somecomfort.SomeComfortError) - self.honeywell.set_temperature(123) + self.honeywell.set_temperature(temperature=123) def test_attributes(self): """Test the attributes.""" diff --git a/tests/components/cover/test_command_line.py b/tests/components/cover/test_command_line.py index bab0137f4f8..e4ef6793127 100644 --- a/tests/components/cover/test_command_line.py +++ b/tests/components/cover/test_command_line.py @@ -17,12 +17,10 @@ class TestCommandCover(unittest.TestCase): def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = ha.HomeAssistant() - self.hass.config.latitude = 32.87336 - self.hass.config.longitude = 117.22743 self.rs = cmd_rs.CommandCover(self.hass, 'foo', - 'cmd_open', 'cmd_close', - 'cmd_stop', 'cmd_state', - None) # FIXME + 'command_open', 'command_close', + 'command_stop', 'command_state', + None) def teardown_method(self, method): """Stop down everything that was started.""" @@ -47,10 +45,10 @@ class TestCommandCover(unittest.TestCase): with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, 'cover_status') test_cover = { - 'statecmd': 'cat {}'.format(path), - 'opencmd': 'echo 1 > {}'.format(path), - 'closecmd': 'echo 1 > {}'.format(path), - 'stopcmd': 'echo 0 > {}'.format(path), + 'command_state': 'cat {}'.format(path), + 'command_open': 'echo 1 > {}'.format(path), + 'command_close': 'echo 1 > {}'.format(path), + 'command_stop': 'echo 0 > {}'.format(path), 'value_template': '{{ value }}' } self.assertTrue(cover.setup(self.hass, { diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index fc03426a7a1..a4d5ee64b32 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -138,7 +138,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) asuswrt.ssh_connection() ssh.login.assert_called_once_with('fake_host', 'fake_user', - 'fake_pass') + password='fake_pass') def test_ssh_login_without_password_or_pubkey(self): \ # pylint: disable=invalid-name diff --git a/tests/components/device_tracker/test_automatic.py b/tests/components/device_tracker/test_automatic.py new file mode 100644 index 00000000000..e026d91a43c --- /dev/null +++ b/tests/components/device_tracker/test_automatic.py @@ -0,0 +1,254 @@ +"""Test the automatic device tracker platform.""" + +import logging +import requests +import unittest +from unittest.mock import patch + +from homeassistant.components.device_tracker.automatic import ( + URL_AUTHORIZE, URL_VEHICLES, URL_TRIPS, setup_scanner, + AutomaticDeviceScanner) + +_LOGGER = logging.getLogger(__name__) + +INVALID_USERNAME = 'bob' +VALID_USERNAME = 'jim' +PASSWORD = 'password' +CLIENT_ID = '12345' +CLIENT_SECRET = '54321' +FUEL_LEVEL = 77.2 +LATITUDE = 32.82336 +LONGITUDE = -117.23743 +ACCURACY = 8 +DISPLAY_NAME = 'My Vehicle' + + +def mocked_requests(*args, **kwargs): + """Mock requests.get invocations.""" + class MockResponse: + """Class to represent a mocked response.""" + + def __init__(self, json_data, status_code): + """Initialize the mock response class.""" + self.json_data = json_data + self.status_code = status_code + + def json(self): + """Return the json of the response.""" + return self.json_data + + @property + def content(self): + """Return the content of the response.""" + return self.json() + + def raise_for_status(self): + """Raise an HTTPError if status is not 200.""" + if self.status_code != 200: + raise requests.HTTPError(self.status_code) + + data = kwargs.get('data') + + if data and data.get('username', None) == INVALID_USERNAME: + return MockResponse({ + "error": "invalid_credentials" + }, 401) + elif str(args[0]).startswith(URL_AUTHORIZE): + return MockResponse({ + "user": { + "sid": "sid", + "id": "id" + }, + "token_type": "Bearer", + "access_token": "accesstoken", + "refresh_token": "refreshtoken", + "expires_in": 31521669, + "scope": "" + }, 200) + elif str(args[0]).startswith(URL_VEHICLES): + return MockResponse({ + "_metadata": { + "count": 2, + "next": None, + "previous": None + }, + "results": [ + { + "url": "https://api.automatic.com/vehicle/vid/", + "id": "vid", + "created_at": "2016-03-05T20:05:16.240000Z", + "updated_at": "2016-08-29T01:52:59.597898Z", + "make": "Honda", + "model": "Element", + "year": 2007, + "submodel": "EX", + "display_name": DISPLAY_NAME, + "fuel_grade": "regular", + "fuel_level_percent": FUEL_LEVEL, + "active_dtcs": [] + }] + }, 200) + elif str(args[0]).startswith(URL_TRIPS): + return MockResponse({ + "_metadata": { + "count": 1594, + "next": "https://api.automatic.com/trip/?page=2", + "previous": None + }, + "results": [ + { + "url": "https://api.automatic.com/trip/tid1/", + "id": "tid1", + "driver": "https://api.automatic.com/user/uid/", + "user": "https://api.automatic.com/user/uid/", + "started_at": "2016-08-28T19:37:23.986000Z", + "ended_at": "2016-08-28T19:43:22.500000Z", + "distance_m": 3931.6, + "duration_s": 358.5, + "vehicle": "https://api.automatic.com/vehicle/vid/", + "start_location": { + "lat": 32.87336, + "lon": -117.22743, + "accuracy_m": 10 + }, + "start_address": { + "name": "123 Fake St, Nowhere, NV 12345", + "display_name": "123 Fake St, Nowhere, NV", + "street_number": "Unknown", + "street_name": "Fake St", + "city": "Nowhere", + "state": "NV", + "country": "US" + }, + "end_location": { + "lat": LATITUDE, + "lon": LONGITUDE, + "accuracy_m": ACCURACY + }, + "end_address": { + "name": "321 Fake St, Nowhere, NV 12345", + "display_name": "321 Fake St, Nowhere, NV", + "street_number": "Unknown", + "street_name": "Fake St", + "city": "Nowhere", + "state": "NV", + "country": "US" + }, + "path": "path", + "vehicle_events": [], + "start_timezone": "America/Denver", + "end_timezone": "America/Denver", + "idling_time_s": 0, + "tags": [] + }, + { + "url": "https://api.automatic.com/trip/tid2/", + "id": "tid2", + "driver": "https://api.automatic.com/user/uid/", + "user": "https://api.automatic.com/user/uid/", + "started_at": "2016-08-28T18:48:00.727000Z", + "ended_at": "2016-08-28T18:55:25.800000Z", + "distance_m": 3969.1, + "duration_s": 445.1, + "vehicle": "https://api.automatic.com/vehicle/vid/", + "start_location": { + "lat": 32.87336, + "lon": -117.22743, + "accuracy_m": 11 + }, + "start_address": { + "name": "123 Fake St, Nowhere, NV, USA", + "display_name": "Fake St, Nowhere, NV", + "street_number": "123", + "street_name": "Fake St", + "city": "Nowhere", + "state": "NV", + "country": "US" + }, + "end_location": { + "lat": 32.82336, + "lon": -117.23743, + "accuracy_m": 10 + }, + "end_address": { + "name": "321 Fake St, Nowhere, NV, USA", + "display_name": "Fake St, Nowhere, NV", + "street_number": "Unknown", + "street_name": "Fake St", + "city": "Nowhere", + "state": "NV", + "country": "US" + }, + "path": "path", + "vehicle_events": [], + "start_timezone": "America/Denver", + "end_timezone": "America/Denver", + "idling_time_s": 0, + "tags": [] + } + ] + }, 200) + else: + _LOGGER.debug('UNKNOWN ROUTE') + + +class TestAutomatic(unittest.TestCase): + """Test cases around the automatic device scanner.""" + + def see_mock(self, **kwargs): + """Mock see function.""" + self.assertEqual('vid', kwargs.get('dev_id')) + self.assertEqual(FUEL_LEVEL, + kwargs.get('attributes', {}).get('fuel_level')) + self.assertEqual((LATITUDE, LONGITUDE), kwargs.get('gps')) + self.assertEqual(ACCURACY, kwargs.get('gps_accuracy')) + + def setUp(self): + """Set up test data.""" + + def tearDown(self): + """Tear down test data.""" + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_invalid_credentials(self, mock_get, mock_post): + """Test error is raised with invalid credentials.""" + config = { + 'platform': 'automatic', + 'username': INVALID_USERNAME, + 'password': PASSWORD, + 'client_id': CLIENT_ID, + 'secret': CLIENT_SECRET + } + + self.assertFalse(setup_scanner(None, config, self.see_mock)) + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_valid_credentials(self, mock_get, mock_post): + """Test error is raised with invalid credentials.""" + config = { + 'platform': 'automatic', + 'username': VALID_USERNAME, + 'password': PASSWORD, + 'client_id': CLIENT_ID, + 'secret': CLIENT_SECRET + } + + self.assertTrue(setup_scanner(None, config, self.see_mock)) + + @patch('requests.get', side_effect=mocked_requests) + @patch('requests.post', side_effect=mocked_requests) + def test_device_attributes(self, mock_get, mock_post): + """Test device attributes are set on load.""" + config = { + 'platform': 'automatic', + 'username': VALID_USERNAME, + 'password': PASSWORD, + 'client_id': CLIENT_ID, + 'secret': CLIENT_SECRET + } + + scanner = AutomaticDeviceScanner(config, self.see_mock) + + self.assertEqual(DISPLAY_NAME, scanner.get_device_name('vid')) diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 393b61a3134..57125d6e6ea 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -17,6 +17,10 @@ DEVICE = 'phone' LOCATION_TOPIC = "owntracks/{}/{}".format(USER, DEVICE) EVENT_TOPIC = "owntracks/{}/{}/event".format(USER, DEVICE) +WAYPOINT_TOPIC = owntracks.WAYPOINT_TOPIC.format(USER, DEVICE) +USER_BLACKLIST = 'ram' +WAYPOINT_TOPIC_BLOCKED = owntracks.WAYPOINT_TOPIC.format(USER_BLACKLIST, + DEVICE) DEVICE_TRACKER_STATE = "device_tracker.{}_{}".format(USER, DEVICE) @@ -24,6 +28,8 @@ IBEACON_DEVICE = 'keys' REGION_TRACKER_STATE = "device_tracker.beacon_{}".format(IBEACON_DEVICE) CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy' +CONF_WAYPOINT_IMPORT = owntracks.CONF_WAYPOINT_IMPORT +CONF_WAYPOINT_WHITELIST = owntracks.CONF_WAYPOINT_WHITELIST LOCATION_MESSAGE = { 'batt': 92, @@ -107,6 +113,48 @@ REGION_LEAVE_INACCURATE_MESSAGE = { 'lat': 20.0, '_type': 'transition'} +WAYPOINTS_EXPORTED_MESSAGE = { + "_type": "waypoints", + "_creator": "test", + "waypoints": [ + { + "_type": "waypoint", + "tst": 3, + "lat": 47, + "lon": 9, + "rad": 10, + "desc": "exp_wayp1" + }, + { + "_type": "waypoint", + "tst": 4, + "lat": 3, + "lon": 9, + "rad": 500, + "desc": "exp_wayp2" + } + ] +} + +WAYPOINTS_UPDATED_MESSAGE = { + "_type": "waypoints", + "_creator": "test", + "waypoints": [ + { + "_type": "waypoint", + "tst": 4, + "lat": 9, + "lon": 47, + "rad": 50, + "desc": "exp_wayp1" + }, + ] +} + +WAYPOINT_ENTITY_NAMES = ['zone.greg_phone__exp_wayp1', + 'zone.greg_phone__exp_wayp2', + 'zone.ram_phone__exp_wayp1', + 'zone.ram_phone__exp_wayp2'] REGION_ENTER_ZERO_MESSAGE = { 'lon': 1.0, @@ -132,6 +180,9 @@ REGION_LEAVE_ZERO_MESSAGE = { 'lat': 20.0, '_type': 'transition'} +BAD_JSON_PREFIX = '--$this is bad json#--' +BAD_JSON_SUFFIX = '** and it ends here ^^' + class TestDeviceTrackerOwnTracks(unittest.TestCase): """Test the OwnTrack sensor.""" @@ -143,7 +194,9 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase): self.assertTrue(device_tracker.setup(self.hass, { device_tracker.DOMAIN: { CONF_PLATFORM: 'owntracks', - CONF_MAX_GPS_ACCURACY: 200 + CONF_MAX_GPS_ACCURACY: 200, + CONF_WAYPOINT_IMPORT: True, + CONF_WAYPOINT_WHITELIST: ['jon', 'greg'] }})) self.hass.states.set( @@ -187,10 +240,18 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase): except FileNotFoundError: pass - def send_message(self, topic, message): + def mock_see(**kwargs): + """Fake see method for owntracks.""" + return + + def send_message(self, topic, message, corrupt=False): """Test the sending of a message.""" - fire_mqtt_message( - self.hass, topic, json.dumps(message)) + str_message = json.dumps(message) + if corrupt: + mod_message = BAD_JSON_PREFIX + str_message + BAD_JSON_SUFFIX + else: + mod_message = str_message + fire_mqtt_message(self.hass, topic, mod_message) self.hass.pool.block_till_done() def assert_location_state(self, location): @@ -530,3 +591,61 @@ class TestDeviceTrackerOwnTracks(unittest.TestCase): self.send_message(EVENT_TOPIC, exit_message) self.assertEqual(owntracks.MOBILE_BEACONS_ACTIVE['greg_phone'], []) + + def test_waypoint_import_simple(self): + """Test a simple import of list of waypoints.""" + waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() + self.send_message(WAYPOINT_TOPIC, waypoints_message) + # Check if it made it into states + wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[0]) + self.assertTrue(wayp is not None) + wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[1]) + self.assertTrue(wayp is not None) + + def test_waypoint_import_blacklist(self): + """Test import of list of waypoints for blacklisted user.""" + waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() + self.send_message(WAYPOINT_TOPIC_BLOCKED, waypoints_message) + # Check if it made it into states + wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[2]) + self.assertTrue(wayp is None) + wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[3]) + self.assertTrue(wayp is None) + + def test_waypoint_import_no_whitelist(self): + """Test import of list of waypoints with no whitelist set.""" + test_config = { + CONF_PLATFORM: 'owntracks', + CONF_MAX_GPS_ACCURACY: 200, + CONF_WAYPOINT_IMPORT: True + } + owntracks.setup_scanner(self.hass, test_config, self.mock_see) + waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() + self.send_message(WAYPOINT_TOPIC_BLOCKED, waypoints_message) + # Check if it made it into states + wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[2]) + self.assertTrue(wayp is not None) + wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[3]) + self.assertTrue(wayp is not None) + + def test_waypoint_import_bad_json(self): + """Test importing a bad JSON payload.""" + waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() + self.send_message(WAYPOINT_TOPIC, waypoints_message, True) + # Check if it made it into states + wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[2]) + self.assertTrue(wayp is None) + wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[3]) + self.assertTrue(wayp is None) + + def test_waypoint_import_existing(self): + """Test importing a zone that exists.""" + waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy() + self.send_message(WAYPOINT_TOPIC, waypoints_message) + # Get the first waypoint exported + wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[0]) + # Send an update + waypoints_message = WAYPOINTS_UPDATED_MESSAGE.copy() + self.send_message(WAYPOINT_TOPIC, waypoints_message) + new_wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[0]) + self.assertTrue(wayp == new_wayp) diff --git a/tests/components/device_tracker/test_unifi.py b/tests/components/device_tracker/test_unifi.py index e3f64cc84c3..8e43eb7485e 100644 --- a/tests/components/device_tracker/test_unifi.py +++ b/tests/components/device_tracker/test_unifi.py @@ -3,9 +3,12 @@ import unittest from unittest import mock import urllib -from homeassistant.components.device_tracker import unifi as unifi -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from unifi import controller +import voluptuous as vol + +from homeassistant.components.device_tracker import DOMAIN, unifi as unifi +from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + CONF_PLATFORM) class TestUnifiScanner(unittest.TestCase): @@ -16,13 +19,14 @@ class TestUnifiScanner(unittest.TestCase): def test_config_minimal(self, mock_ctrl, mock_scanner): """Test the setup with minimal configuration.""" config = { - 'device_tracker': { + DOMAIN: unifi.PLATFORM_SCHEMA({ + CONF_PLATFORM: unifi.DOMAIN, CONF_USERNAME: 'foo', CONF_PASSWORD: 'password', - } + }) } result = unifi.get_scanner(None, config) - self.assertEqual(unifi.UnifiScanner.return_value, result) + self.assertEqual(mock_scanner.return_value, result) mock_ctrl.assert_called_once_with('localhost', 'foo', 'password', 8443, 'v4', 'default') mock_scanner.assert_called_once_with(mock_ctrl.return_value) @@ -32,49 +36,38 @@ class TestUnifiScanner(unittest.TestCase): def test_config_full(self, mock_ctrl, mock_scanner): """Test the setup with full configuration.""" config = { - 'device_tracker': { + DOMAIN: unifi.PLATFORM_SCHEMA({ + CONF_PLATFORM: unifi.DOMAIN, CONF_USERNAME: 'foo', CONF_PASSWORD: 'password', CONF_HOST: 'myhost', 'port': 123, 'site_id': 'abcdef01', - } + }) } result = unifi.get_scanner(None, config) - self.assertEqual(unifi.UnifiScanner.return_value, result) + self.assertEqual(mock_scanner.return_value, result) mock_ctrl.assert_called_once_with('myhost', 'foo', 'password', 123, 'v4', 'abcdef01') mock_scanner.assert_called_once_with(mock_ctrl.return_value) - @mock.patch('homeassistant.components.device_tracker.unifi.UnifiScanner') - @mock.patch.object(controller, 'Controller') - def test_config_error(self, mock_ctrl, mock_scanner): + def test_config_error(self): """Test for configuration errors.""" - config = { - 'device_tracker': { + with self.assertRaises(vol.Invalid): + unifi.PLATFORM_SCHEMA({ + # no username + CONF_PLATFORM: unifi.DOMAIN, CONF_HOST: 'myhost', 'port': 123, - } - } - result = unifi.get_scanner(None, config) - self.assertFalse(result) - self.assertFalse(mock_ctrl.called) - - @mock.patch('homeassistant.components.device_tracker.unifi.UnifiScanner') - @mock.patch.object(controller, 'Controller') - def test_config_badport(self, mock_ctrl, mock_scanner): - """Test the setup with a bad port.""" - config = { - 'device_tracker': { + }) + with self.assertRaises(vol.Invalid): + unifi.PLATFORM_SCHEMA({ + CONF_PLATFORM: unifi.DOMAIN, CONF_USERNAME: 'foo', CONF_PASSWORD: 'password', CONF_HOST: 'myhost', - 'port': 'foo', - } - } - result = unifi.get_scanner(None, config) - self.assertFalse(result) - self.assertFalse(mock_ctrl.called) + 'port': 'foo', # bad port! + }) @mock.patch('homeassistant.components.device_tracker.unifi.UnifiScanner') @mock.patch.object(controller, 'Controller') @@ -82,6 +75,7 @@ class TestUnifiScanner(unittest.TestCase): """Test for controller failure.""" config = { 'device_tracker': { + CONF_PLATFORM: unifi.DOMAIN, CONF_USERNAME: 'foo', CONF_PASSWORD: 'password', } @@ -91,7 +85,7 @@ class TestUnifiScanner(unittest.TestCase): result = unifi.get_scanner(None, config) self.assertFalse(result) - def test_scanner_update(self): + def test_scanner_update(self): # pylint: disable=no-self-use """Test the scanner update.""" ctrl = mock.MagicMock() fake_clients = [ @@ -102,7 +96,7 @@ class TestUnifiScanner(unittest.TestCase): unifi.UnifiScanner(ctrl) ctrl.get_clients.assert_called_once_with() - def test_scanner_update_error(self): + def test_scanner_update_error(self): # pylint: disable=no-self-use """Test the scanner update for error.""" ctrl = mock.MagicMock() ctrl.get_clients.side_effect = urllib.error.HTTPError( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 4c5f14bf0f1..3678585141d 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -149,7 +149,7 @@ class TestMQTT(unittest.TestCase): def test_subscribe_topic(self): """Test the subscription of a topic.""" - mqtt.subscribe(self.hass, 'test-topic', self.record_calls) + unsub = mqtt.subscribe(self.hass, 'test-topic', self.record_calls) fire_mqtt_message(self.hass, 'test-topic', 'test-payload') @@ -158,6 +158,13 @@ class TestMQTT(unittest.TestCase): self.assertEqual('test-topic', self.calls[0][0]) self.assertEqual('test-payload', self.calls[0][1]) + unsub() + + fire_mqtt_message(self.hass, 'test-topic', 'test-payload') + + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_subscribe_topic_not_match(self): """Test if subscribed topic is not a match.""" mqtt.subscribe(self.hass, 'test-topic', self.record_calls) diff --git a/tests/components/notify/test_command_line.py b/tests/components/notify/test_command_line.py index ffe156deb9d..d350b0e4b37 100644 --- a/tests/components/notify/test_command_line.py +++ b/tests/components/notify/test_command_line.py @@ -2,13 +2,12 @@ import os import tempfile import unittest +from unittest.mock import patch import homeassistant.components.notify as notify - +from homeassistant import bootstrap from tests.common import get_test_home_assistant -from unittest.mock import patch - class TestCommandLine(unittest.TestCase): """Test the command line notifications.""" @@ -21,20 +20,23 @@ class TestCommandLine(unittest.TestCase): """Stop down everything that was started.""" self.hass.stop() + def test_setup(self): + """Test setup.""" + assert bootstrap.setup_component(self.hass, 'notify', { + 'notify': { + 'name': 'test', + 'platform': 'command_line', + 'command': 'echo $(cat); exit 1', + }}) + def test_bad_config(self): - """Test set up the platform with bad/missing config.""" + """Test set up the platform with bad/missing configuration.""" self.assertFalse(notify.setup(self.hass, { 'notify': { 'name': 'test', 'platform': 'bad_platform', } })) - self.assertFalse(notify.setup(self.hass, { - 'notify': { - 'name': 'test', - 'platform': 'command_line', - } - })) def test_command_line_output(self): """Test the command line output.""" diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index f0a05a01c1f..6f0daeaf7b8 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -68,7 +68,7 @@ class TestNotifyDemo(unittest.TestCase): 'data': {'hello': 'world'} } == data - def test_calling_notify_from_script_loaded_from_yaml(self): + def test_calling_notify_from_script_loaded_from_yaml_without_title(self): """Test if we can call a notify from a script.""" yaml_conf = """ service: notify.notify @@ -92,7 +92,38 @@ data_template: assert { 'message': 'Test 123 4', 'target': None, - 'title': 'Home Assistant', + 'data': { + 'push': { + 'sound': + 'US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav'}} + } == self.events[0].data + + def test_calling_notify_from_script_loaded_from_yaml_with_title(self): + """Test if we can call a notify from a script.""" + yaml_conf = """ +service: notify.notify +data: + data: + push: + sound: US-EN-Morgan-Freeman-Roommate-Is-Arriving.wav +data_template: + title: Test + message: > + Test 123 {{ 2 + 2 }} +""" + + with tempfile.NamedTemporaryFile() as fp: + fp.write(yaml_conf.encode('utf-8')) + fp.flush() + conf = yaml.load_yaml(fp.name) + + script.call_from_config(self.hass, conf) + self.hass.pool.block_till_done() + self.assertTrue(len(self.events) == 1) + assert { + 'message': 'Test 123 4', + 'title': 'Test', + 'target': None, 'data': { 'push': { 'sound': diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index 87d275b05f9..2564b0bd65a 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -8,6 +8,7 @@ import homeassistant.components.notify as notify from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT) import homeassistant.util.dt as dt_util +from homeassistant.bootstrap import _setup_component from tests.common import get_test_home_assistant @@ -25,11 +26,11 @@ class TestNotifyFile(unittest.TestCase): def test_bad_config(self): """Test set up the platform with bad/missing config.""" - self.assertFalse(notify.setup(self.hass, { + self.assertFalse(_setup_component(self.hass, notify.DOMAIN, { 'notify': { 'name': 'test', 'platform': 'file', - } + }, })) @patch('homeassistant.util.dt.utcnow') @@ -45,7 +46,7 @@ class TestNotifyFile(unittest.TestCase): 'name': 'test', 'platform': 'file', 'filename': filename, - 'timestamp': 0 + 'timestamp': False, } })) title = '{} notifications (Log started: {})\n{}\n'.format( diff --git a/tests/components/notify/test_smtp.py b/tests/components/notify/test_smtp.py index 7fa61fbdc24..bbaca71ee13 100644 --- a/tests/components/notify/test_smtp.py +++ b/tests/components/notify/test_smtp.py @@ -37,7 +37,7 @@ class TestNotifySmtp(unittest.TestCase): expected = ('Content-Type: text/plain; charset="us-ascii"\n' 'MIME-Version: 1.0\n' 'Content-Transfer-Encoding: 7bit\n' - 'Subject: \n' + 'Subject: Home Assistant\n' 'To: testrecip@test.com\n' 'From: test@test.com\n' 'X-Mailer: HomeAssistant\n' diff --git a/tests/components/sensor/test_command_line.py b/tests/components/sensor/test_command_line.py index bd083f7b63e..b089a82356b 100644 --- a/tests/components/sensor/test_command_line.py +++ b/tests/components/sensor/test_command_line.py @@ -2,7 +2,7 @@ import unittest from homeassistant.components.sensor import command_line - +from homeassistant import bootstrap from tests.common import get_test_home_assistant @@ -21,7 +21,8 @@ class TestCommandSensorSensor(unittest.TestCase): """Test sensor setup.""" config = {'name': 'Test', 'unit_of_measurement': 'in', - 'command': 'echo 5'} + 'command': 'echo 5' + } devices = [] def add_dev_callback(devs): @@ -29,8 +30,7 @@ class TestCommandSensorSensor(unittest.TestCase): for dev in devs: devices.append(dev) - command_line.setup_platform( - self.hass, config, add_dev_callback) + command_line.setup_platform(self.hass, config, add_dev_callback) self.assertEqual(1, len(devices)) entity = devices[0] @@ -40,19 +40,13 @@ class TestCommandSensorSensor(unittest.TestCase): def test_setup_bad_config(self): """Test setup with a bad configuration.""" - config = {} + config = {'name': 'test', + 'platform': 'not_command_line', + } - devices = [] - - def add_dev_callback(devs): - """Add a callback to add devices.""" - for dev in devs: - devices.append(dev) - - self.assertFalse(command_line.setup_platform( - self.hass, config, add_dev_callback)) - - self.assertEqual(0, len(devices)) + self.assertFalse(bootstrap.setup_component(self.hass, 'test', { + 'command_line': config, + })) def test_template(self): """Test command sensor with template.""" diff --git a/tests/components/sensor/test_mfi.py b/tests/components/sensor/test_mfi.py index 8180ca152f3..c1e6ac899ec 100644 --- a/tests/components/sensor/test_mfi.py +++ b/tests/components/sensor/test_mfi.py @@ -24,16 +24,14 @@ class TestMfiSensorSetup(unittest.TestCase): 'port': 6123, 'username': 'user', 'password': 'pass', - 'use_tls': True, - 'verify_tls': True, + 'ssl': True, + 'verify_ssl': True, } } def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.hass.config.latitude = 32.87336 - self.hass.config.longitude = 117.22743 def teardown_method(self, method): """Stop everything that was started.""" @@ -54,9 +52,8 @@ class TestMfiSensorSetup(unittest.TestCase): mock_client.FailedToLogin = Exception() mock_client.MFiClient.side_effect = mock_client.FailedToLogin self.assertFalse( - self.PLATFORM.setup_platform(self.hass, - dict(self.GOOD_CONFIG), - None)) + self.PLATFORM.setup_platform( + self.hass, dict(self.GOOD_CONFIG), None)) @mock.patch('mficlient.client') def test_setup_failed_connect(self, mock_client): @@ -64,9 +61,8 @@ class TestMfiSensorSetup(unittest.TestCase): mock_client.FailedToLogin = Exception() mock_client.MFiClient.side_effect = requests.exceptions.ConnectionError self.assertFalse( - self.PLATFORM.setup_platform(self.hass, - dict(self.GOOD_CONFIG), - None)) + self.PLATFORM.setup_platform( + self.hass, dict(self.GOOD_CONFIG), None)) @mock.patch('mficlient.client.MFiClient') def test_setup_minimum(self, mock_client): @@ -74,9 +70,8 @@ class TestMfiSensorSetup(unittest.TestCase): config = dict(self.GOOD_CONFIG) del config[self.THING]['port'] assert self.COMPONENT.setup(self.hass, config) - mock_client.assert_called_once_with('foo', 'user', 'pass', - port=6443, use_tls=True, - verify=True) + mock_client.assert_called_once_with( + 'foo', 'user', 'pass', port=6443, use_tls=True, verify=True) @mock.patch('mficlient.client.MFiClient') def test_setup_with_port(self, mock_client): @@ -84,21 +79,19 @@ class TestMfiSensorSetup(unittest.TestCase): config = dict(self.GOOD_CONFIG) config[self.THING]['port'] = 6123 assert self.COMPONENT.setup(self.hass, config) - mock_client.assert_called_once_with('foo', 'user', 'pass', - port=6123, use_tls=True, - verify=True) + mock_client.assert_called_once_with( + 'foo', 'user', 'pass', port=6123, use_tls=True, verify=True) @mock.patch('mficlient.client.MFiClient') def test_setup_with_tls_disabled(self, mock_client): """Test setup without TLS.""" config = dict(self.GOOD_CONFIG) del config[self.THING]['port'] - config[self.THING]['use_tls'] = False - config[self.THING]['verify_tls'] = False + config[self.THING]['ssl'] = False + config[self.THING]['verify_ssl'] = False assert self.COMPONENT.setup(self.hass, config) - mock_client.assert_called_once_with('foo', 'user', 'pass', - port=6080, use_tls=False, - verify=False) + mock_client.assert_called_once_with( + 'foo', 'user', 'pass', port=6080, use_tls=False, verify=False) @mock.patch('mficlient.client.MFiClient') @mock.patch('homeassistant.components.sensor.mfi.MfiSensor') @@ -123,8 +116,6 @@ class TestMfiSensor(unittest.TestCase): def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.hass.config.latitude = 32.87336 - self.hass.config.longitude = 117.22743 self.port = mock.MagicMock() self.sensor = mfi.MfiSensor(self.port, self.hass) @@ -156,6 +147,11 @@ class TestMfiSensor(unittest.TestCase): self.port.tag = 'balloons' self.assertEqual('balloons', self.sensor.unit_of_measurement) + def test_uom_uninitialized(self): + """Test that the UOM defaults if not initialized.""" + type(self.port).tag = mock.PropertyMock(side_effect=ValueError) + self.assertEqual('State', self.sensor.unit_of_measurement) + def test_state_digital(self): """Test the digital input.""" self.port.model = 'Input Digital' @@ -175,6 +171,11 @@ class TestMfiSensor(unittest.TestCase): with mock.patch.dict(mfi.DIGITS, {}): self.assertEqual(1.0, self.sensor.state) + def test_state_uninitialized(self): + """Test the state of uninitialized sensors.""" + type(self.port).tag = mock.PropertyMock(side_effect=ValueError) + self.assertEqual(mfi.STATE_OFF, self.sensor.state) + def test_update(self): """Test the update.""" self.sensor.update() diff --git a/tests/components/sensor/test_moldindicator.py b/tests/components/sensor/test_moldindicator.py index da2798e2a4d..c634c043db5 100644 --- a/tests/components/sensor/test_moldindicator.py +++ b/tests/components/sensor/test_moldindicator.py @@ -36,7 +36,7 @@ class TestSensorMoldIndicator(unittest.TestCase): 'indoor_temp_sensor': 'test.indoortemp', 'outdoor_temp_sensor': 'test.outdoortemp', 'indoor_humidity_sensor': 'test.indoorhumidity', - 'calibration_factor': '2.0' + 'calibration_factor': 2.0 } })) @@ -59,13 +59,11 @@ class TestSensorMoldIndicator(unittest.TestCase): 'indoor_temp_sensor': 'test.indoortemp', 'outdoor_temp_sensor': 'test.outdoortemp', 'indoor_humidity_sensor': 'test.indoorhumidity', - 'calibration_factor': '2.0' + 'calibration_factor': 2.0 } })) moldind = self.hass.states.get('sensor.mold_indicator') assert moldind - - # assert state assert moldind.state == '0' def test_calculation(self): @@ -76,7 +74,7 @@ class TestSensorMoldIndicator(unittest.TestCase): 'indoor_temp_sensor': 'test.indoortemp', 'outdoor_temp_sensor': 'test.outdoortemp', 'indoor_humidity_sensor': 'test.indoorhumidity', - 'calibration_factor': '2.0' + 'calibration_factor': 2.0 } })) @@ -108,23 +106,20 @@ class TestSensorMoldIndicator(unittest.TestCase): 'indoor_temp_sensor': 'test.indoortemp', 'outdoor_temp_sensor': 'test.outdoortemp', 'indoor_humidity_sensor': 'test.indoorhumidity', - 'calibration_factor': '2.0' + 'calibration_factor': 2.0 } })) - # Change indoor temp self.hass.states.set('test.indoortemp', '30', {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.pool.block_till_done() assert self.hass.states.get('sensor.mold_indicator').state == '90' - # Change outdoor temp self.hass.states.set('test.outdoortemp', '25', {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.pool.block_till_done() assert self.hass.states.get('sensor.mold_indicator').state == '57' - # Change humidity self.hass.states.set('test.indoorhumidity', '20', {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.pool.block_till_done() diff --git a/tests/components/switch/test_command_line.py b/tests/components/switch/test_command_line.py index f71fb9c25aa..5e17710f8fd 100644 --- a/tests/components/switch/test_command_line.py +++ b/tests/components/switch/test_command_line.py @@ -27,8 +27,8 @@ class TestCommandSwitch(unittest.TestCase): with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, 'switch_status') test_switch = { - 'oncmd': 'echo 1 > {}'.format(path), - 'offcmd': 'echo 0 > {}'.format(path), + 'command_on': 'echo 1 > {}'.format(path), + 'command_off': 'echo 0 > {}'.format(path), } self.assertTrue(switch.setup(self.hass, { 'switch': { @@ -59,9 +59,9 @@ class TestCommandSwitch(unittest.TestCase): with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, 'switch_status') test_switch = { - 'statecmd': 'cat {}'.format(path), - 'oncmd': 'echo 1 > {}'.format(path), - 'offcmd': 'echo 0 > {}'.format(path), + 'command_state': 'cat {}'.format(path), + 'command_on': 'echo 1 > {}'.format(path), + 'command_off': 'echo 0 > {}'.format(path), 'value_template': '{{ value=="1" }}' } self.assertTrue(switch.setup(self.hass, { @@ -95,9 +95,9 @@ class TestCommandSwitch(unittest.TestCase): oncmd = json.dumps({'status': 'ok'}) offcmd = json.dumps({'status': 'nope'}) test_switch = { - 'statecmd': 'cat {}'.format(path), - 'oncmd': 'echo \'{}\' > {}'.format(oncmd, path), - 'offcmd': 'echo \'{}\' > {}'.format(offcmd, path), + 'command_state': 'cat {}'.format(path), + 'command_on': 'echo \'{}\' > {}'.format(oncmd, path), + 'command_off': 'echo \'{}\' > {}'.format(offcmd, path), 'value_template': '{{ value_json.status=="ok" }}' } self.assertTrue(switch.setup(self.hass, { @@ -129,9 +129,9 @@ class TestCommandSwitch(unittest.TestCase): with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, 'switch_status') test_switch = { - 'statecmd': 'cat {}'.format(path), - 'oncmd': 'echo 1 > {}'.format(path), - 'offcmd': 'echo 0 > {}'.format(path), + 'command_state': 'cat {}'.format(path), + 'command_on': 'echo 1 > {}'.format(path), + 'command_off': 'echo 0 > {}'.format(path), } self.assertTrue(switch.setup(self.hass, { 'switch': { diff --git a/tests/components/switch/test_mfi.py b/tests/components/switch/test_mfi.py index 5eccb88f2ca..95a1000cc46 100644 --- a/tests/components/switch/test_mfi.py +++ b/tests/components/switch/test_mfi.py @@ -22,6 +22,8 @@ class TestMfiSwitchSetup(test_mfi_sensor.TestMfiSensorSetup): 'port': 6123, 'username': 'user', 'password': 'pass', + 'ssl': True, + 'verify_ssl': True, } } @@ -48,8 +50,6 @@ class TestMfiSwitch(unittest.TestCase): def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.hass.config.latitude = 32.87336 - self.hass.config.longitude = 117.22743 self.port = mock.MagicMock() self.switch = mfi.MfiSwitch(self.port) diff --git a/tests/components/test_group.py b/tests/components/test_group.py index d815489ae21..e82190a3f29 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -1,6 +1,7 @@ """The tests for the Group components.""" # pylint: disable=protected-access,too-many-public-methods import unittest +from unittest.mock import patch from homeassistant.bootstrap import _setup_component from homeassistant.const import ( @@ -308,3 +309,33 @@ class TestComponentsGroup(unittest.TestCase): self.assertEqual(STATE_NOT_HOME, self.hass.states.get( group.ENTITY_ID_FORMAT.format('peeps')).state) + + def test_reloading_groups(self): + """Test reloading the group config.""" + _setup_component(self.hass, 'group', {'group': { + 'second_group': { + 'entities': 'light.Bowl', + 'icon': 'mdi:work', + 'view': True, + }, + 'test_group': 'hello.world,sensor.happy', + 'empty_group': {'name': 'Empty Group', 'entities': None}, + } + }) + + assert sorted(self.hass.states.entity_ids()) == \ + ['group.empty_group', 'group.second_group', 'group.test_group'] + assert self.hass.bus.listeners['state_changed'] == 3 + + with patch('homeassistant.config.load_yaml_config_file', return_value={ + 'group': { + 'hello': { + 'entities': 'light.Bowl', + 'icon': 'mdi:work', + 'view': True, + }}}): + group.reload(self.hass) + self.hass.pool.block_till_done() + + assert self.hass.states.entity_ids() == ['group.hello'] + assert self.hass.bus.listeners['state_changed'] == 1 diff --git a/tests/components/test_weblink.py b/tests/components/test_weblink.py index 70a32e850ed..bb539d902ff 100644 --- a/tests/components/test_weblink.py +++ b/tests/components/test_weblink.py @@ -2,6 +2,7 @@ import unittest from homeassistant.components import weblink +from homeassistant import bootstrap from tests.common import get_test_home_assistant @@ -17,16 +18,23 @@ class TestComponentWeblink(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() + def test_bad_config(self): + """Test if new entity is created.""" + self.assertFalse(bootstrap.setup_component(self.hass, 'weblink', { + 'weblink': { + 'entities': [{}], + } + })) + def test_entities_get_created(self): """Test if new entity is created.""" self.assertTrue(weblink.setup(self.hass, { weblink.DOMAIN: { 'entities': [ { - weblink.ATTR_NAME: 'My router', - weblink.ATTR_URL: 'http://127.0.0.1/' + weblink.CONF_NAME: 'My router', + weblink.CONF_URL: 'http://127.0.0.1/' }, - {} ] } })) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 14d80d9104d..d9da2c51da7 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1,4 +1,6 @@ from datetime import timedelta +import os +import tempfile import pytest import voluptuous as vol @@ -59,6 +61,24 @@ def test_port(): schema(value) +def test_isfile(): + """Validate that the value is an existing file.""" + schema = vol.Schema(cv.isfile) + + with tempfile.NamedTemporaryFile() as fp: + pass + + for value in ('invalid', None, -1, 0, 80000, fp.name): + with pytest.raises(vol.Invalid): + schema(value) + + with tempfile.TemporaryDirectory() as tmp_path: + tmp_file = os.path.join(tmp_path, "test.txt") + with open(tmp_file, "w") as tmp_handl: + tmp_handl.write("test file") + schema(tmp_file) + + def test_url(): """Test URL.""" schema = vol.Schema(cv.url) @@ -279,9 +299,7 @@ def test_template(): """Test template validator.""" schema = vol.Schema(cv.template) - for value in ( - None, '{{ partial_print }', '{% if True %}Hello', {'dict': 'isbad'} - ): + for value in (None, '{{ partial_print }', '{% if True %}Hello', ['test']): with pytest.raises(vol.MultipleInvalid): schema(value) @@ -293,6 +311,24 @@ def test_template(): schema(value) +def test_template_complex(): + """Test template_complex validator.""" + schema = vol.Schema(cv.template_complex) + + for value in (None, '{{ partial_print }', '{% if True %}Hello'): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ( + 1, 'Hello', + '{{ beer }}', + '{% if 1 == 1 %}Hello{% else %}World{% endif %}', + {'test': 1, 'test': '{{ beer }}'}, + ['{{ beer }}', 1] + ): + schema(value) + + def test_time_zone(): """Test time zone validation.""" schema = vol.Schema(cv.time_zone) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 5d9f8d28e20..704a501eefc 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -65,13 +65,21 @@ class TestEventHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(2, len(runs)) + unsub = track_point_in_time( + self.hass, lambda x: runs.append(1), birthday_paulus) + unsub() + + self._send_time_changed(after_birthday) + self.hass.pool.block_till_done() + self.assertEqual(2, len(runs)) + def test_track_time_change(self): """Test tracking time change.""" wildcard_runs = [] specific_runs = [] - track_time_change(self.hass, lambda x: wildcard_runs.append(1)) - track_utc_time_change( + unsub = track_time_change(self.hass, lambda x: wildcard_runs.append(1)) + unsub_utc = track_utc_time_change( self.hass, lambda x: specific_runs.append(1), second=[0, 30]) self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) @@ -89,6 +97,14 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(2, len(specific_runs)) self.assertEqual(3, len(wildcard_runs)) + unsub() + unsub_utc() + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) + self.hass.pool.block_till_done() + self.assertEqual(2, len(specific_runs)) + self.assertEqual(3, len(wildcard_runs)) + def test_track_state_change(self): """Test track_state_change.""" # 2 lists to track how often our callbacks get called @@ -186,11 +202,12 @@ class TestEventHelpers(unittest.TestCase): # Track sunrise runs = [] - track_sunrise(self.hass, lambda: runs.append(1)) + unsub = track_sunrise(self.hass, lambda: runs.append(1)) offset_runs = [] offset = timedelta(minutes=30) - track_sunrise(self.hass, lambda: offset_runs.append(1), offset) + unsub2 = track_sunrise(self.hass, lambda: offset_runs.append(1), + offset) # run tests self._send_time_changed(next_rising - offset) @@ -208,6 +225,14 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(2, len(runs)) self.assertEqual(1, len(offset_runs)) + unsub() + unsub2() + + self._send_time_changed(next_rising + offset) + self.hass.pool.block_till_done() + self.assertEqual(2, len(runs)) + self.assertEqual(1, len(offset_runs)) + def test_track_sunset(self): """Test track the sunset.""" latitude = 32.87336 @@ -232,11 +257,11 @@ class TestEventHelpers(unittest.TestCase): # Track sunset runs = [] - track_sunset(self.hass, lambda: runs.append(1)) + unsub = track_sunset(self.hass, lambda: runs.append(1)) offset_runs = [] offset = timedelta(minutes=30) - track_sunset(self.hass, lambda: offset_runs.append(1), offset) + unsub2 = track_sunset(self.hass, lambda: offset_runs.append(1), offset) # Run tests self._send_time_changed(next_setting - offset) @@ -254,6 +279,14 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(2, len(runs)) self.assertEqual(1, len(offset_runs)) + unsub() + unsub2() + + self._send_time_changed(next_setting + offset) + self.hass.pool.block_till_done() + self.assertEqual(2, len(runs)) + self.assertEqual(1, len(offset_runs)) + def _send_time_changed(self, now): """Send a time changed event.""" self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) @@ -262,7 +295,7 @@ class TestEventHelpers(unittest.TestCase): """Test periodic tasks per minute.""" specific_runs = [] - track_utc_time_change( + unsub = track_utc_time_change( self.hass, lambda x: specific_runs.append(1), minute='/5') self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) @@ -277,11 +310,17 @@ class TestEventHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(2, len(specific_runs)) + unsub() + + self._send_time_changed(datetime(2014, 5, 24, 12, 5, 0)) + self.hass.pool.block_till_done() + self.assertEqual(2, len(specific_runs)) + def test_periodic_task_hour(self): """Test periodic tasks per hour.""" specific_runs = [] - track_utc_time_change( + unsub = track_utc_time_change( self.hass, lambda x: specific_runs.append(1), hour='/2') self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) @@ -304,11 +343,17 @@ class TestEventHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(3, len(specific_runs)) + unsub() + + self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0)) + self.hass.pool.block_till_done() + self.assertEqual(3, len(specific_runs)) + def test_periodic_task_day(self): """Test periodic tasks per day.""" specific_runs = [] - track_utc_time_change( + unsub = track_utc_time_change( self.hass, lambda x: specific_runs.append(1), day='/2') self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) @@ -323,11 +368,17 @@ class TestEventHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(2, len(specific_runs)) + unsub() + + self._send_time_changed(datetime(2014, 5, 4, 0, 0, 0)) + self.hass.pool.block_till_done() + self.assertEqual(2, len(specific_runs)) + def test_periodic_task_year(self): """Test periodic tasks per year.""" specific_runs = [] - track_utc_time_change( + unsub = track_utc_time_change( self.hass, lambda x: specific_runs.append(1), year='/2') self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) @@ -342,6 +393,12 @@ class TestEventHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(2, len(specific_runs)) + unsub() + + self._send_time_changed(datetime(2016, 5, 2, 0, 0, 0)) + self.hass.pool.block_till_done() + self.assertEqual(2, len(specific_runs)) + def test_periodic_task_wrong_input(self): """Test periodic tasks with wrong input.""" specific_runs = [] diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 5372b6a77d4..34f321776d6 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -43,6 +43,11 @@ class TestServiceHelpers(unittest.TestCase): 'entity_id': 'hello.world', 'data_template': { 'hello': '{{ \'goodbye\' }}', + 'data': { + 'value': '{{ \'complex\' }}', + 'simple': 'simple' + }, + 'list': ['{{ \'list\' }}', '2'], }, } runs = [] @@ -54,6 +59,9 @@ class TestServiceHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual('goodbye', runs[0].data['hello']) + self.assertEqual('complex', runs[0].data['data']['value']) + self.assertEqual('simple', runs[0].data['data']['simple']) + self.assertEqual('list', runs[0].data['list'][0]) def test_passing_variables_to_templates(self): """Test passing variables to templates.""" diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 266138d1fd5..64bac46264d 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -127,6 +127,30 @@ class TestUtilTemplate(unittest.TestCase): template.render(self.hass, '{{ %s | multiply(10) | round }}' % inp)) + def test_timestamp_custom(self): + """Test the timestamps to custom filter.""" + tests = [ + (None, None, None, 'None'), + (1469119144, None, True, '2016-07-21 16:39:04'), + (1469119144, '%Y', True, '2016'), + (1469119144, 'invalid', True, 'invalid'), + (dt_util.as_timestamp(dt_util.utcnow()), None, False, + dt_util.now().strftime('%Y-%m-%d %H:%M:%S')) + ] + + for inp, fmt, local, out in tests: + if fmt: + fil = 'timestamp_custom(\'{}\')'.format(fmt) + elif fmt and local: + fil = 'timestamp_custom(\'{0}\', {1})'.format(fmt, local) + else: + fil = 'timestamp_custom' + + self.assertEqual( + out, + template.render(self.hass, '{{ %s | %s }}' % (inp, fil)) + ) + def test_timestamp_local(self): """Test the timestamps to local filter.""" tests = { diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index fbd80760c12..f9ebaa634ff 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -137,7 +137,10 @@ class TestCheckConfig(unittest.TestCase): self.maxDiff = None with patch_yaml_files(files): - res = check_config.check(get_test_config_dir('secret.yaml')) + config_path = get_test_config_dir('secret.yaml') + secrets_path = get_test_config_dir('secrets.yaml') + + res = check_config.check(config_path) change_yaml_files(res) # convert secrets OrderedDict to dict for assertequal @@ -148,7 +151,7 @@ class TestCheckConfig(unittest.TestCase): 'components': {'http': {'api_password': 'abc123', 'server_port': 8123}}, 'except': {}, - 'secret_cache': {'secrets.yaml': {'http_pw': 'abc123'}}, + 'secret_cache': {secrets_path: {'http_pw': 'abc123'}}, 'secrets': {'http_pw': 'abc123'}, - 'yaml_files': ['.../secret.yaml', 'secrets.yaml'] + 'yaml_files': ['.../secret.yaml', '.../secrets.yaml'] }, res) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index d41dc60ee15..8ad9d1cc409 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -177,6 +177,7 @@ class TestBootstrap: return_value=False) def test_component_not_installed_if_requirement_fails(self, mock_install): """Component setup should fail if requirement can't install.""" + self.hass.config.skip_pip = False loader.set_component( 'comp', MockModule('comp', requirements=['package==0.0.1'])) @@ -210,19 +211,19 @@ class TestBootstrap: deps = ['non_existing'] loader.set_component('comp', MockModule('comp', dependencies=deps)) - assert not bootstrap._setup_component(self.hass, 'comp', None) + assert not bootstrap._setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components self.hass.config.components.append('non_existing') - assert bootstrap._setup_component(self.hass, 'comp', None) + assert bootstrap._setup_component(self.hass, 'comp', {}) def test_component_failing_setup(self): """Test component that fails setup.""" loader.set_component( 'comp', MockModule('comp', setup=lambda hass, config: False)) - assert not bootstrap._setup_component(self.hass, 'comp', None) + assert not bootstrap._setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components def test_component_exception_setup(self): @@ -233,7 +234,7 @@ class TestBootstrap: loader.set_component('comp', MockModule('comp', setup=exception_setup)) - assert not bootstrap._setup_component(self.hass, 'comp', None) + assert not bootstrap._setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components def test_home_assistant_core_config_validation(self): @@ -279,7 +280,7 @@ class TestBootstrap: loader.set_component( 'switch.platform_a', - MockPlatform('comp_b', platform_schema=platform_schema)) + MockPlatform(platform_schema=platform_schema)) assert not bootstrap.setup_component(self.hass, 'switch', { 'switch': { diff --git a/tests/test_core.py b/tests/test_core.py index aa3cdd2aecc..76c82252d30 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -166,14 +166,37 @@ class TestEventBus(unittest.TestCase): self.assertEqual(old_count + 1, len(self.bus.listeners)) # Try deleting a non registered listener, nothing should happen - self.bus.remove_listener('test', lambda x: len) + self.bus._remove_listener('test', lambda x: len) # Remove listener - self.bus.remove_listener('test', listener) + self.bus._remove_listener('test', listener) self.assertEqual(old_count, len(self.bus.listeners)) # Try deleting listener while category doesn't exist either - self.bus.remove_listener('test', listener) + self.bus._remove_listener('test', listener) + + def test_unsubscribe_listener(self): + """Test unsubscribe listener from returned function.""" + self.bus._pool.add_worker() + calls = [] + + def listener(event): + """Mock listener.""" + calls.append(event) + + unsub = self.bus.listen('test', listener) + + self.bus.fire('test') + self.bus._pool.block_till_done() + + assert len(calls) == 1 + + unsub() + + self.bus.fire('event') + self.bus._pool.block_till_done() + + assert len(calls) == 1 def test_listen_once_event(self): """Test listen_once_event method.""" diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 4ce0def08ac..6b35e4f844c 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -3,59 +3,68 @@ import io import unittest import os import tempfile +from unittest.mock import patch + from homeassistant.exceptions import HomeAssistantError from homeassistant.util import yaml -import homeassistant.config as config_util -from tests.common import get_test_config_dir +from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file +from tests.common import get_test_config_dir, patch_yaml_files class TestYaml(unittest.TestCase): """Test util.yaml loader.""" + # pylint: disable=no-self-use,invalid-name def test_simple_list(self): """Test simple list.""" conf = "config:\n - simple\n - list" - with io.StringIO(conf) as f: - doc = yaml.yaml.safe_load(f) + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) assert doc['config'] == ["simple", "list"] def test_simple_dict(self): """Test simple dict.""" conf = "key: value" - with io.StringIO(conf) as f: - doc = yaml.yaml.safe_load(f) + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) assert doc['key'] == 'value' def test_duplicate_key(self): - """Test simple dict.""" - conf = "key: thing1\nkey: thing2" - try: - with io.StringIO(conf) as f: - yaml.yaml.safe_load(f) - except Exception: - pass - else: - assert 0 + """Test duplicate dict keys.""" + files = {YAML_CONFIG_FILE: 'key: thing1\nkey: thing2'} + with self.assertRaises(HomeAssistantError): + with patch_yaml_files(files): + load_yaml_config_file(YAML_CONFIG_FILE) + + def test_unhashable_key(self): + """Test an unhasable key.""" + files = {YAML_CONFIG_FILE: 'message:\n {{ states.state }}'} + with self.assertRaises(HomeAssistantError), \ + patch_yaml_files(files): + load_yaml_config_file(YAML_CONFIG_FILE) + + def test_no_key(self): + """Test item without an key.""" + files = {YAML_CONFIG_FILE: 'a: a\nnokeyhere'} + with self.assertRaises(HomeAssistantError), \ + patch_yaml_files(files): + yaml.load_yaml(YAML_CONFIG_FILE) def test_enviroment_variable(self): """Test config file with enviroment variable.""" os.environ["PASSWORD"] = "secret_password" conf = "password: !env_var PASSWORD" - with io.StringIO(conf) as f: - doc = yaml.yaml.safe_load(f) + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) assert doc['password'] == "secret_password" del os.environ["PASSWORD"] def test_invalid_enviroment_variable(self): """Test config file with no enviroment variable sat.""" conf = "password: !env_var PASSWORD" - try: - with io.StringIO(conf) as f: - yaml.yaml.safe_load(f) - except Exception: - pass - else: - assert 0 + with self.assertRaises(HomeAssistantError): + with io.StringIO(conf) as file: + yaml.yaml.safe_load(file) def test_include_yaml(self): """Test include yaml.""" @@ -63,8 +72,8 @@ class TestYaml(unittest.TestCase): include_file.write(b"value") include_file.seek(0) conf = "key: !include {}".format(include_file.name) - with io.StringIO(conf) as f: - doc = yaml.yaml.safe_load(f) + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) assert doc["key"] == "value" def test_include_dir_list(self): @@ -79,8 +88,8 @@ class TestYaml(unittest.TestCase): file_2.write(b"two") file_2.close() conf = "key: !include_dir_list {}".format(include_dir) - with io.StringIO(conf) as f: - doc = yaml.yaml.safe_load(f) + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) assert sorted(doc["key"]) == sorted(["one", "two"]) def test_include_dir_named(self): @@ -98,8 +107,8 @@ class TestYaml(unittest.TestCase): correct = {} correct[os.path.splitext(os.path.basename(file_1.name))[0]] = "one" correct[os.path.splitext(os.path.basename(file_2.name))[0]] = "two" - with io.StringIO(conf) as f: - doc = yaml.yaml.safe_load(f) + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) assert doc["key"] == correct def test_include_dir_merge_list(self): @@ -114,8 +123,8 @@ class TestYaml(unittest.TestCase): file_2.write(b"- two\n- three") file_2.close() conf = "key: !include_dir_merge_list {}".format(include_dir) - with io.StringIO(conf) as f: - doc = yaml.yaml.safe_load(f) + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) assert sorted(doc["key"]) == sorted(["one", "two", "three"]) def test_include_dir_merge_named(self): @@ -130,23 +139,25 @@ class TestYaml(unittest.TestCase): file_2.write(b"key2: two\nkey3: three") file_2.close() conf = "key: !include_dir_merge_named {}".format(include_dir) - with io.StringIO(conf) as f: - doc = yaml.yaml.safe_load(f) + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) assert doc["key"] == { "key1": "one", "key2": "two", "key3": "three" } +FILES = {} + def load_yaml(fname, string): """Write a string to file and return the parsed yaml.""" - with open(fname, 'w') as file: - file.write(string) - return config_util.load_yaml_config_file(fname) + FILES[fname] = string + with patch_yaml_files(FILES): + return load_yaml_config_file(fname) -class FakeKeyring(): +class FakeKeyring(): # pylint: disable=too-few-public-methods """Fake a keyring class.""" def __init__(self, secrets_dict): @@ -162,20 +173,16 @@ class FakeKeyring(): class TestSecrets(unittest.TestCase): """Test the secrets parameter in the yaml utility.""" + # pylint: disable=protected-access,invalid-name def setUp(self): # pylint: disable=invalid-name """Create & load secrets file.""" config_dir = get_test_config_dir() yaml.clear_secret_cache() - self._yaml_path = os.path.join(config_dir, - config_util.YAML_CONFIG_FILE) + self._yaml_path = os.path.join(config_dir, YAML_CONFIG_FILE) self._secret_path = os.path.join(config_dir, yaml._SECRET_YAML) self._sub_folder_path = os.path.join(config_dir, 'subFolder') - if not os.path.exists(self._sub_folder_path): - os.makedirs(self._sub_folder_path) self._unrelated_path = os.path.join(config_dir, 'unrelated') - if not os.path.exists(self._unrelated_path): - os.makedirs(self._unrelated_path) load_yaml(self._secret_path, 'http_pw: pwhttp\n' @@ -194,12 +201,7 @@ class TestSecrets(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """Clean up secrets.""" yaml.clear_secret_cache() - for path in [self._yaml_path, self._secret_path, - os.path.join(self._sub_folder_path, 'sub.yaml'), - os.path.join(self._sub_folder_path, yaml._SECRET_YAML), - os.path.join(self._unrelated_path, yaml._SECRET_YAML)]: - if os.path.isfile(path): - os.remove(path) + FILES.clear() def test_secrets_from_yaml(self): """Did secrets load ok.""" @@ -263,3 +265,12 @@ class TestSecrets(unittest.TestCase): """Ensure logger: debug was removed.""" with self.assertRaises(yaml.HomeAssistantError): load_yaml(self._yaml_path, 'api_password: !secret logger') + + @patch('homeassistant.util.yaml._LOGGER.error') + def test_bad_logger_value(self, mock_error): + """Ensure logger: debug was removed.""" + yaml.clear_secret_cache() + load_yaml(self._secret_path, 'logger: info\npw: abc') + load_yaml(self._yaml_path, 'api_password: !secret pw') + assert mock_error.call_count == 1, \ + "Expected an error about logger: value"