Skip to content

Commit

Permalink
Merge branch 'async' into PyISY_beta
Browse files Browse the repository at this point in the history
  • Loading branch information
shbatm committed Sep 12, 2020
2 parents f101c42 + c9507b9 commit 1e815c8
Show file tree
Hide file tree
Showing 13 changed files with 167 additions and 45 deletions.
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
4 changes: 4 additions & 0 deletions .github/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
template: |
## What's Changed
$CHANGES
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 1 addition & 4 deletions .github/workflows/pythonpublish.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
# 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

# Add secrets.PYPI_USERNAME and secrets.PYPI_PASSWORD here:
# https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets

name: Upload Python Package

on:
release:
types: [created]
types: [published]

jobs:
deploy:
Expand Down
35 changes: 34 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,50 @@
- Module can now be used/tested from the command-line with the new `__main__.py` script; you can test a connection with `python3 -m pyisy http://your-isy-url:80 username password`.
- A new helper function has been added to create an `aiohttp.ClientSession` compliant with the ISY: `Connection.get_new_client_session(use_https, tls_ver=1.1)` will return a web session that can be passed to the init functions of `ISY` and `Connection` classes.

### [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

Expand Down
1 change: 1 addition & 0 deletions pyisy/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def __init__(
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
Expand Down
27 changes: 25 additions & 2 deletions pyisy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@
ATTR_VALUE = "value" # Used for everything else.
ATTR_VAR = "var"

DEFAULT_PRECISION = "0"
DEFAULT_UNIT_OF_MEASURE = ""

TAG_ADDRESS = "address"
TAG_CATEGORY = "cat"
TAG_DESC = "desc"
Expand Down Expand Up @@ -134,6 +137,18 @@
PROTO_ZIGBEE = "zigbee"
PROTO_ZWAVE = "zwave"

FAMILY_CORE = 0
FAMILY_INSTEON = 1
FAMILY_UPB = 2
FAMILY_RCS = 3
FAMILY_ZWAVE = 4
FAMILY_AUTO = 5
FAMILY_GENERIC = 6
FAMILY_UDI = 7
FAMILY_BRULTECH = 8
FAMILY_NCD = 9
FAMILY_NODESERVER = 10

PROP_BATTERY_LEVEL = "BATLVL"
PROP_BUSY = "BUSY"
PROP_COMMS_ERROR = "ERR"
Expand Down Expand Up @@ -696,8 +711,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.
Expand Down
10 changes: 6 additions & 4 deletions pyisy/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
ATTR_PRECISION,
ATTR_UNIT_OF_MEASURE,
ATTR_VALUE,
DEFAULT_PRECISION,
DEFAULT_UNIT_OF_MEASURE,
INSTEON_RAMP_RATES,
ISY_EPOCH_OFFSET,
ISY_PROP_NOT_SET,
Expand Down Expand Up @@ -45,9 +47,9 @@ def parse_xml_properties(xmldoc):

for prop in props:
prop_id = attr_from_element(prop, ATTR_ID)
uom = attr_from_element(prop, ATTR_UNIT_OF_MEASURE, "")
uom = attr_from_element(prop, ATTR_UNIT_OF_MEASURE, DEFAULT_UNIT_OF_MEASURE)
value = attr_from_element(prop, ATTR_VALUE, "").strip()
prec = attr_from_element(prop, ATTR_PRECISION, "0")
prec = attr_from_element(prop, ATTR_PRECISION, DEFAULT_PRECISION)
formatted = attr_from_element(prop, ATTR_FORMATTED, value)

# ISY firmwares < 5 return a list of possible units.
Expand Down Expand Up @@ -197,8 +199,8 @@ def __init__(
self,
control,
value=ISY_VALUE_UNKNOWN,
prec="0",
uom="",
prec=DEFAULT_PRECISION,
uom=DEFAULT_UNIT_OF_MEASURE,
formatted=None,
address=None,
):
Expand Down
1 change: 1 addition & 0 deletions pyisy/isy.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def __init__(
format=LOG_FORMAT, datefmt=LOG_DATE_FORMAT, level=LOG_LEVEL
)
_LOGGER.addHandler(logging.NullHandler())
logging.getLogger("urllib3").setLevel(logging.WARNING)

self.conn = Connection(
address=address,
Expand Down
24 changes: 17 additions & 7 deletions pyisy/nodes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
ATTR_NODE_DEF_ID,
ATTR_PRECISION,
ATTR_UNIT_OF_MEASURE,
DEFAULT_PRECISION,
DEFAULT_UNIT_OF_MEASURE,
EVENT_PROPS_IGNORED,
FAMILY_BRULTECH,
FAMILY_NODESERVER,
FAMILY_RCS,
FAMILY_ZWAVE,
INSTEON_RAMP_RATES,
ISY_VALUE_UNKNOWN,
NC_NODE_ERROR,
Expand Down Expand Up @@ -203,8 +209,10 @@ def update_received(self, xmldoc):
return
value = value_from_xml(xmldoc, ATTR_ACTION)
value = int(value) if value != "" else ISY_VALUE_UNKNOWN
prec = attr_from_xml(xmldoc, ATTR_ACTION, ATTR_PRECISION, "0")
uom = attr_from_xml(xmldoc, ATTR_ACTION, ATTR_UNIT_OF_MEASURE, "")
prec = attr_from_xml(xmldoc, ATTR_ACTION, ATTR_PRECISION, DEFAULT_PRECISION)
uom = attr_from_xml(
xmldoc, ATTR_ACTION, ATTR_UNIT_OF_MEASURE, DEFAULT_UNIT_OF_MEASURE
)
formatted = value_from_xml(xmldoc, TAG_FORMATTED)

# Process the action and value if provided in event data.
Expand Down Expand Up @@ -239,8 +247,10 @@ def control_message_received(self, xmldoc):
node.update_last_update()
value = value_from_xml(xmldoc, ATTR_ACTION, 0)
value = int(value) if value != "" else ISY_VALUE_UNKNOWN
prec = attr_from_xml(xmldoc, ATTR_ACTION, ATTR_PRECISION, "0")
uom = attr_from_xml(xmldoc, ATTR_ACTION, ATTR_UNIT_OF_MEASURE, "")
prec = attr_from_xml(xmldoc, ATTR_ACTION, ATTR_PRECISION, DEFAULT_PRECISION)
uom = attr_from_xml(
xmldoc, ATTR_ACTION, ATTR_UNIT_OF_MEASURE, DEFAULT_UNIT_OF_MEASURE
)
formatted = value_from_xml(xmldoc, TAG_FORMATTED)

if cntrl == PROP_RAMP_RATE:
Expand Down Expand Up @@ -302,14 +312,14 @@ def parse(self, xml):
zwave_props = None
node_server = None
if family is not None:
if family == "4":
if family == FAMILY_ZWAVE:
protocol = PROTO_ZWAVE
zwave_props = ZWaveProperties(
feature.getElementsByTagName(TAG_DEVICE_TYPE)[0]
)
elif family in ("3", "8"):
elif family in (FAMILY_BRULTECH, FAMILY_RCS):
protocol = PROTO_ZIGBEE
elif family == "10":
elif family == FAMILY_NODESERVER:
# Node Server Slot is stored with family as text:
node_server = attr_from_xml(feature, TAG_FAMILY, ATTR_INSTANCE)
if node_server:
Expand Down
62 changes: 44 additions & 18 deletions pyisy/nodes/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,26 @@
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,
ZWAVE_CAT_DIMMABLE,
ZWAVE_CAT_LOCK,
ZWAVE_CAT_THERMOSTAT,
)
from ..exceptions import XML_ERRORS, XML_PARSE_ERROR, ISYResponseParseError
from ..helpers import EventEmitter, NodeProperty, now, parse_xml_properties
Expand Down Expand Up @@ -99,18 +105,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):
Expand All @@ -122,22 +120,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
Expand Down Expand Up @@ -230,7 +256,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

Expand Down
8 changes: 6 additions & 2 deletions pyisy/nodes/nodebase.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,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)
Expand Down
14 changes: 7 additions & 7 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
pylint==2.4.4
pylint==2.6.0
pylint-strict-informational==0.1
black==19.10b0
codespell==1.16.0
black==20.8b1
codespell==1.17.1
flake8-docstrings==1.5.0
flake8==3.8.1
isort==4.3.21
pydocstyle==5.0.2
pyupgrade==2.3.0
flake8==3.8.3
isort==5.5.2
pydocstyle==5.1.1
pyupgrade==2.7.2
pre-commit>=2.4.0

0 comments on commit 1e815c8

Please sign in to comment.