From 658c0ca6fcae016167efb15da4be31e61185869c Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 8 Sep 2020 02:43:21 -0500 Subject: [PATCH 01/17] Guard against overwriting known attributes with blanks (#112) ISYv4 firmware sends blank UOMs in the event updates, which can cause issues if it overwrites a known, valid UOM. This should resolve the issue home-assistant/core#37537 once a new package is published. --- pyisy/nodes/node.py | 5 +++-- pyisy/nodes/nodebase.py | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pyisy/nodes/node.py b/pyisy/nodes/node.py index 47dbfdfd..d237432e 100755 --- a/pyisy/nodes/node.py +++ b/pyisy/nodes/node.py @@ -230,7 +230,7 @@ def update_state(self, state): self._prec = state.prec changed = True - if state.uom != self._uom: + if state.uom != self._uom and state.uom != "": self._uom = state.uom changed = True @@ -339,7 +339,8 @@ def _set_climate_setpoint(self, val, setpoint_name, setpoint_prop): if not self.is_thermostat: self.isy.log.warning( "Failed to set %s setpoint on %s, it is not a thermostat node.", - setpoint_name, self.address, + setpoint_name, + self.address, ) return # ISY wants 2 times the temperature for Insteon in order to not lose precision diff --git a/pyisy/nodes/nodebase.py b/pyisy/nodes/nodebase.py index ca052922..ab4d3b80 100755 --- a/pyisy/nodes/nodebase.py +++ b/pyisy/nodes/nodebase.py @@ -205,8 +205,12 @@ def update_property(self, prop): self.update_last_update() aux_prop = self.aux_properties.get(prop.control) - if aux_prop and aux_prop == prop: - return + if aux_prop: + if prop.uom == "" and not aux_prop.uom == "": + # Guard against overwriting known UOM with blank UOM (ISYv4). + prop.uom = aux_prop.uom + if aux_prop == prop: + return self.aux_properties[prop.control] = prop self.update_last_changed() self.status_events.notify(self.status_feedback) From 8c230e07cf6ee12bfc731a10077aefb48f55d49f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 09:46:02 +0200 Subject: [PATCH 02/17] Create release-drafter.yml --- .github/release-drafter.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/release-drafter.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..a4dce4c2 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,4 @@ +template: | + ## What's Changed + + $CHANGES From 9725accb12ae7519b3248af5880152a10e9595c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 09:46:28 +0200 Subject: [PATCH 03/17] Create dependabot.yml --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..491deae0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 From 84866e139a1f9f47a40d03a037c908f03331499a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 09:47:09 +0200 Subject: [PATCH 04/17] Create pythonpublish.yml --- .github/workflows/pythonpublish.yml | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/pythonpublish.yml diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 00000000..8fadfb1f --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,31 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [published] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* From 297087b9b1826b4153dce561a65e8caf50d88ef4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 09:48:42 +0200 Subject: [PATCH 05/17] Create ci.yml --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..72334791 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [master] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: pre-commit/action@v2.0.0 From 14ba6fc2db60508c3288bc97b8b139484b7b26ce Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 8 Sep 2020 03:02:31 -0500 Subject: [PATCH 06/17] Add Devcontainer, Update Requirements, Use PyUpgrade (#105) --- .devcontainer/Dockerfile | 20 ++++++++++++++++++++ .devcontainer/devcontainer.json | 23 +++++++++++++++++++++++ .devcontainer/postCreate.sh | 13 +++++++++++++ .gitignore | 4 +--- .pre-commit-config.yaml | 19 ++++++++++++++----- .vscode/extensions.json | 3 +++ .vscode/settings.json | 11 +++++++++++ README.md | 23 +++++++++++++++++------ examples/connection_test.py | 1 + pyisy/connection.py | 2 +- pyisy/eventreader.py | 3 +-- pyisy/events.py | 2 +- pyisy/programs/program.py | 2 +- pyisy/variables/variable.py | 2 +- requirements-dev.txt | 11 ++++++++++- requirements.txt | 1 + setup.py | 2 +- 17 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/postCreate.sh create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..0c0935e4 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.8 + +RUN \ + apt-get update && apt-get install -y --no-install-recommends \ + git \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspaces + +# Install Python dependencies from requirements +COPY requirements.txt requirements-dev.txt ./ +RUN pip3 install -r requirements.txt \ + && pip3 install -r requirements-dev.txt \ + && rm -f requirements.txt requirements-dev.txt + +ENV PATH=/root/.local/bin:${PATH} + +# Set the default shell to bash instead of sh +ENV SHELL /bin/bash diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..db21ebb6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "name": "PyISY Devcontainer", + "context": "..", + "dockerFile": "Dockerfile", + "postCreateCommand": ".devcontainer/postCreate.sh", + "runArgs": ["-e", "GIT_EDITOR=code --wait"], + "extensions": [ + "ms-python.python", + "visualstudioexptteam.vscodeintellicode", + "esbenp.prettier-vscode" + ], + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true, + "terminal.integrated.shell.linux": "/bin/bash" + } +} diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100644 index 00000000..808be337 --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +cd /workspaces/PyISY + +# Setup the test_scripts folder as copy of the examples. +mkdir test_scripts +cp -r examples/* test_scripts/ + +# Install the editable local package +pip3 install -e . + +# Install pre-commit requirements +pre-commit install diff --git a/.gitignore b/.gitignore index 0a3fbbf3..c9f4bf43 100755 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ lib/ lib64/ parts/ sdist/ +test_scripts/ var/ *.egg-info/ .installed.cfg @@ -61,6 +62,3 @@ docs/_build/ # PyBuilder target/ .AppleDouble - -# VSCode Configs -.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7438e05..6c45e9df 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,23 @@ --- repos: + - repo: https://github.com/asottile/pyupgrade + rev: v2.3.0 + hooks: + - id: pyupgrade + args: [--py37-plus] - hooks: - args: [--safe, --quiet] - files: ^((pyisy)/.+)?[^/]+\.py$ + files: ^((pyisy|examples)/.+)?[^/]+\.py$ id: black repo: https://github.com/psf/black rev: 19.10b0 - hooks: - - args: ['--ignore-words-list=pyisy,hass,isy,nid,dof,dfof,don,dfon,tim', - '--skip="./.*,*.json"', --quiet-level=2] + - args: + [ + "--ignore-words-list=pyisy,hass,isy,nid,dof,dfof,don,dfon,tim", + '--skip="./.*,*.json"', + --quiet-level=2, + ] exclude_types: [json] id: codespell repo: https://github.com/codespell-project/codespell @@ -18,8 +27,8 @@ repos: files: ^(pyisy)/.+\.py$ id: flake8 repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + rev: 3.8.1 - hooks: - - {id: isort} + - { id: isort } repo: https://github.com/pre-commit/mirrors-isort rev: v4.3.21 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..95113413 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode", "ms-python.python"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..bccd3c0e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.linting.pylintEnabled": false, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true, + "terminal.integrated.shell.linux": "/bin/bash", + "python.linting.flake8Enabled": true +} diff --git a/README.md b/README.md index f8739b8d..1075b001 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This library allows for easy interaction with ISY nodes, programs, variables, an assigned as handlers when ISY parameters are changed. ISY parameters can be monitored automatically as changes are reported from the device. -**NOTE:** Significant changes have been made in V2, please refer to the [CHANGELOG](CHANGELOG.md) for details. It is recommended you do not update to the latest version without testing for any unknown breaking changes or impacts to your dependent code. +**NOTE:** Significant changes have been made in V2, please refer to the [CHANGELOG](CHANGELOG.md) for details. It is recommended you do not update to the latest version without testing for any unknown breaking changes or impacts to your dependent code. ### Examples @@ -14,9 +14,9 @@ See the [examples](examples/) folder for connection examples. ### Development Team -* Greg Laabs ([@OverloadUT]) - Maintainer -* Ryan Kraus ([@rmkraus]) - Creator -* Tim Bond ([@shbatm]) - Version 2 Contributor +- Greg Laabs ([@overloadut]) - Maintainer +- Ryan Kraus ([@rmkraus]) - Creator +- Tim Bond ([@shbatm]) - Version 2 Contributor ### Contributing @@ -32,6 +32,17 @@ pip install pre-commit pre-commit install ``` -[@OverloadUT]: https://github.com/overloadut +A [VSCode DevContainer](https://code.visualstudio.com/docs/remote/containers#_getting-started) is also available to provide a consistent development environment. + +Assuming you have the pre-requisites installed from the link above (VSCode, Docker, & Remote-Containers Extension), to get started: + +1. Fork the repository. +2. Clone the repository to your computer. +3. Open the repository using Visual Studio code. +4. When you open this repository with Visual Studio code you are asked to "Reopen in Container", this will start the build of the container. + - If you don't see this notification, open the command palette and select Remote-Containers: Reopen Folder in Container. +5. Once started, you will also have a `test_scripts/` folder with a copy of the example scripts to run in the container which won't be committed to the repo, so you can update them with your connection details and test directly on your ISY. + +[@overloadut]: https://github.com/overloadut [@rmkraus]: https://github.com/rmkraus -[@shbatm]: https://github.com/shbatm \ No newline at end of file +[@shbatm]: https://github.com/shbatm diff --git a/examples/connection_test.py b/examples/connection_test.py index c11142ca..47197b3b 100644 --- a/examples/connection_test.py +++ b/examples/connection_test.py @@ -24,6 +24,7 @@ _LOGGER = logging.getLogger(__name__) + def main(arguments): """Execute primary loop.""" logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, level=LOG_LEVEL) diff --git a/pyisy/connection.py b/pyisy/connection.py index 1c51b96a..65a8a0d1 100644 --- a/pyisy/connection.py +++ b/pyisy/connection.py @@ -252,7 +252,7 @@ def __init__(self, tls_ver): self.tls = ssl.PROTOCOL_TLSv1_1 elif tls_ver == 1.2: self.tls = ssl.PROTOCOL_TLSv1_2 - super(TLSHttpAdapter, self).__init__() + super().__init__() def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs): """Initialize the Pool Manager.""" diff --git a/pyisy/eventreader.py b/pyisy/eventreader.py index d69fcfab..c01575cc 100644 --- a/pyisy/eventreader.py +++ b/pyisy/eventreader.py @@ -1,7 +1,6 @@ """ISY Event Reader.""" import errno import select -import socket import ssl from .constants import SOCKET_BUFFER_SIZE @@ -81,7 +80,7 @@ def _receive_into_buffer(self, timeout): self._event_buffer += new_data except ssl.SSLWantReadError: pass - except socket.error as ex: + except OSError as ex: if ex.errno != errno.EWOULDBLOCK: raise diff --git a/pyisy/events.py b/pyisy/events.py index c8050b1f..78c6bce4 100644 --- a/pyisy/events.py +++ b/pyisy/events.py @@ -247,7 +247,7 @@ def watch(self): ) self._lost_connection() return - except socket.error as ex: + except OSError as ex: self.isy.log.warning( "PyISY encountered a socket error while reading the event stream: %s.", ex, diff --git a/pyisy/programs/program.py b/pyisy/programs/program.py index 484f9750..7af70a37 100644 --- a/pyisy/programs/program.py +++ b/pyisy/programs/program.py @@ -44,7 +44,7 @@ def __init__( prunning, ): """Initialize a Program class.""" - super(Program, self).__init__(programs, address, pname, pstatus, plastup) + super().__init__(programs, address, pname, pstatus, plastup) self._enabled = penabled self._last_finished = plastfin self._last_run = plastrun diff --git a/pyisy/variables/variable.py b/pyisy/variables/variable.py index ce6f4ad8..b6d995cc 100644 --- a/pyisy/variables/variable.py +++ b/pyisy/variables/variable.py @@ -36,7 +36,7 @@ class Variable: def __init__(self, variables, vid, vtype, vname, init, status, ts): """Initialize a Variable class.""" - super(Variable, self).__init__() + super().__init__() self._id = vid self._init = init self._last_edited = ts diff --git a/requirements-dev.txt b/requirements-dev.txt index e42b23c1..5788b22e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1,10 @@ -pre-commit \ No newline at end of file +pylint==2.4.4 +pylint-strict-informational==0.1 +black==19.10b0 +codespell==1.16.0 +flake8-docstrings==1.5.0 +flake8==3.8.1 +isort==4.3.21 +pydocstyle==5.0.2 +pyupgrade==2.3.0 +pre-commit>=2.4.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8b37518d..dafd5c68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ requests>=2.22 +python-dateutil>=2.8.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 79d21d15..d34bb90a 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ include_package_data=True, platforms="any", setup_requires=["setuptools-git-version"], - install_requires=["requests"], + install_requires=["requests", "python-dateutil"], keywords=["home automation", "isy", "isy994", "isy-994", "UDI"], classifiers=[ "Intended Audience :: Developers", From 9fa674ab08f6d289cd5c0ea9959d7f7b1801915c Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 7 Sep 2020 17:14:48 -0500 Subject: [PATCH 07/17] Fix #109 - Update for events depreciation warning --- pyisy/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyisy/events.py b/pyisy/events.py index 78c6bce4..c4872df8 100644 --- a/pyisy/events.py +++ b/pyisy/events.py @@ -123,7 +123,7 @@ def update_received(self, xmldoc): def running(self): """Return the running state of the thread.""" try: - return self._thread.isAlive() + return self._thread.is_alive() except (AttributeError, RuntimeError, ThreadError): return False From 77835b06f211fd8cd61b88c414288d472299f492 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2020 05:50:33 +0000 Subject: [PATCH 08/17] Bump isort from 4.3.21 to 5.5.2 Bumps [isort](https://github.com/pycqa/isort) from 4.3.21 to 5.5.2. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/4.3.21...5.5.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5788b22e..9fa705da 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ black==19.10b0 codespell==1.16.0 flake8-docstrings==1.5.0 flake8==3.8.1 -isort==4.3.21 +isort==5.5.2 pydocstyle==5.0.2 pyupgrade==2.3.0 pre-commit>=2.4.0 \ No newline at end of file From f15a169fa72bad49d6f42fd55c81ae6add600092 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 05:53:42 +0000 Subject: [PATCH 09/17] Bump pylint from 2.4.4 to 2.6.0 Bumps [pylint](https://github.com/PyCQA/pylint) from 2.4.4 to 2.6.0. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog) - [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.4.4...pylint-2.6.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9fa705da..b89b78ec 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -pylint==2.4.4 +pylint==2.6.0 pylint-strict-informational==0.1 black==19.10b0 codespell==1.16.0 From 14be663f89d8c9224127ac083a1b32b4452bfea9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2020 09:52:27 +0000 Subject: [PATCH 10/17] Bump pydocstyle from 5.0.2 to 5.1.1 Bumps [pydocstyle](https://github.com/PyCQA/pydocstyle) from 5.0.2 to 5.1.1. - [Release notes](https://github.com/PyCQA/pydocstyle/releases) - [Changelog](https://github.com/PyCQA/pydocstyle/blob/master/docs/release_notes.rst) - [Commits](https://github.com/PyCQA/pydocstyle/compare/5.0.2...5.1.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index b89b78ec..bda2c821 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,6 @@ codespell==1.16.0 flake8-docstrings==1.5.0 flake8==3.8.1 isort==5.5.2 -pydocstyle==5.0.2 +pydocstyle==5.1.1 pyupgrade==2.3.0 pre-commit>=2.4.0 \ No newline at end of file From 6ef137206379da9a4ce8ecdcd9fb8ab29d9ecbd3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2020 09:52:31 +0000 Subject: [PATCH 11/17] Bump flake8 from 3.8.1 to 3.8.3 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.8.1 to 3.8.3. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.8.1...3.8.3) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index bda2c821..65734d71 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ pylint-strict-informational==0.1 black==19.10b0 codespell==1.16.0 flake8-docstrings==1.5.0 -flake8==3.8.1 +flake8==3.8.3 isort==5.5.2 pydocstyle==5.1.1 pyupgrade==2.3.0 From 6d1a62493ed932b03ba970e4f58b958ede718bca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2020 09:55:06 +0000 Subject: [PATCH 12/17] Bump codespell from 1.16.0 to 1.17.1 Bumps [codespell](https://github.com/codespell-project/codespell) from 1.16.0 to 1.17.1. - [Release notes](https://github.com/codespell-project/codespell/releases) - [Commits](https://github.com/codespell-project/codespell/compare/v1.16.0...v1.17.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 65734d71..96910baf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ pylint==2.6.0 pylint-strict-informational==0.1 black==19.10b0 -codespell==1.16.0 +codespell==1.17.1 flake8-docstrings==1.5.0 flake8==3.8.3 isort==5.5.2 From 044f8bc8215fce5f3826f78b9371a1caec026b83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2020 09:55:07 +0000 Subject: [PATCH 13/17] Bump pyupgrade from 2.3.0 to 2.7.2 Bumps [pyupgrade](https://github.com/asottile/pyupgrade) from 2.3.0 to 2.7.2. - [Release notes](https://github.com/asottile/pyupgrade/releases) - [Commits](https://github.com/asottile/pyupgrade/compare/v2.3.0...v2.7.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 96910baf..b804a6e7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,5 +6,5 @@ flake8-docstrings==1.5.0 flake8==3.8.3 isort==5.5.2 pydocstyle==5.1.1 -pyupgrade==2.3.0 +pyupgrade==2.7.2 pre-commit>=2.4.0 \ No newline at end of file From ec42c6bdb3a864194914f2405ade23989a2a4985 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2020 09:56:05 +0000 Subject: [PATCH 14/17] Bump black from 19.10b0 to 20.8b1 Bumps [black](https://github.com/psf/black) from 19.10b0 to 20.8b1. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/master/CHANGES.md) - [Commits](https://github.com/psf/black/commits) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index b804a6e7..3a06626f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ pylint==2.6.0 pylint-strict-informational==0.1 -black==19.10b0 +black==20.8b1 codespell==1.17.1 flake8-docstrings==1.5.0 flake8==3.8.3 From 976324d389028adae69f31e6b8eb712c8f369a74 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 11 Sep 2020 20:14:44 -0500 Subject: [PATCH 15/17] Logging cleanup and consolidation (#106) * Logging cleanup and consolidation * Update connection test --- examples/connection_test.py | 3 +-- pyisy/clock.py | 5 +++-- pyisy/configuration.py | 10 +++------ pyisy/connection.py | 45 +++++++++++++++++++------------------ pyisy/constants.py | 8 ++++++- pyisy/events.py | 31 +++++++++++++------------ pyisy/helpers.py | 9 -------- pyisy/isy.py | 43 ++++++++++++++++++----------------- pyisy/networking.py | 11 +++++---- pyisy/nodes/__init__.py | 27 +++++++++++----------- pyisy/nodes/node.py | 35 +++++++++++++---------------- pyisy/nodes/nodebase.py | 15 ++++++------- pyisy/programs/__init__.py | 9 ++++---- pyisy/programs/folder.py | 7 +++--- pyisy/variables/__init__.py | 11 ++++----- pyisy/variables/variable.py | 5 +++-- 16 files changed, 134 insertions(+), 140 deletions(-) diff --git a/examples/connection_test.py b/examples/connection_test.py index 47197b3b..b3568ce9 100644 --- a/examples/connection_test.py +++ b/examples/connection_test.py @@ -28,6 +28,7 @@ def main(arguments): """Execute primary loop.""" logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, level=LOG_LEVEL) + logging.getLogger("urllib3").setLevel(logging.WARNING) # Test the connection to ISY controller. try: @@ -38,7 +39,6 @@ def main(arguments): password=PASSWORD, use_https=USE_HTTPS, tls_ver=TLS_VER, - log=_LOGGER, webroot=WEBROOT, ) except ValueError as err: @@ -59,7 +59,6 @@ def main(arguments): password=PASSWORD, use_https=USE_HTTPS, tls_ver=TLS_VER, - log=_LOGGER, webroot=WEBROOT, ) diff --git a/pyisy/clock.py b/pyisy/clock.py index 3779de45..b8289481 100644 --- a/pyisy/clock.py +++ b/pyisy/clock.py @@ -3,6 +3,7 @@ from xml.dom import minidom from .constants import ( + _LOGGER, EMPTY_TIME, TAG_DST, TAG_LATITUDE, @@ -81,7 +82,7 @@ def parse(self, xml): try: xmldoc = minidom.parseString(xml) except XML_ERRORS: - self.isy.log.error("%s: Clock", XML_PARSE_ERROR) + _LOGGER.error("%s: Clock", XML_PARSE_ERROR) else: tz_offset_sec = int(value_from_xml(xmldoc, TAG_TZ_OFFSET)) self._tz_offset = tz_offset_sec / 3600 @@ -93,7 +94,7 @@ def parse(self, xml): self._sunrise = ntp_to_system_time(int(value_from_xml(xmldoc, TAG_SUNRISE))) self._sunset = ntp_to_system_time(int(value_from_xml(xmldoc, TAG_SUNSET))) - self.isy.log.info("ISY Loaded Clock Information") + _LOGGER.info("ISY Loaded Clock Information") def update(self, wait_time=0): """ diff --git a/pyisy/configuration.py b/pyisy/configuration.py index 7dfd51a6..9c550419 100755 --- a/pyisy/configuration.py +++ b/pyisy/configuration.py @@ -2,6 +2,7 @@ from xml.dom import minidom from .constants import ( + _LOGGER, ATTR_DESC, ATTR_ID, TAG_DESC, @@ -62,20 +63,15 @@ class Configuration(dict): # configuration['21040'] True - ATTRIBUTES: - log: The logger to use. - """ - def __init__(self, log, xml=None): + def __init__(self, xml=None): """ Initialize configuration class. - log: logger to use xml: String of xml data containing the configuration data """ super().__init__() - self.log = log if xml is not None: self.parse(xml) @@ -104,4 +100,4 @@ def parse(self, xml): self[idnum] = installed self[desc] = self[idnum] - self.log.info("ISY Loaded Configuration") + _LOGGER.info("ISY Loaded Configuration") diff --git a/pyisy/connection.py b/pyisy/connection.py index 65a8a0d1..c1081222 100644 --- a/pyisy/connection.py +++ b/pyisy/connection.py @@ -13,6 +13,10 @@ from urllib3.poolmanager import PoolManager from .constants import ( + _LOGGER, + LOG_DATE_FORMAT, + LOG_FORMAT, + LOG_LEVEL, METHOD_GET, URL_CLOCK, URL_CONFIG, @@ -31,7 +35,6 @@ XML_FALSE, XML_TRUE, ) -from .helpers import NullHandler MAX_RETRIES = 5 @@ -47,15 +50,15 @@ def __init__( password, use_https=False, tls_ver=1.1, - log=None, webroot="", ): """Initialize the Connection object.""" - if log is None: - self.log = logging.getLogger(__name__) - self.log.addHandler(NullHandler()) - else: - self.log = log + if not len(_LOGGER.handlers): + logging.basicConfig( + format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, level=LOG_LEVEL + ) + _LOGGER.addHandler(logging.NullHandler()) + logging.getLogger("urllib3").setLevel(logging.WARNING) self._address = address self._port = port @@ -67,7 +70,7 @@ def __init__( # setup proper HTTPS handling for the ISY if use_https: - if can_https(self.log, tls_ver): + if can_https(tls_ver): self.use_https = True self._tls_ver = tls_ver # Most SSL certs will not be valid. Let's not warn about them. @@ -129,32 +132,32 @@ def compile_url(self, path, query=None): def request(self, url, retries=0, ok404=False): """Execute request to ISY REST interface.""" - if self.log is not None: - self.log.info("ISY Request: %s", url) + if _LOGGER is not None: + _LOGGER.info("ISY Request: %s", url) try: req = self.req_session.get( url, auth=(self._username, self._password), timeout=10, verify=False ) except requests.ConnectionError: - self.log.error( + _LOGGER.error( "ISY Could not receive response " "from device because of a network " "issue." ) return None except requests.exceptions.Timeout: - self.log.error("Timed out waiting for response from the ISY device.") + _LOGGER.error("Timed out waiting for response from the ISY device.") return None if req.status_code == 200: - self.log.debug("ISY Response Received") + _LOGGER.debug("ISY Response Received") return req.text if req.status_code == 404 and ok404: - self.log.debug("ISY Response Received") + _LOGGER.debug("ISY Response Received") return "" - self.log.warning( + _LOGGER.warning( "Bad ISY Request: %s %s: retry #%s", url, req.status_code, retries ) @@ -164,7 +167,7 @@ def request(self, url, retries=0, ok404=False): # recurse to try again return self.request(url, retries + 1, ok404=False) # fail for good - self.log.error( + _LOGGER.error( "Bad ISY Request: %s %s: Failed after %s retries", url, req.status_code, @@ -261,24 +264,22 @@ def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs): ) -def can_https(log, tls_ver): +def can_https(tls_ver): """ Verify minimum requirements to use an HTTPS connection. Returns boolean indicating whether HTTPS is available. - - | log: The logger class to write results to """ output = True # check python version if sys.version_info < (3, 7): - log.error("PyISY cannot use HTTPS: Invalid Python version. See docs.") + _LOGGER.error("PyISY cannot use HTTPS: Invalid Python version. See docs.") output = False # check that Python was compiled against correct OpenSSL lib if "PROTOCOL_TLSv1_1" not in dir(ssl): - log.error( + _LOGGER.error( "PyISY cannot use HTTPS: Compiled against old OpenSSL " + "library. See docs." ) @@ -286,7 +287,7 @@ def can_https(log, tls_ver): # check the requested TLS version if tls_ver not in [1.1, 1.2]: - log.error( + _LOGGER.error( "PyISY cannot use HTTPS: Only TLS 1.1 and 1.2 are supported " + "by the ISY controller." ) diff --git a/pyisy/constants.py b/pyisy/constants.py index 16d9a7e8..6c95d70b 100644 --- a/pyisy/constants.py +++ b/pyisy/constants.py @@ -1,9 +1,15 @@ """Constants for the PyISY Module.""" import datetime +import logging from xml.parsers.expat import ExpatError +_LOGGER = logging.getLogger(__package__) +LOG_LEVEL = logging.DEBUG +LOG_VERBOSE = 5 +LOG_FORMAT = "%(asctime)s %(levelname)s [%(name)s] %(message)s" +LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + UPDATE_INTERVAL = 0.5 -VERBOSE = 5 # Verbose Logging Level # Time Constants / Strings diff --git a/pyisy/events.py b/pyisy/events.py index c4872df8..3d43eb11 100644 --- a/pyisy/events.py +++ b/pyisy/events.py @@ -1,4 +1,5 @@ """ISY Event Stream.""" +import logging import socket import ssl from threading import Thread, ThreadError @@ -18,15 +19,17 @@ ES_INITIALIZING, ES_LOADED, ES_LOST_STREAM_CONNECTION, + LOG_VERBOSE, POLL_TIME, PROP_STATUS, RECONNECT_DELAY, TAG_NODE, - VERBOSE, ) from .eventreader import ISYEventReader, ISYMaxConnections, ISYStreamDataError from .helpers import attr_from_xml, now, value_from_xml +_LOGGER = logging.getLogger(__name__) # Allows targeting pyisy.events in handlers. + class EventStream: """Class to represent the Event Stream from the ISY.""" @@ -75,9 +78,9 @@ def _route_message(self, msg): try: xmldoc = minidom.parseString(msg) except xml.parsers.expat.ExpatError: - self.isy.log.warning("ISY Received Malformed XML:\n" + msg) + _LOGGER.warning("ISY Received Malformed XML:\n" + msg) return - self.isy.log.log(VERBOSE, "ISY Update Received:\n" + msg) + _LOGGER.log(LOG_VERBOSE, "ISY Update Received:\n" + msg) # A wild stream id appears! if f"{ATTR_STREAM_ID}=" in msg and ATTR_STREAM_ID not in self.data: @@ -96,7 +99,7 @@ def _route_message(self, msg): self.isy.connection_events.notify(ES_LOADED) self._lasthb = now() self._hbwait = int(value_from_xml(xmldoc, ATTR_ACTION)) - self.isy.log.debug("ISY HEARTBEAT: %s", self._lasthb.isoformat()) + _LOGGER.debug("ISY HEARTBEAT: %s", self._lasthb.isoformat()) elif cntrl == PROP_STATUS: # NODE UPDATE self.isy.nodes.update_received(xmldoc) elif cntrl[0] != "_": # NODE CONTROL EVENT @@ -117,7 +120,7 @@ def _route_message(self, msg): def update_received(self, xmldoc): """Set the socket ID.""" self.data[ATTR_STREAM_ID] = attr_from_xml(xmldoc, "Event", ATTR_STREAM_ID) - self.isy.log.debug("ISY Updated Events Stream ID") + _LOGGER.debug("ISY Updated Events Stream ID") @property def running(self): @@ -130,7 +133,7 @@ def running(self): @running.setter def running(self, val): if val and not self.running: - self.isy.log.info("ISY Starting Updates") + _LOGGER.info("ISY Starting Updates") if self.connect(): self.subscribe() self._running = True @@ -138,7 +141,7 @@ def running(self, val): self._thread.daemon = True self._thread.start() else: - self.isy.log.info("ISY Stopping Updates") + _LOGGER.info("ISY Stopping Updates") self._running = False self.unsubscribe() self.disconnect() @@ -158,7 +161,7 @@ def connect(self): if self.data.get("tls"): self.cert = self.socket.getpeercert() except OSError: - self.isy.log.error("PyISY could not connect to ISY event stream.") + _LOGGER.error("PyISY could not connect to ISY event stream.") if self._on_lost_function is not None: self._on_lost_function() return False @@ -212,7 +215,7 @@ def heartbeat_time(self): def _lost_connection(self, delay=0): """React when the event stream connection is lost.""" self.disconnect() - self.isy.log.warning("PyISY lost connection to the ISY event stream.") + _LOGGER.warning("PyISY lost connection to the ISY event stream.") self.isy.connection_events.notify(ES_LOST_STREAM_CONNECTION) if self._on_lost_function is not None: time.sleep(delay) @@ -221,7 +224,7 @@ def _lost_connection(self, delay=0): def watch(self): """Watch the subscription connection and report if dead.""" if not self._subscribed: - self.isy.log.debug("PyISY watch called without a subscription.") + _LOGGER.debug("PyISY watch called without a subscription.") return event_reader = ISYEventReader(self.socket) @@ -235,20 +238,20 @@ def watch(self): try: events = event_reader.read_events(POLL_TIME) except ISYMaxConnections: - self.isy.log.error( + _LOGGER.error( "PyISY reached maximum connections, delaying reconnect attempt by %s seconds.", RECONNECT_DELAY, ) self._lost_connection(RECONNECT_DELAY) return except ISYStreamDataError as ex: - self.isy.log.warning( + _LOGGER.warning( "PyISY encountered an error while reading the event stream: %s.", ex ) self._lost_connection() return except OSError as ex: - self.isy.log.warning( + _LOGGER.warning( "PyISY encountered a socket error while reading the event stream: %s.", ex, ) @@ -259,7 +262,7 @@ def watch(self): try: self._route_message(message) except Exception as ex: # pylint: disable=broad-except - self.isy.log.warning( + _LOGGER.warning( "PyISY encountered while routing message '%s': %s", message, ex ) diff --git a/pyisy/helpers.py b/pyisy/helpers.py index 9390ddd3..08b99357 100644 --- a/pyisy/helpers.py +++ b/pyisy/helpers.py @@ -1,6 +1,5 @@ """Helper functions for the PyISY Module.""" import datetime -from logging import Handler import time from .constants import ( @@ -263,14 +262,6 @@ def __setattr__(self, name, value): self[name] = value -class NullHandler(Handler): - """NullHandler Logging Class Override.""" - - def emit(self, record): - """Override the Emit function.""" - pass - - class ZWaveProperties(dict): """Class to hold Z-Wave Product Details from a Z-Wave Node.""" diff --git a/pyisy/isy.py b/pyisy/isy.py index 3185af99..d81f66fc 100755 --- a/pyisy/isy.py +++ b/pyisy/isy.py @@ -6,16 +6,20 @@ from .configuration import Configuration from .connection import Connection from .constants import ( + _LOGGER, CMD_X10, ES_RECONNECT_FAILED, ES_RECONNECTING, ES_START_UPDATES, ES_STOP_UPDATES, + LOG_DATE_FORMAT, + LOG_FORMAT, + LOG_LEVEL, URL_QUERY, X10_COMMANDS, ) from .events import EventStream -from .helpers import EventEmitter, NullHandler +from .helpers import EventEmitter from .networking import NetworkResources from .nodes import Nodes from .programs import Programs @@ -33,7 +37,6 @@ class ISY: | use_https: [optional] Boolean of whether secured HTTP should be used | tls_ver: [optional] Number indicating the version of TLS encryption to use. Valid options are 1.1 or 1.2. - | log: [optional] Log file class from logging module :ivar auto_reconnect: Boolean value that indicates if the class should auto-reconnect to the event stream if the connection @@ -43,7 +46,6 @@ class ISY: values to be updated automatically. :ivar connected: Read only boolean value indicating if the class is connected to the controller. - :ivar log: Logger used by the class and its children. :ivar nodes: :class:`pyisy.nodes.Nodes` manager that interacts with Insteon nodes and groups. :ivar programs: Program manager that interacts with ISY programs and i @@ -61,33 +63,34 @@ def __init__( password, use_https=False, tls_ver=1.1, - log=None, webroot="", ): """Initialize the primary ISY Class.""" self._events = None # create this JIT so no socket reuse self._reconnect_thread = None - self.log = log - if log is None: - self.log = logging.getLogger(__name__) - self.log.addHandler(NullHandler()) + if not len(_LOGGER.handlers): + logging.basicConfig( + format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, level=LOG_LEVEL + ) + _LOGGER.addHandler(logging.NullHandler()) + logging.getLogger("urllib3").setLevel(logging.WARNING) try: self.conn = Connection( - address, port, username, password, use_https, tls_ver, self.log, webroot + address, port, username, password, use_https, tls_ver, webroot ) except ValueError as err: self._connected = False try: - self.log.error(err.message) + _LOGGER.error(err.message) except AttributeError: - self.log.error(err.args[0]) + _LOGGER.error(err.args[0]) return self._hostname = address self._connected = True - self.configuration = Configuration(self.log, xml=self.conn.get_config()) + self.configuration = Configuration(xml=self.conn.get_config()) self.clock = Clock(self, xml=self.conn.get_time()) self.nodes = Nodes(self, xml=self.conn.get_nodes()) self.programs = Programs(self, xml=self.conn.get_programs()) @@ -153,7 +156,7 @@ def _on_lost_event_stream(self): def _auto_reconnecter(self): """Auto-reconnect to the event stream.""" while self.auto_reconnect and not self.auto_update: - self.log.warning("PyISY attempting stream reconnect.") + _LOGGER.warning("PyISY attempting stream reconnect.") del self._events self._events = EventStream( self, self.conn.connection_info, self._on_lost_event_stream @@ -164,10 +167,10 @@ def _auto_reconnecter(self): if not self.auto_update: del self._events self._events = None - self.log.warning("PyISY could not reconnect to the event stream.") + _LOGGER.warning("PyISY could not reconnect to the event stream.") self.connection_events.notify(ES_RECONNECT_FAILED) else: - self.log.warning("PyISY reconnected to the event stream.") + _LOGGER.warning("PyISY reconnected to the event stream.") self._reconnect_thread = None @@ -178,9 +181,9 @@ def query(self, address=None): req_path.append(address) req_url = self.conn.compile_url(req_path) if not self.conn.request(req_url): - self.log.warning("Error performing query.") + _LOGGER.warning("Error performing query.") return False - self.log.debug("ISY Query requested successfully.") + _LOGGER.debug("ISY Query requested successfully.") def send_x10_cmd(self, address, cmd): """ @@ -194,8 +197,6 @@ def send_x10_cmd(self, address, cmd): req_url = self.conn.compile_url([CMD_X10, address, str(command)]) result = self.conn.request(req_url) if result is not None: - self.log.info("ISY Sent X10 Command: %s To: %s", cmd, address) + _LOGGER.info("ISY Sent X10 Command: %s To: %s", cmd, address) else: - self.log.error( - "ISY Failed to send X10 Command: %s To: %s", cmd, address - ) + _LOGGER.error("ISY Failed to send X10 Command: %s To: %s", cmd, address) diff --git a/pyisy/networking.py b/pyisy/networking.py index 4b7478f8..cdd4aa20 100755 --- a/pyisy/networking.py +++ b/pyisy/networking.py @@ -3,6 +3,7 @@ from xml.dom import minidom from .constants import ( + _LOGGER, ATTR_ID, TAG_NAME, TAG_NET_RULE, @@ -65,7 +66,7 @@ def parse(self, xml): try: xmldoc = minidom.parseString(xml) except XML_ERRORS: - self.isy.log.error("%s: NetworkResources", XML_PARSE_ERROR) + _LOGGER.error("%s: NetworkResources", XML_PARSE_ERROR) else: features = xmldoc.getElementsByTagName(TAG_NET_RULE) for feature in features: @@ -77,7 +78,7 @@ def parse(self, xml): self.nnames.append(nname) self.nobjs.append(nobj) - self.isy.log.info("ISY Loaded Network Resources Commands") + _LOGGER.info("ISY Loaded Network Resources Commands") def update(self, wait_time=0): """ @@ -175,8 +176,6 @@ def run(self): req_url = self.isy.conn.compile_url([URL_NETWORK, URL_RESOURCES, str(self._id)]) if not self.isy.conn.request(req_url, ok404=True): - self.isy.log.warning( - "ISY could not run networking command: %s", str(self._id) - ) + _LOGGER.warning("ISY could not run networking command: %s", str(self._id)) return - self.isy.log.debug("ISY ran networking command: %s", str(self._id)) + _LOGGER.debug("ISY ran networking command: %s", str(self._id)) diff --git a/pyisy/nodes/__init__.py b/pyisy/nodes/__init__.py index c119ef1b..c5ac94f0 100755 --- a/pyisy/nodes/__init__.py +++ b/pyisy/nodes/__init__.py @@ -3,6 +3,7 @@ from xml.dom import minidom from ..constants import ( + _LOGGER, ATTR_ACTION, ATTR_CONTROL, ATTR_FLAG, @@ -194,7 +195,7 @@ def update_received(self, xmldoc): node = self.get_by_id(address) if not node: - self.isy.log.debug( + _LOGGER.debug( "Received a node update for node %s but could not find a record of this " "node. Please try restarting the module if the problem persists, this " "may be due to a new node being added to the ISY since last restart.", @@ -211,7 +212,7 @@ def update_received(self, xmldoc): node.update_state( NodeProperty(PROP_STATUS, value, prec, uom, formatted, address) ) - self.isy.log.debug("ISY Updated Node: " + address) + _LOGGER.debug("ISY Updated Node: " + address) def control_message_received(self, xmldoc): """ @@ -227,7 +228,7 @@ def control_message_received(self, xmldoc): node = self.get_by_id(address) if not node: - self.isy.log.debug( + _LOGGER.debug( "Received a node update for node %s but could not find a record of this " "node. Please try restarting the module if the problem persists, this " "may be due to a new node being added to the ISY since last restart.", @@ -257,7 +258,7 @@ def control_message_received(self, xmldoc): elif cntrl not in EVENT_PROPS_IGNORED: node.update_property(node_property) node.control_events.notify(node_property) - self.isy.log.debug("ISY Node Control Event: %s", node_property) + _LOGGER.debug("ISY Node Control Event: %s", node_property) def node_changed_received(self, xmldoc): """Handle Node Change/Update events from an event stream message.""" @@ -266,7 +267,7 @@ def node_changed_received(self, xmldoc): return node = value_from_xml(xmldoc, TAG_NODE) if action == NC_NODE_ERROR: - self.isy.log.warning("ISY Could not communicate with device: %s", node) + _LOGGER.warning("ISY Could not communicate with device: %s", node) # FUTURE: Handle additional node change actions to force updates. def parse(self, xml): @@ -278,7 +279,7 @@ def parse(self, xml): try: xmldoc = minidom.parseString(xml) except XML_ERRORS: - self.isy.log.error("%s: Nodes", XML_PARSE_ERROR) + _LOGGER.error("%s: Nodes", XML_PARSE_ERROR) return False # get nodes @@ -353,9 +354,7 @@ def parse(self, xml): # the ISY MAC address in newer versions of # ISY firmwares > 5.0.6+ .. if int(flag) & 0x08: - self.isy.log.debug( - "Skipping root group flag=%s %s", flag, address - ) + _LOGGER.debug("Skipping root group flag=%s %s", flag, address) continue mems = feature.getElementsByTagName(TAG_LINK) # Build list of members @@ -380,7 +379,7 @@ def parse(self, xml): ), ntype, ) - self.isy.log.debug("ISY Loaded %s", ntype) + _LOGGER.debug("ISY Loaded %s", ntype) def update(self, wait_time=0): """ @@ -395,13 +394,13 @@ def update(self, wait_time=0): xml = self.isy.conn.get_status() if xml is None: - self.isy.log.warning("ISY Failed to update nodes.") + _LOGGER.warning("ISY Failed to update nodes.") return try: xmldoc = minidom.parseString(xml) except XML_ERRORS: - self.isy.log.error("%s: Nodes", XML_PARSE_ERROR) + _LOGGER.error("%s: Nodes", XML_PARSE_ERROR) return False for feature in xmldoc.getElementsByTagName(TAG_NODE): @@ -411,7 +410,7 @@ def update(self, wait_time=0): self.get_by_id(address).update(xmldoc=feature) continue - self.isy.log.info("ISY Updated Node Statuses.") + _LOGGER.info("ISY Updated Node Statuses.") def update_nodes(self, wait_time=0): """ @@ -425,7 +424,7 @@ def update_nodes(self, wait_time=0): sleep(wait_time) xml = self.isy.conn.get_nodes() if xml is None: - self.isy.log.warning("ISY Failed to update nodes.") + _LOGGER.warning("ISY Failed to update nodes.") return self.parse(xml) diff --git a/pyisy/nodes/node.py b/pyisy/nodes/node.py index d237432e..e023f443 100755 --- a/pyisy/nodes/node.py +++ b/pyisy/nodes/node.py @@ -4,6 +4,7 @@ from xml.dom import minidom from ..constants import ( + _LOGGER, CLIMATE_SETPOINT_MIN_GAP, CMD_CLIMATE_FAN_SETTING, CMD_CLIMATE_MODE, @@ -200,28 +201,28 @@ def update(self, event=None, wait_time=0, hint=None, xmldoc=None): try: xmldoc = minidom.parseString(xml) except XML_ERRORS: - self.isy.log.error("%s: Nodes", XML_PARSE_ERROR) + _LOGGER.error("%s: Nodes", XML_PARSE_ERROR) return elif hint is not None: # assume value was set correctly, auto update will correct errors self.status = hint - self.isy.log.debug("ISY updated node: %s", self._id) + _LOGGER.debug("ISY updated node: %s", self._id) return if xmldoc is None: - self.isy.log.warning("ISY could not update node: %s", self._id) + _LOGGER.warning("ISY could not update node: %s", self._id) return self._last_update = now() state, aux_props = parse_xml_properties(xmldoc) self._aux_properties.update(aux_props) self.update_state(state) - self.isy.log.debug("ISY updated node: %s", self._id) + _LOGGER.debug("ISY updated node: %s", self._id) def update_state(self, state): """Update the various state properties when received.""" if not isinstance(state, NodeProperty): - self.isy.log.error("Could not update state values. Invalid type provided.") + _LOGGER.error("Could not update state values. Invalid type provided.") return changed = False self._last_update = now() @@ -250,7 +251,7 @@ def update_state(self, state): def get_command_value(self, uom, cmd): """Check against the list of UOM States if this is a valid command.""" if cmd not in UOM_TO_STATES[uom].values(): - self.isy.log.warning( + _LOGGER.warning( "Failed to call %s on %s, invalid command.", cmd, self.address ) return None @@ -286,25 +287,21 @@ def get_property_uom(self, prop): def secure_lock(self): """Send a command to securely lock a lock device.""" if not self.is_lock: - self.isy.log.warning( - "Failed to lock %s, it is not a lock node.", self.address - ) + _LOGGER.warning("Failed to lock %s, it is not a lock node.", self.address) return return self.send_cmd(CMD_SECURE, "1") def secure_unlock(self): """Send a command to securely lock a lock device.""" if not self.is_lock: - self.isy.log.warning( - "Failed to unlock %s, it is not a lock node.", self.address - ) + _LOGGER.warning("Failed to unlock %s, it is not a lock node.", self.address) return return self.send_cmd(CMD_SECURE, "0") def set_climate_mode(self, cmd): """Send a command to the device to set the climate mode.""" if not self.is_thermostat: - self.isy.log.warning( + _LOGGER.warning( "Failed to set setpoint on %s, it is not a thermostat node.", self.address, ) @@ -316,7 +313,7 @@ def set_climate_mode(self, cmd): def set_climate_setpoint(self, val): """Send a command to the device to set the system setpoints.""" if not self.is_thermostat: - self.isy.log.warning( + _LOGGER.warning( "Failed to set setpoint on %s, it is not a thermostat node.", self.address, ) @@ -337,7 +334,7 @@ def set_climate_setpoint_cool(self, val): def _set_climate_setpoint(self, val, setpoint_name, setpoint_prop): """Send a command to the device to set the system heat setpoint.""" if not self.is_thermostat: - self.isy.log.warning( + _LOGGER.warning( "Failed to set %s setpoint on %s, it is not a thermostat node.", setpoint_name, self.address, @@ -360,7 +357,7 @@ def set_fan_mode(self, cmd): def set_on_level(self, val): """Set the ON Level for a device.""" if not val or isnan(val) or int(val) not in range(256): - self.isy.log.warning( + _LOGGER.warning( "Invalid value for On Level for %s. Valid values are 0-255.", self._id ) return False @@ -369,7 +366,7 @@ def set_on_level(self, val): def set_ramp_rate(self, val): """Set the Ramp Rate for a device.""" if not val or isnan(val) or int(val) not in range(32): - self.isy.log.warning( + _LOGGER.warning( "Invalid value for Ramp Rate for %s. " "Valid values are 0-31. See 'INSTEON_RAMP_RATES' in constants.py for values.", self._id, @@ -379,14 +376,14 @@ def set_ramp_rate(self, val): def start_manual_dimming(self): """Begin manually dimming a device.""" - self.isy.log.warning( + _LOGGER.warning( f"'{CMD_MANUAL_DIM_BEGIN}' is depreciated. Use Fade Commands instead." ) return self.send_cmd(CMD_MANUAL_DIM_BEGIN) def stop_manual_dimming(self): """Stop manually dimming a device.""" - self.isy.log.warning( + _LOGGER.warning( f"'{CMD_MANUAL_DIM_STOP}' is depreciated. Use Fade Commands instead." ) return self.send_cmd(CMD_MANUAL_DIM_STOP) diff --git a/pyisy/nodes/nodebase.py b/pyisy/nodes/nodebase.py index ab4d3b80..c4f62be4 100755 --- a/pyisy/nodes/nodebase.py +++ b/pyisy/nodes/nodebase.py @@ -2,6 +2,7 @@ from xml.dom import minidom from ..constants import ( + _LOGGER, ATTR_LAST_CHANGED, ATTR_LAST_UPDATE, ATTR_STATUS, @@ -178,7 +179,7 @@ def parse_notes(self): try: notesdom = minidom.parseString(notes_xml) except XML_ERRORS: - self.isy.log.error("%s: Node Notes %s", XML_PARSE_ERROR, notes_xml) + _LOGGER.error("%s: Node Notes %s", XML_PARSE_ERROR, notes_xml) else: spoken = value_from_xml(notesdom, TAG_SPOKEN) location = value_from_xml(notesdom, TAG_LOCATION) @@ -198,9 +199,7 @@ def update(self, event=None, wait_time=0, hint=None, xmldoc=None): def update_property(self, prop): """Update an aux property for the node when received.""" if not isinstance(prop, NodeProperty): - self.isy.log.error( - "Could not update property value. Invalid type provided." - ) + _LOGGER.error("Could not update property value. Invalid type provided.") return self.update_last_update() @@ -238,13 +237,13 @@ def send_cmd(self, cmd, val=None, uom=None, query=None): req.append(_uom) req_url = self.isy.conn.compile_url(req, query) if not self.isy.conn.request(req_url): - self.isy.log.warning( + _LOGGER.warning( "ISY could not send %s command to %s.", COMMAND_FRIENDLY_NAME.get(cmd), self._id, ) return False - self.isy.log.debug( + _LOGGER.debug( "ISY command %s sent to %s.", COMMAND_FRIENDLY_NAME.get(cmd), self._id ) @@ -281,7 +280,7 @@ def disable(self): if not self.isy.conn.request( self.isy.conn.compile_url([URL_NODES, str(self._id), CMD_DISABLE]) ): - self.isy.log.warning("ISY could not %s %s.", CMD_DISABLE, self._id) + _LOGGER.warning("ISY could not %s %s.", CMD_DISABLE, self._id) return False return True @@ -290,7 +289,7 @@ def enable(self): if not self.isy.conn.request( self.isy.conn.compile_url([URL_NODES, str(self._id), CMD_ENABLE]) ): - self.isy.log.warning("ISY could not %s %s.", CMD_ENABLE, self._id) + _LOGGER.warning("ISY could not %s %s.", CMD_ENABLE, self._id) return False return True diff --git a/pyisy/programs/__init__.py b/pyisy/programs/__init__.py index ddc42cee..c2856dbf 100755 --- a/pyisy/programs/__init__.py +++ b/pyisy/programs/__init__.py @@ -5,6 +5,7 @@ from dateutil import parser from ..constants import ( + _LOGGER, ATTR_ID, ATTR_PARENT, ATTR_STATUS, @@ -181,7 +182,7 @@ def update_received(self, xmldoc): # Status didn't change, but something did, so fire the event. pobj.status_events.notify(new_status) - self.isy.log.debug("ISY Updated Program: " + address) + _LOGGER.debug("ISY Updated Program: " + address) def parse(self, xml): """ @@ -192,7 +193,7 @@ def parse(self, xml): try: xmldoc = minidom.parseString(xml) except XML_ERRORS: - self.isy.log.error("%s: Programs", XML_PARSE_ERROR) + _LOGGER.error("%s: Programs", XML_PARSE_ERROR) else: plastup = now() @@ -255,7 +256,7 @@ def parse(self, xml): pobj = self.get_by_id(address).leaf pobj.update(data=data) - self.isy.log.info("ISY Loaded/Updated Programs") + _LOGGER.info("ISY Loaded/Updated Programs") def update(self, wait_time=UPDATE_INTERVAL, address=None): """ @@ -270,7 +271,7 @@ def update(self, wait_time=UPDATE_INTERVAL, address=None): if xml is not None: self.parse(xml) else: - self.isy.log.warning("ISY Failed to update programs.") + _LOGGER.warning("ISY Failed to update programs.") def insert(self, address, pname, pparent, pobj, ptype): """ diff --git a/pyisy/programs/folder.py b/pyisy/programs/folder.py index 9b4b4a94..a487c128 100644 --- a/pyisy/programs/folder.py +++ b/pyisy/programs/folder.py @@ -1,5 +1,6 @@ """ISY Program Folders.""" from ..constants import ( + _LOGGER, ATTR_LAST_CHANGED, ATTR_LAST_UPDATE, ATTR_STATUS, @@ -134,11 +135,9 @@ def send_cmd(self, command): req_url = self.isy.conn.compile_url([URL_PROGRAMS, str(self._id), command]) result = self.isy.conn.request(req_url) if not result: - self.isy.log.warning( - 'ISY could not call "%s" on program: %s', command, self._id - ) + _LOGGER.warning('ISY could not call "%s" on program: %s', command, self._id) return False - self.isy.log.debug('ISY ran "%s" on program: %s', command, self._id) + _LOGGER.debug('ISY ran "%s" on program: %s', command, self._id) if not self.isy.auto_update: self.update() return True diff --git a/pyisy/variables/__init__.py b/pyisy/variables/__init__.py index d7f45e50..2ba37401 100644 --- a/pyisy/variables/__init__.py +++ b/pyisy/variables/__init__.py @@ -5,6 +5,7 @@ from dateutil import parser from ..constants import ( + _LOGGER, ATTR_ID, ATTR_INIT, ATTR_TS, @@ -99,7 +100,7 @@ def parse_definitions(self, xmls): try: xmldoc = minidom.parseString(xmls[ind]) except XML_ERRORS: - self.isy.log.error("%s: Type %s Variables", XML_PARSE_ERROR, ind + 1) + _LOGGER.error("%s: Type %s Variables", XML_PARSE_ERROR, ind + 1) continue features = xmldoc.getElementsByTagName(TAG_VARIABLE) @@ -112,7 +113,7 @@ def parse(self, xml): try: xmldoc = minidom.parseString(xml) except XML_ERRORS: - self.isy.log.error("%s: Variables", XML_PARSE_ERROR) + _LOGGER.error("%s: Variables", XML_PARSE_ERROR) return features = xmldoc.getElementsByTagName(ATTR_VAR) @@ -135,7 +136,7 @@ def parse(self, xml): vobj.status = val vobj.last_edited = t_s - self.isy.log.info("ISY Loaded Variables") + _LOGGER.info("ISY Loaded Variables") def update(self, wait_time=0): """ @@ -148,7 +149,7 @@ def update(self, wait_time=0): if xml is not None: self.parse(xml) else: - self.isy.log.warning("ISY Failed to update variables.") + _LOGGER.warning("ISY Failed to update variables.") def update_received(self, xmldoc): """Process an update received from the event stream.""" @@ -167,7 +168,7 @@ def update_received(self, xmldoc): vobj.status = int(value_from_xml(xmldoc, ATTR_VAL)) vobj.last_edited = parser.parse(value_from_xml(xmldoc, ATTR_TS)) - self.isy.log.debug("ISY Updated Variable: %s.%s", str(vtype), str(vid)) + _LOGGER.debug("ISY Updated Variable: %s.%s", str(vtype), str(vid)) def __getitem__(self, val): """ diff --git a/pyisy/variables/variable.py b/pyisy/variables/variable.py index b6d995cc..0eedf7e6 100644 --- a/pyisy/variables/variable.py +++ b/pyisy/variables/variable.py @@ -1,5 +1,6 @@ """Manage variables from the ISY.""" from ..constants import ( + _LOGGER, ATTR_INIT, ATTR_LAST_CHANGED, ATTR_LAST_UPDATE, @@ -183,14 +184,14 @@ def set_value(self, val, init=False): ] ) if not self.isy.conn.request(req_url): - self.isy.log.warning( + _LOGGER.warning( "ISY could not set variable%s: %s.%s", " init value" if init else "", str(self._type), str(self._id), ) return - self.isy.log.debug( + _LOGGER.debug( "ISY set variable%s: %s.%s", " init value" if init else "", str(self._type), From ef858508464692d0194a7e0dc812ff81957836fc Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 11 Sep 2020 20:19:07 -0500 Subject: [PATCH 16/17] Revise Node.dimmable property to exclude non-dimmable subnodes (#122) Node.dimmable has been depreciated in favor of Node.is_dimmable to make the naming more consistent with is_lock and is_thermostat. Node.dimmable will still work, however, plan for it to be removed in the future. Node.is_dimmable will only include the first subnode for Insteon devices in type 1. This should represent the main (load) button for KeypadLincs and the light for FanLincs, all other subnodes (KPL buttons and Fan Motor) are not dimmable (fixes #110) Revise is_lock, is_thermostat, and is_lock properties of the Node class to be more consistent. Use string literals for special Insteon types --- pyisy/constants.py | 12 +++++++-- pyisy/nodes/node.py | 60 ++++++++++++++++++++++++++++++++------------- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/pyisy/constants.py b/pyisy/constants.py index 6c95d70b..05b0edf2 100644 --- a/pyisy/constants.py +++ b/pyisy/constants.py @@ -687,8 +687,16 @@ # Thermostat Types/Categories. 4.8 Trane, 5.3 venstar, 5.10 Insteon Wireless, # 5.11 Insteon, 5.17 Insteon (EU), 5.18 Insteon (Aus/NZ) -THERMOSTAT_TYPES = ["4.8", "5.3", "5.10", "5.11", "5.17", "5.18"] -THERMOSTAT_ZWAVE_CAT = ["140"] +INSTEON_TYPE_THERMOSTAT = ["4.8", "5.3", "5.10", "5.11", "5.17", "5.18"] +ZWAVE_CAT_THERMOSTAT = ["140"] + +# Other special categories or types +INSTEON_TYPE_LOCK = ["4.64"] +ZWAVE_CAT_LOCK = ["111"] + +INSTEON_TYPE_DIMMABLE = ["1."] +INSTEON_SUBNODE_DIMMABLE = " 1" +ZWAVE_CAT_DIMMABLE = ["109", "119", "186"] # Referenced from ISY-WSDK 4_fam.xml # Included for user translations in external modules. diff --git a/pyisy/nodes/node.py b/pyisy/nodes/node.py index e023f443..73f17f70 100755 --- a/pyisy/nodes/node.py +++ b/pyisy/nodes/node.py @@ -11,22 +11,28 @@ CMD_MANUAL_DIM_BEGIN, CMD_MANUAL_DIM_STOP, CMD_SECURE, + INSTEON_SUBNODE_DIMMABLE, + INSTEON_TYPE_DIMMABLE, + INSTEON_TYPE_LOCK, + INSTEON_TYPE_THERMOSTAT, METHOD_GET, PROP_ON_LEVEL, PROP_RAMP_RATE, PROP_SETPOINT_COOL, PROP_SETPOINT_HEAT, PROP_STATUS, + PROTO_INSTEON, PROTO_ZWAVE, TAG_GROUP, - THERMOSTAT_TYPES, - THERMOSTAT_ZWAVE_CAT, UOM_CLIMATE_MODES, UOM_FAN_MODES, UOM_TO_STATES, URL_NODES, XML_ERRORS, XML_PARSE_ERROR, + ZWAVE_CAT_DIMMABLE, + ZWAVE_CAT_LOCK, + ZWAVE_CAT_THERMOSTAT, ) from ..helpers import EventEmitter, NodeProperty, now, parse_xml_properties from .nodebase import NodeBase @@ -100,18 +106,10 @@ def dimmable(self): """ Return the best guess if this is a dimmable node. - Check ISYv4 UOM, then Insteon and Z-Wave Types for dimmable types. + DEPRECIATED: USE is_dimmable INSTEAD. Will be removed in future release. """ - dimmable = ( - "%" in str(self._uom) - or (isinstance(self._type, str) and self._type.startswith("1.")) - or ( - self._protocol == PROTO_ZWAVE - and self._zwave_props is not None - and self._zwave_props.category in ["109", "119", "186"] - ) - ) - return dimmable + _LOGGER.info("Node.dimmable is depreciated. Use Node.is_dimmable instead.") + return self.is_dimmable @property def enabled(self): @@ -123,22 +121,50 @@ def formatted(self): """Return the formatted value with units, if provided.""" return self._formatted + @property + def is_dimmable(self): + """ + Return the best guess if this is a dimmable node. + + Check ISYv4 UOM, then Insteon and Z-Wave Types for dimmable types. + """ + dimmable = ( + "%" in str(self._uom) + or ( + self._protocol == PROTO_INSTEON + and self.type + and any([self.type.startswith(t) for t in INSTEON_TYPE_DIMMABLE]) + and self._id.endswith(INSTEON_SUBNODE_DIMMABLE) + ) + or ( + self._protocol == PROTO_ZWAVE + and self._zwave_props is not None + and self._zwave_props.category in ZWAVE_CAT_DIMMABLE + ) + ) + return dimmable + @property def is_lock(self): """Determine if this device is a door lock type.""" - return (self.type and self.type.startswith("4.64")) or ( - self.protocol == PROTO_ZWAVE and self.zwave_props.category == "111" + return ( + self.type and any([self.type.startswith(t) for t in INSTEON_TYPE_LOCK]) + ) or ( + self.protocol == PROTO_ZWAVE + and self.zwave_props.category + and self.zwave_props.category in ZWAVE_CAT_LOCK ) @property def is_thermostat(self): """Determine if this device is a thermostat/climate control device.""" return ( - self.type and any([self.type.startswith(t) for t in THERMOSTAT_TYPES]) + self.type + and any([self.type.startswith(t) for t in INSTEON_TYPE_THERMOSTAT]) ) or ( self._protocol == PROTO_ZWAVE and self.zwave_props.category - and any(self.zwave_props.category in THERMOSTAT_ZWAVE_CAT) + and self.zwave_props.category in ZWAVE_CAT_THERMOSTAT ) @property From a806061d05282afe091e7d813c9788589bc9aaac Mon Sep 17 00:00:00 2001 From: shbatm Date: Thu, 10 Sep 2020 17:32:30 +0000 Subject: [PATCH 17/17] Update CHANGELOG for 2.1.0 draft release --- CHANGELOG.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e5290e..33d05d5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,49 @@ ## CHANGELOG -### [v2.0.3] - Fix Property Updates, Add Timestamps, Unused Status Handling +### [v2.1.0] - Property Updates, Timestamps, Status Handling, and more... + +#### Breaking Changes + +- `Node.dimmable` has been depreciated in favor of `Node.is_dimmable` to make the naming more consistent with `is_lock` and `is_thermostat`. `Node.dimmable` will still work, however, plan for it to be removed in the future. +- `Node.is_dimmable` will only include the first subnode for Insteon devices in type 1. This should represent the main (load) button for KeypadLincs and the light for FanLincs, all other subnodes (KPL buttons and Fan Motor) are not dimmable (fixes #110) +- This removes the `log=` parameter when initializing new `Connection` and `ISY` class instances. Please update any loading functions you may use to remove this `log=` parameter. + #### Changed / Fixed - Changed the default Status Property (`ST`) unit of measurement (UOM) to `ISY_PROP_NOT_SET = "-1"`: Some NodeServer and Z-Wave nodes do not make use of the `ST` (or status) property in the ISY and only report `aux_properties`; in addition, most NodeServer nodes do not report the `ST` property when all nodes are retrieved, they only report it when queried directly or in the Event Stream. Previously, there was no way to differentiate between Insteon Nodes that don't have a valid status yet (after ISY reboot) and the other types of nodes that don't report the property correctly since they both reported `ISY_VALUE_UNKNOWN`. The `ISY_PROP_NOT_SET` allows differentiation between the two conditions based on having a valid UOM or not. Fixes #98. - Rewrite the Node status update receiver: currently, when a Node's status is updated, the `formatted` property is not updated and the `uom`/`prec` are updated with separate functions from outside of the Node's class. This updates the receiver to pass a `NodeProperty` instance into the Node, and allows the Node to update all of it's properties if they've changed, before reporting the status change to the subscribers. This makes the `formatted` property actually useful. +- Logging Cleanup: Removes reliance on `isy` parent objects to provide logger and uses a module-wide `_LOGGER`. Everything will be logged under the `pyisy` namespace except Events. Events maintains a separate logger namespace to allow targeting in handlers of `pyisy.events`. #### Added - Added `*.last_update` and `*.last_changed` properties which are UTC Datetime Timestamps, to allow detection of stale data. Fixes #99 - Add connection events for the Event Stream to allow subscription and callbacks. Attach a callback with `isy.connection_events(callback)` and receive a string with the event detail. See `constants.py` for events starting with prefix `ES_`. +- Add a VSCode Devcontainer based on Python 3.8 +- Update the package requirements to explicitly include dateutil and the dev requirements for pre-commit +- Add pyupgrade hook to pre-commit and run it on the whole repo. + +#### All PRs in this Version: + +- Revise Node.dimmable property to exclude non-dimmable subnodes (#122) +- Logging cleanup and consolidation (#106) +- Fix #109 - Update for events depreciation warning +- Add Devcontainer, Update Requirements, Use PyUpgrade (#105) +- Guard against overwriting known attributes with blanks (#112) +- Minor code cleanups (#104) +- Fix Property Updates, Add Timestamps, Unused Status Handling (#100) +- Fix parameter name (#102) +- Add connection events target (#101) + +#### Dependency Changes: + +- Bump black from 19.10b0 to 20.8b1 +- Bump pyupgrade from 2.3.0 to 2.7.2 +- Bump codespell from 1.16.0 to 1.17.1 +- Bump flake8 from 3.8.1 to 3.8.3 +- Bump pydocstyle from 5.0.2 to 5.1.1 +- Bump pylint from 2.4.4 to 2.6.0 +- Bump isort from 4.3.21 to 5.5.2 ### [v2.0.2] - Version 2.0 Initial Release