From e0efc9243760f72f17b6befa8aae6c281bdde789 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:46:11 -0700 Subject: [PATCH 01/53] Make set_scg_config and set_chem_data kwargs only (#53) * Make set_scg and set_chem kwargs only * Ensure all params exist for set_chem * SL specific errors * Initial implementation of retrying requests * Fix logging * Use async_make_request * Retry tests * Expose retry count to gateway async_send_message * Set max_retries on gateway * Variable CLIENT_ID (#43) * Support specifying client_id, otherwise random * Take client_id at gateway instantiation not host * Update `MockConnectedGateway` to late connect * Update tests * Update to late connect * Allow setting max_retries at instantiate * Changes to instantiate. Formatting. * Make changelogs lists * Wording. * Move .decode into getString * Additional message code notes * Latest data example * Late connect for MockConnectedGateway * Make set_scg kwargs only * Reorder get/set * Make set_scg and set_chem kwargs only * Ensure all params exist for set_chem * SL specific errors * Latest data example * Make set_scg kwargs only * Fix output with 0 * New output format * Update set chem test * Merge fixes * Fixes for python 3.8 * New data validation * SCG and Chem settings moved to optional args * Update for optional args * Updates for optional args * Account for 0 being falsy * Refactor CLI * Deprecate support for Python 3.8 * Refactor option * Refactor validation * Possible ultra temp equip flag * Fix calcium hardness spelling * SLValueRange for limits, ON_OFF.from_bool returns enum * Force kwargs for chem_data and scg_config * Tests for cli updates * Handle missing existing values for kwarg methods --- README.md | 66 +++-- screenlogicpy/cli.py | 353 +++++++++++++++++------- screenlogicpy/const/common.py | 36 ++- screenlogicpy/const/data.py | 3 +- screenlogicpy/device_const/chemistry.py | 11 +- screenlogicpy/device_const/scg.py | 14 +- screenlogicpy/gateway.py | 148 ++++++---- screenlogicpy/requests/chemistry.py | 41 ++- screenlogicpy/requests/scg.py | 14 +- screenlogicpy/requests/status.py | 2 +- screenlogicpy/validation.py | 133 +++++++++ tests/data_sets.py | 56 +++- tests/test_cli.py | 86 +++++- tests/test_gateway.py | 46 ++- 14 files changed, 777 insertions(+), 232 deletions(-) create mode 100644 screenlogicpy/validation.py diff --git a/README.md b/README.md index 98f1489..0a997b0 100644 --- a/README.md +++ b/README.md @@ -275,41 +275,29 @@ success = await gateway.async_set_color_lights(light_command) ## Setting chlorinator output levels -Chlorinator output levels can be set with `async_set_scg_config()`. `async_set_scg_config` takes two `int` arguments, `pool_output` and `spa_output`. +Chlorinator output levels can be set with `async_set_scg_config()`. The method takes only key-word arguments to set specific values: `pool_output` and `spa_output`. ```python -success = await gateway.async_set_scg_config(pool_output, spa_output) +success = await gateway.async_set_scg_config(pool_output=pool_pct, spa_output=spa_pct) ``` * _New in v0.5.0._ +* _**Changed in v0.9.0**: All values are settable individually and only as key-word args._ ## Setting IntelliChem Chemistry values -Chemistry values used in the IntelliChem system can be set with `async_set_chem_data()`. `async_set_chem_data` takes six arguments, `ph_setpoint`, `orp_setpoint`, `calcium`, `alkalinity`, `cyanuric`, and `salt`. `ph_setpoint` is a `float` and the rest are `int`. +Chemistry values used in the IntelliChem system can be set with `async_set_chem_data()`. This method takes only key-word arguments to set specific values: `ph_setpoint`, `orp_setpoint`, `calcium_harness`, `total_alkalinity`, `cya`, and `salt_tds_ppm`. -```python -success = await gateway.async_set_chem_data(ph_setpoint, orp_setpoint, calcium, alkalinity, cyanuric, salt) -``` - -Currently all values are required, even if you only want to change one of them. For this reason, it is recommended that the calling code gathers all the current values first, then updates whichever value(s) are desired before calling `async_set_chem_data()`. +`ph_setpoint` is a `float` and the rest are `int`. ```python -chem_data = gateway.get_data()[DATA.KEY_CHEMISTRY] -ph = chem_data["ph_setpoint"]["value"] -orp = chem_data["orp_setpoint"]["value"] -ch = chem_data["calcium_harness"]["value"] -ta = chem_data["total_alkalinity"]["value"] -ca = chem_data["cya"]["value"] -sa = chem_data["salt_tds_ppm"]["value"] - -ph = ... # Code to update any of the values - -success = await gateway.async_set_chem_data(ph, orp, ch, ta, ca, sa) +success = await gateway.async_set_chem_data(ph_setpoint=ph, orp_setpoint=orp, calcium_harness=ch, total_alkalinity=ta, cya=cya, salt_tds_ppm=salt) ``` -**Note:** Only `ph_setpoint` and `orp_setpoint` are settable through the command line. +All values are accessible only as key-word arguments. * _New in v0.6.0._ +* _**Changed in v0.9.0**: All values are settable individually and only as key-word args._ ## Handling unsolicited messages @@ -526,7 +514,7 @@ screenlogicpy set circuit [circuit number] [circuit state] ``` Sets the specified circuit to the specified circuit state. -**Note:** `[circuit state]` can be an `int` or `string` representing the desired [circuit state](#circuit-state). +**Note:** `[circuit state]` can be an `int` or `string` representing the desired [state](#state). #### set `heat-mode, hm` @@ -560,28 +548,48 @@ Sets a color mode for all color-capable lights configured on the pool controller #### set `salt-generator, scg` ```shell -screenlogicpy set salt-generator [pool_pct] [spa_pct] +screenlogicpy set salt-generator [--pool OUTPUT] [--spa OUTPUT] ``` -Sets the chlorinator output levels for the pool and spa. Pentair treats spa output level as a percentage of the pool's output level. -**Note:** `[pool_pct]` can be an `int` between `0`-`100`, or `*` to keep the current value. `[spa_pct]` can be an `int` between `0`-`100`, or `*` to keep the current value. +Sets the chlorinator output levels for the pool and/or spa. Takes one or both of two optional arguments: (`-p`, `--pool`) and (`-s`, `--spa`). `OUTPUT` is an `int` between `0`-`100` for both settings. +**Note:** Pentair treats the spa output level as a percentage of the pool's output level. For example, if the pool output level is set to 80, and the spa output level is set to 50, the actual spa output would be 40%. * _New in v0.5.0._ +* _**Changed in v0.9.0**: Values are now settable individually via optional arguments._ + +#### set `super-chlorinate, sup` + +```shel +screenlogicpy set super-chlorinate [--state STATE] [--time HOURS] +``` + +Starts or stops super chlorination and/or sets the runtime in hours. Takes one or both optional arguments: (`-s`, `--state`) and (`-t`, `--time`). STATE can be an `int` or `string` representing the desired [state](#state). HOURS is an `int` the range of `0`-`72`. + +#### set `chem-setpoint, csp` + +```shell +screenlogicpy set chem-setpoint [--ph PH_SETPOINT] [--orp ORP_SETPOINT] +``` + +Sets the pH and/or ORP set points for the IntelliChem system. Takes one or both optional arguments: (`-p`, `--ph`) and (`-o`, `--orp`). `PH_SETPOINT` can be a `float` between `7.2`-`7.6`. `ORP_SETPOINT` can be an `int` between `400`-`800`. +**Note:** + +* _**New in v0.9.0**: pH and ORP settings are now here._ -#### set `chem-data, ch` +#### set `chem-data, cd` ```shell -screenlogicpy set chem-data [ph_setpoint] [orp_setpoint] +screenlogicpy set chem-data [--ch CALCIUM_HARDNESS] [--ta TOTAL_ALKALINITY] [--cya CYANURIC_ACID] [--salt SALT_or_TDS] ``` -Sets the pH and/or ORP set points for the IntelliChem system. -**Note:** `[ph_setpoint]` can be a `float` between `7.2`-`7.6`, or `*` to keep the current value. `[orp_setpoint]` can be an `int` between `400`-`800`, or `*` to keep the current value. +Sets various chemistry values used in LSI calculations within the IntelliChem system. Takes one or more optional arguments: (`-c`, `--ch`), (`-t`, `--ta`), (`-y`, `--cya`), and (`-s`, `--salt`). * _New in v0.6.0._ +* _**Changed in v0.9.0**: `chem-data` now sets values pertaining to LSI calculation in the IntelliChem system. pH and ORP settings have moved to the `chem-setpoint` sub-command._ # Reference -## Circuit State +## State | `int` | `string` | Name | | ----- | -------- | ---- | diff --git a/screenlogicpy/cli.py b/screenlogicpy/cli.py index 3823796..a17d2c4 100644 --- a/screenlogicpy/cli.py +++ b/screenlogicpy/cli.py @@ -1,4 +1,4 @@ -import asyncio +import logging import string import json import argparse @@ -6,7 +6,6 @@ from screenlogicpy.gateway import ScreenLogicGateway from screenlogicpy.const.common import ( ON_OFF, - RANGE, SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT, @@ -14,9 +13,10 @@ ScreenLogicError, ScreenLogicWarning, ) -from screenlogicpy.device_const.chemistry import RANGE_ORP_SETPOINT, RANGE_PH_SETPOINT +from screenlogicpy.device_const.chemistry import CHEM_RANGE from screenlogicpy.device_const.heat import HEAT_MODE from screenlogicpy.device_const.system import BODY_TYPE, COLOR_MODE +from screenlogicpy.device_const.scg import SCG_RANGE from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE @@ -47,6 +47,8 @@ def optionsFromDict(mapping: dict): async def cli(cli_args): """Handle command line args""" + gateway = ScreenLogicGateway() + def vFormat(slElement: dict, slClass=None): if args.verbose: if slClass: @@ -77,7 +79,7 @@ async def async_get_circuit(): return 0 async def async_set_circuit(): - state = ON_OFF.parse(args.state).value + state = (ON_OFF.parse(args.state)).value circuit_id = int(args.circuit_num) if circuit_id not in gateway.get_data(DEVICE.CIRCUIT): print(f"Invalid circuit number: {args.circuit_num}") @@ -172,72 +174,104 @@ async def async_set_color_light(): return 0 return 32 - async def async_set_scg_config(): - if args.scg_pool == "*" and args.scg_spa == "*": - print("No new Chlorinator values. Nothing to do.") + async def async_set_scg_setpoint(): + return await async_set_scg_config(pool=args.pool, spa=args.spa) + + async def async_set_scg_super(): + return await async_set_scg_config(state=args.state, time=args.time) + + async def async_set_scg_config( + *, + pool: int | None = None, + spa: int | None = None, + state: int | None = None, + time: int | None = None, + ): + if all( + ( + pool is None, + spa is None, + state is None, + time is None, + ) + ): + print("No new chlorinator values. Nothing to do.") return 65 - scg_config_data = gateway.get_data(DEVICE.SCG, GROUP.CONFIGURATION) - try: - scg_1 = ( - scg_config_data[VALUE.POOL_SETPOINT][ATTR.VALUE] - if args.scg_pool == "*" - else int(args.scg_pool) - ) - scg_2 = ( - scg_config_data[VALUE.SPA_SETPOINT][ATTR.VALUE] - if args.scg_spa == "*" - else int(args.scg_spa) - ) - except ValueError: - print("Invalid Chlorinator value") - return 66 + kwargs = { + VALUE.POOL_SETPOINT: pool, + VALUE.SPA_SETPOINT: spa, + VALUE.SUPER_CHLORINATE: state, + VALUE.SUPER_CHLOR_TIMER: time, + } - if await gateway.async_set_scg_config(scg_1, scg_2): - await gateway.async_update() + if await gateway.async_set_scg_config(**kwargs): + # await asyncio.sleep(3) + await gateway.async_get_scg() new_scg_config_data = gateway.get_data(DEVICE.SCG, GROUP.CONFIGURATION) print( - vFormat(new_scg_config_data[VALUE.POOL_SETPOINT]), - vFormat(new_scg_config_data[VALUE.SPA_SETPOINT]), + *[ + vFormat(new_scg_config_data[key]) + for key, value in kwargs.items() + if key in new_scg_config_data and value is not None + ] ) return 0 return 64 - async def async_set_chem_data(): - if args.ph_setpoint == "*" and args.orp_setpoint == "*": - print("No new setpoint values. Nothing to do.") - return 129 + async def async_set_chem_setpoint(): + return await async_set_chem_data(ph=args.ph, orp=args.orp) - chem_config_data = gateway.get_data(DEVICE.INTELLICHEM, GROUP.CONFIGURATION) - try: - ph = ( - chem_config_data[VALUE.PH_SETPOINT][ATTR.VALUE] - if args.ph_setpoint == "*" - else float(args.ph_setpoint) - ) - orp = ( - chem_config_data[VALUE.ORP_SETPOINT][ATTR.VALUE] - if args.orp_setpoint == "*" - else int(args.orp_setpoint) - ) - except ValueError: - print("Invalid Chemistry Setpoint value") - return 130 + async def async_set_chem_value(): + return await async_set_chem_data( + calcium_hardness=args.calcium_hardness, + total_alkalinity=args.total_alkalinity, + cyanuric_acid=args.cyanuric_acid, + total_dissolved_solids=args.total_dissolved_solids, + ) - ch = chem_config_data[VALUE.CALCIUM_HARNESS][ATTR.VALUE] - ta = chem_config_data[VALUE.TOTAL_ALKALINITY][ATTR.VALUE] - ca = chem_config_data[VALUE.CYA][ATTR.VALUE] - sa = chem_config_data[VALUE.SALT_TDS_PPM][ATTR.VALUE] + async def async_set_chem_data( + *, + ph: float | None = None, + orp: int | None = None, + calcium_hardness: int | None = None, + total_alkalinity: int | None = None, + cyanuric_acid: int | None = None, + total_dissolved_solids: int | None = None, + ): + if all( + ( + ph is None, + orp is None, + calcium_hardness is None, + total_alkalinity is None, + cyanuric_acid is None, + total_dissolved_solids is None, + ) + ): + print("No new chemistry values. Nothing to do.") + return 129 - if await gateway.async_set_chem_data(ph, orp, ch, ta, ca, sa): - await asyncio.sleep(3) - await gateway.async_update() + kwargs = { + VALUE.PH_SETPOINT: ph, + VALUE.ORP_SETPOINT: orp, + VALUE.CALCIUM_HARDNESS: calcium_hardness, + VALUE.TOTAL_ALKALINITY: total_alkalinity, + VALUE.CYA: cyanuric_acid, + VALUE.SALT_TDS_PPM: total_dissolved_solids, + } + if await gateway.async_set_chem_data(**kwargs): + # await asyncio.sleep(3) + await gateway.async_get_chemistry() new_chem_config_data = gateway.get_data( DEVICE.INTELLICHEM, GROUP.CONFIGURATION ) print( - vFormat(new_chem_config_data[VALUE.PH_SETPOINT]), - vFormat(new_chem_config_data[VALUE.ORP_SETPOINT]), + *[ + vFormat(new_chem_config_data[key]) + for key, value in kwargs.items() + if key in new_chem_config_data and value is not None + ] ) return 0 return 128 @@ -248,12 +282,24 @@ async def async_get_json(): return 0 option_parser = argparse.ArgumentParser( - description="Interface for Pentair Screenlogic gateway" + prog="screenlogicpy", description="Interface for Pentair Screenlogic gateway" ) - option_parser.add_argument("-v", "--verbose", action="store_true") - option_parser.add_argument("-i", "--ip") - option_parser.add_argument("-p", "--port", default=80) + option_parser.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="Enables verbose output. Additional 'v's increase logging up to '-vvv' for DEBUG logging", + ) + option_parser.add_argument( + "-i", + "--ip", + help="Bypasses discovery and specifies the ip address of the protocol adapter", + ) + option_parser.add_argument( + "-p", "--port", default=80, help="Specifies the port of the protocol adapter" + ) subparsers = option_parser.add_subparsers(dest="action") @@ -264,7 +310,7 @@ async def async_get_json(): ) # Get options - get_parser = subparsers.add_parser("get", help="Gets the option specified") + get_parser = subparsers.add_parser("get", help="Gets the specified value or state") get_subparsers = get_parser.add_subparsers(dest="get_option") get_subparsers.required = True @@ -274,7 +320,9 @@ async def async_get_json(): "type": int, "help": "Circuit number", } - get_circuit_parser = get_subparsers.add_parser("circuit", aliases=["c"]) + get_circuit_parser = get_subparsers.add_parser( + "circuit", aliases=["c"], help="Get the state of the specified circuit" + ) get_circuit_parser.add_argument(**ARGUMENT_CIRCUIT_NUM) get_circuit_parser.set_defaults(async_func=async_get_circuit) @@ -286,32 +334,54 @@ async def async_get_json(): "choices": body_options, "help": f"Body of water. One of: {body_options}", } - get_heat_mode_parser = get_subparsers.add_parser("heat-mode", aliases=["hm"]) + get_heat_mode_parser = get_subparsers.add_parser( + "heat-mode", aliases=["hm"], help="Get the heat mode for the specified body" + ) get_heat_mode_parser.add_argument(**ARGUMENT_BODY) get_heat_mode_parser.set_defaults(async_func=async_get_heat_mode) - get_heat_temp_parser = get_subparsers.add_parser("heat-temp", aliases=["ht"]) + get_heat_temp_parser = get_subparsers.add_parser( + "heat-temp", + aliases=["ht"], + help="Get the target temperature for the specified body", + ) get_heat_temp_parser.add_argument(**ARGUMENT_BODY) get_heat_temp_parser.set_defaults(async_func=async_get_heat_temp) - get_heat_state_parser = get_subparsers.add_parser("heat-state", aliases=["hs"]) + get_heat_state_parser = get_subparsers.add_parser( + "heat-state", + aliases=["hs"], + help="Get the current heating state for the specified body", + ) get_heat_state_parser.add_argument(**ARGUMENT_BODY) get_heat_state_parser.set_defaults(async_func=async_get_heat_state) - get_current_temp_parser = get_subparsers.add_parser("current-temp", aliases=["t"]) + get_current_temp_parser = get_subparsers.add_parser( + "current-temp", + aliases=["t"], + help="Get the current temperature for the specified body", + ) get_current_temp_parser.add_argument(**ARGUMENT_BODY) get_current_temp_parser.set_defaults(async_func=async_get_current_temp) - get_json_parser = get_subparsers.add_parser("json", aliases=["j"]) + get_json_parser = get_subparsers.add_parser( + "json", aliases=["j"], help="Return the full data dict as JSON" + ) get_json_parser.set_defaults(async_func=async_get_json) # Set options - set_parser = subparsers.add_parser("set") + set_parser = subparsers.add_parser( + "set", help="Sets the specified option, state, or value" + ) set_subparsers = set_parser.add_subparsers(dest="set_option") set_subparsers.required = True on_off_options = ON_OFF.parsable() - set_circuit_parser = set_subparsers.add_parser("circuit", aliases=["c"]) + set_circuit_parser = set_subparsers.add_parser( + "circuit", + aliases=["c"], + help="Set the specified circuit to the specified state", + ) set_circuit_parser.add_argument(**ARGUMENT_CIRCUIT_NUM) set_circuit_parser.add_argument( "state", @@ -323,7 +393,11 @@ async def async_get_json(): cl_options = COLOR_MODE.parsable() set_circuit_parser.set_defaults(async_func=async_set_circuit) - set_color_light_parser = set_subparsers.add_parser("color-lights", aliases=["cl"]) + set_color_light_parser = set_subparsers.add_parser( + "color-lights", + aliases=["cl"], + help="Send the specified color lights or IntelliBrite command", + ) set_color_light_parser.add_argument( "mode", metavar="MODE", @@ -333,7 +407,11 @@ async def async_get_json(): ) set_color_light_parser.set_defaults(async_func=async_set_color_light) - set_heat_mode_parser = set_subparsers.add_parser("heat-mode", aliases=["hm"]) + set_heat_mode_parser = set_subparsers.add_parser( + "heat-mode", + aliases=["hm"], + help="Set the specified heat mode for the specified body", + ) set_heat_mode_parser.add_argument(**ARGUMENT_BODY) hm_options = HEAT_MODE.parsable() set_heat_mode_parser.add_argument( @@ -346,53 +424,142 @@ async def async_get_json(): ) set_heat_mode_parser.set_defaults(async_func=async_set_heat_mode) - set_heat_temp_parser = set_subparsers.add_parser("heat-temp", aliases=["ht"]) + set_heat_temp_parser = set_subparsers.add_parser( + "heat-temp", + aliases=["ht"], + help="Set the specified target temperature for the specified body", + ) set_heat_temp_parser.add_argument(**ARGUMENT_BODY) set_heat_temp_parser.add_argument( "temp", type=int, metavar="TEMP", - default=None, help="Temperature to set in same unit of measurement as controller settings", ) set_heat_temp_parser.set_defaults(async_func=async_set_heat_temp) - set_scg_config_parser = set_subparsers.add_parser("salt-generator", aliases=["scg"]) - set_scg_config_parser.add_argument( - "scg_pool", - type=str, - metavar="POOL_PCT", + set_scg_setpoint_parser = set_subparsers.add_parser( + "salt-generator", + aliases=["scg"], + help="Set the SCG output level(s) for the pool and/or spa", + ) + set_scg_setpoint_parser.add_argument( + "-p", + "--pool", + type=int, + metavar="OUTPUT", + default=None, + help=f"Chlorinator output for when system is in POOL mode. {SCG_RANGE.POOL_SETPOINT.minimum}-{SCG_RANGE.POOL_SETPOINT.maximum}", + ) + set_scg_setpoint_parser.add_argument( + "-s", + "--spa", + type=int, + metavar="OUTPUT", default=None, - help="Chlorinator output for when system is in POOL mode. 0-100, or * to keep current value.", + help=f"Chlorinator output for when system is in SPA mode. {SCG_RANGE.SPA_SETPOINT.minimum}-{SCG_RANGE.SPA_SETPOINT.maximum}", ) - set_scg_config_parser.add_argument( - "scg_spa", + set_scg_setpoint_parser.set_defaults(async_func=async_set_scg_setpoint) + + set_scg_super_parser = set_subparsers.add_parser( + "super-chlorinate", aliases=["sc"], help="Configure super chlorination" + ) + set_scg_super_parser.add_argument( + "-s", + "--state", type=str, - metavar="SPA_PCT", + choices=on_off_options, default=None, - help="Chlorinator output for when system is in SPA mode. 0-20, or * to keep current value.", + help=f"State of super chlorination. One of {on_off_options}", + ) + + set_scg_super_parser.add_argument( + "-t", + "--time", + type=int, + metavar="HOURS", + default=None, + help=f"Time in hours to run super chlorination. {SCG_RANGE.SUPER_CHLOR_RT.minimum}-{SCG_RANGE.SUPER_CHLOR_RT.maximum}", + ) + set_scg_super_parser.set_defaults(async_func=async_set_scg_super) + + set_chem_setpoint_parser = set_subparsers.add_parser( + "chemistry-setpoint", + aliases=["cs"], + help="Set the specified pH and/or ORP setpoint(s) for the IntelliChem system", ) - set_scg_config_parser.set_defaults(async_func=async_set_scg_config) - set_chem_data_parser = set_subparsers.add_parser("chem-data", aliases=["ch"]) + set_chem_setpoint_parser.add_argument( + "-p", + "--ph", + type=float, + default=None, + help=( + "PH set point for IntelliChem. " + f"{CHEM_RANGE.PH_SETPOINT.minimum}-{CHEM_RANGE.PH_SETPOINT.maximum}" + ), + ) + + set_chem_setpoint_parser.add_argument( + "-o", + "--orp", + type=int, + default=None, + help=( + "ORP set point for IntelliChem. " + f"{CHEM_RANGE.ORP_SETPOINT.minimum}-{CHEM_RANGE.ORP_SETPOINT.maximum}" + ), + ) + set_chem_setpoint_parser.set_defaults(async_func=async_set_chem_setpoint) + + set_chem_data_parser = set_subparsers.add_parser( + "chemistry-value", + aliases=["cv"], + help="Set various chemistry values for LSI calculation in the IntelliChem system", + ) set_chem_data_parser.add_argument( - "ph_setpoint", - type=str, - metavar="PH_SETPOINT", + "-ch", + "--calcium-hardness", + type=int, default=None, - help=f"PH set point for IntelliChem. {RANGE_PH_SETPOINT[RANGE.MIN]}-{RANGE_PH_SETPOINT[RANGE.MAX]}, or * to keep current value.", + help="Calcium hardness for LSI calculations in the IntelliChem system.", ) set_chem_data_parser.add_argument( - "orp_setpoint", - type=str, - metavar="ORP_SETPOINT", + "-ta", + "--total-alkalinity", + type=int, + default=None, + help="Total alkalinity for LSI calculations in the IntelliChem system.", + ) + set_chem_data_parser.add_argument( + "-cya", + "--cyanuric-acid", + type=int, default=None, - help=f"ORP set point for IntelliChem. {RANGE_ORP_SETPOINT[RANGE.MIN]}-{RANGE_ORP_SETPOINT[RANGE.MAX]}, or * to keep current value.", + help="Cyanuric acid for LSI calculations in the IntelliChem system.", ) - set_chem_data_parser.set_defaults(async_func=async_set_chem_data) + set_chem_data_parser.add_argument( + "-tds", + "--total-dissolved-solids", + type=int, + default=None, + help="Salt or total dissolved solids (if not using a SCG) for LSI calculations in the IntelliChem system.", + ) + set_chem_data_parser.set_defaults(async_func=async_set_chem_value) args = option_parser.parse_args(cli_args) + if args.verbose == 2: + logging.basicConfig( + level=logging.INFO, + ) + elif args.verbose == 3: + logging.basicConfig( + format="%(asctime)s %(levelname)-8s %(message)s", + level=logging.DEBUG, + datefmt="%Y-%m-%d %H:%M:%S", + ) + try: host = {SL_GATEWAY_IP: args.ip, SL_GATEWAY_PORT: args.port} discovered = False @@ -432,8 +599,6 @@ async def async_get_json(): print("No ScreenLogic gateways found.") return 1 - gateway = ScreenLogicGateway() - await gateway.async_connect(**host) await gateway.async_update() diff --git a/screenlogicpy/const/common.py b/screenlogicpy/const/common.py index 88a2954..6e83c4a 100644 --- a/screenlogicpy/const/common.py +++ b/screenlogicpy/const/common.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from dataclasses import dataclass from enum import IntEnum CLIENT_ID = 49151 @@ -17,8 +20,10 @@ class ScreenLogicException(Exception): """Common class for all ScreenLogic exceptions.""" - def __init__(self, message: str, *args: object) -> None: - self.msg = message + def __init__(self, *args: object) -> None: + self.msg = None + if len(args) > 0: + self.msg = args[0] super().__init__(*args) @@ -36,7 +41,7 @@ class ScreenLogicRequestError(ScreenLogicException): class SLIntEnum(IntEnum): @classmethod - def parse(cls, value: str, default=0) -> "SLIntEnum": + def parse(cls, value: str, default=0) -> SLIntEnum: """Attempt to return and Enum from the provided string.""" try: return ( @@ -66,6 +71,27 @@ def title(self) -> str: return self._title() +@dataclass +class SLValueRange: + minimum: int | float + maximum: int | float + unit: str | None = None + + def check(self, value: int | float) -> None: + if not self.in_range(value): + raise ValueError(f"{value} not in range {self.minimum}-{self.maximum}") + + def in_range(self, value: int | float) -> bool: + return self.minimum <= value <= self.maximum + + def parse_check(self, string: str) -> int | float: + value = float(string) if isinstance(self.minimum, float) else int(string) + if self.minimum <= value <= self.maximum: + return value + else: + raise ValueError(f"{value} not in range {self.minimum}-{self.maximum}") + + class DATA_REQUEST: CHEMISTRY = "chemistry" KEY_COLOR = "color" @@ -133,5 +159,5 @@ class ON_OFF(SLIntEnum): ON = 1 @classmethod - def from_bool(cls, expression: bool): - return cls.ON.value if expression else cls.OFF.value + def from_bool(cls, expression: bool) -> ON_OFF: + return cls.ON if expression else cls.OFF diff --git a/screenlogicpy/const/data.py b/screenlogicpy/const/data.py index a65b2e2..d817509 100644 --- a/screenlogicpy/const/data.py +++ b/screenlogicpy/const/data.py @@ -52,7 +52,7 @@ class GROUP: class VALUE: ACTIVE_ALERT = "active_alert" AIR_TEMPERATURE = "air_temperature" - CALCIUM_HARNESS = "calcium_harness" + CALCIUM_HARDNESS = "calcium_hardness" CIRCUIT_COUNT = "circuit_count" CLEANER_DELAY = "cleaner_delay" COLOR_COUNT = "color_count" @@ -122,6 +122,7 @@ class VALUE: SPA_SETPOINT = "spa_setpoint" STATE = "state" SUPER_CHLOR_TIMER = "super_chlor_timer" + SUPER_CHLORINATE = "super_chlorinate" TOTAL_ALKALINITY = "total_alkalinity" TYPE = "type" WATTS_NOW = "watts_now" diff --git a/screenlogicpy/device_const/chemistry.py b/screenlogicpy/device_const/chemistry.py index 5e94780..8007418 100644 --- a/screenlogicpy/device_const/chemistry.py +++ b/screenlogicpy/device_const/chemistry.py @@ -1,6 +1,6 @@ from enum import IntFlag from ..const import SLIntEnum -from ..const.common import RANGE +from ..const.common import SLValueRange class ALARM_FLAG(IntFlag): @@ -79,5 +79,10 @@ class DOSE_TYPE_PH(SLIntEnum): # Valid ranges listed in IntelliChem documentation -RANGE_PH_SETPOINT = {RANGE.MIN: 7.2, RANGE.MAX: 7.6} -RANGE_ORP_SETPOINT = {RANGE.MIN: 400, RANGE.MAX: 800} +class CHEM_RANGE: + PH_SETPOINT = SLValueRange(7.2, 7.6) + ORP_SETPOINT = SLValueRange(400, 800) + CALCIUM_HARDNESS = SLValueRange(25, 800) + CYANURIC_ACID = SLValueRange(0, 201) + TOTAL_ALKALINITY = SLValueRange(25, 800) + SALT_TDS = SLValueRange(500, 6500) diff --git a/screenlogicpy/device_const/scg.py b/screenlogicpy/device_const/scg.py index d52e7c5..41d1ea2 100644 --- a/screenlogicpy/device_const/scg.py +++ b/screenlogicpy/device_const/scg.py @@ -1,4 +1,14 @@ from .system import BODY_TYPE +from ..const.common import SLValueRange -LIMIT_FOR_BODY = {BODY_TYPE.POOL: 100, BODY_TYPE.SPA: 100} -MAX_SC_RUNTIME = 72 + +class SCG_RANGE: + POOL_SETPOINT = SLValueRange(0, 100) + SPA_SETPOINT = SLValueRange(0, 100) + SUPER_CHLOR_RT = SLValueRange(0, 72) + + +RANGE_FOR_BODY = { + BODY_TYPE.POOL: SCG_RANGE.POOL_SETPOINT, + BODY_TYPE.SPA: SCG_RANGE.SPA_SETPOINT, +} diff --git a/screenlogicpy/gateway.py b/screenlogicpy/gateway.py index 590929b..6334df3 100644 --- a/screenlogicpy/gateway.py +++ b/screenlogicpy/gateway.py @@ -6,14 +6,14 @@ from .client import ClientManager from .const.common import ( DATA_REQUEST, - RANGE, + ON_OFF, ScreenLogicError, ScreenLogicRequestError, ) from .const.msg import COM_MAX_RETRIES -from .device_const.chemistry import RANGE_PH_SETPOINT, RANGE_ORP_SETPOINT -from .device_const.system import BODY_TYPE, EQUIPMENT_FLAG -from .device_const.scg import LIMIT_FOR_BODY +from .device_const.chemistry import CHEM_RANGE as cr +from .device_const.system import EQUIPMENT_FLAG +from .device_const.scg import SCG_RANGE as sr from .const.data import ATTR, DEVICE, GROUP, VALUE from .requests import ( async_connect_to_gateway, @@ -340,44 +340,116 @@ async def async_set_color_lights(self, light_command: int): async_request_pool_lights_command, light_command ) - async def async_set_scg_config(self, pool_output: int, spa_output: int): - """Set the salt-chlorine-generator output for both pool and spa.""" - if not self._is_valid_scg_value(pool_output, BODY_TYPE.POOL): - raise ValueError(f"Invalid pool_output: {pool_output}") - if not self._is_valid_scg_value(spa_output, BODY_TYPE.SPA): - raise ValueError(f"Invalid spa_output: {spa_output}") + async def async_set_scg_config( + self, + *, + pool_setpoint: int | None = None, + spa_setpoint: int | None = None, + super_chlorinate: int | None = None, + super_chlor_timer: int | None = None, + ): + """Set the salt-chlorine-generator output. + + Sets output values for both pool and spa, along with super chlorination timer. + """ + SCG_CONFIG = (DEVICE.SCG, GROUP.CONFIGURATION) + try: + if pool_setpoint is None: + pool_setpoint = self.get_value( + *SCG_CONFIG, VALUE.POOL_SETPOINT, strict=True + ) + + if spa_setpoint is None: + spa_setpoint = self.get_value( + *SCG_CONFIG, VALUE.SPA_SETPOINT, strict=True + ) + + if super_chlorinate is None: + super_chlorinate = ( + 0 # self.get_data(*SCG_CONFIG, VALUE.SUPER_CHLORINATE, strict=True) + ) + + if super_chlor_timer is None: + super_chlor_timer = self.get_value( + *SCG_CONFIG, VALUE.SUPER_CHLOR_TIMER, strict=True + ) + + sr.POOL_SETPOINT.check(pool_setpoint) + sr.SPA_SETPOINT.check(spa_setpoint) + super_chlorinate = ON_OFF.parse(super_chlorinate).value + sr.SUPER_CHLOR_RT.check(super_chlor_timer) + except (KeyError, ValueError) as ex: + raise ScreenLogicError(ex.args[0]) from ex return await self._async_connected_request( - async_request_set_scg_config, pool_output, spa_output + async_request_set_scg_config, + pool_setpoint, + spa_setpoint, + super_chlorinate, + super_chlor_timer, ) async def async_set_chem_data( self, - ph_setpoint: float, - orp_setpoint: int, - calcium: int, - alkalinity: int, - cyanuric: int, - salt: int, + *, + ph_setpoint: float | None = None, + orp_setpoint: int | None = None, + calcium_hardness: int | None = None, + total_alkalinity: int | None = None, + cya: int | None = None, + salt_tds_ppm: int | None = None, ): """Set configurable chemistry values.""" - if self._is_valid_ph_setpoint(ph_setpoint): - ph_setpoint = int(ph_setpoint * 100) - else: - raise ValueError(f"Invalid PH Set point: {ph_setpoint}") - if not self._is_valid_orp_setpoint(orp_setpoint): - raise ValueError(f"Invalid ORP Set point: {orp_setpoint}") - if calcium < 0 or alkalinity < 0 or cyanuric < 0 or salt < 0: - raise ValueError("Invalid Chemistry setting.") + INTELLICHEM_CONFIG = (DEVICE.INTELLICHEM, GROUP.CONFIGURATION) + + try: + if ph_setpoint is None: + ph_setpoint = self.get_value( + *INTELLICHEM_CONFIG, VALUE.PH_SETPOINT, strict=True + ) + + if orp_setpoint is None: + orp_setpoint = self.get_value( + *INTELLICHEM_CONFIG, VALUE.ORP_SETPOINT, strict=True + ) + + if calcium_hardness is None: + calcium_hardness = self.get_value( + *INTELLICHEM_CONFIG, VALUE.CALCIUM_HARDNESS, strict=True + ) + + if total_alkalinity is None: + total_alkalinity = self.get_value( + *INTELLICHEM_CONFIG, VALUE.TOTAL_ALKALINITY, strict=True + ) + + if cya is None: + cya = self.get_value(*INTELLICHEM_CONFIG, VALUE.CYA, strict=True) + + if salt_tds_ppm is None: + salt_tds_ppm = self.get_value( + *INTELLICHEM_CONFIG, VALUE.SALT_TDS_PPM, strict=True + ) + + cr.PH_SETPOINT.check(ph_setpoint) + cr.ORP_SETPOINT.check(orp_setpoint) + cr.CALCIUM_HARDNESS.check(calcium_hardness) + cr.TOTAL_ALKALINITY.check(total_alkalinity) + cr.CYANURIC_ACID.check(cya) + cr.SALT_TDS.check(salt_tds_ppm) + except (KeyError, ValueError) as ex: + raise ScreenLogicError(ex.args[0]) from ex + + ph_setpoint = int(ph_setpoint * 100) return await self._async_connected_request( async_request_set_chem_data, ph_setpoint, orp_setpoint, - calcium, - alkalinity, - cyanuric, - salt, + calcium_hardness, + total_alkalinity, + cya, + salt_tds_ppm, ) async def async_subscribe_client( @@ -482,21 +554,3 @@ def _is_valid_heattemp(self, body, temp): def _is_valid_color_mode(self, mode): """Validate color mode number.""" return 0 <= mode <= 21 - - def _is_valid_scg_value(self, scg_value, body_type): - """Validate chlorinator value for body.""" - return 0 <= scg_value <= LIMIT_FOR_BODY[body_type] - - def _is_valid_ph_setpoint(self, ph_setpoint: float): - """Validate pH setpoint.""" - return ( - RANGE_PH_SETPOINT[RANGE.MIN] <= ph_setpoint <= RANGE_PH_SETPOINT[RANGE.MAX] - ) - - def _is_valid_orp_setpoint(self, orp_setpoint: int): - """Validate ORP setpoint.""" - return ( - RANGE_ORP_SETPOINT[RANGE.MIN] - <= orp_setpoint - <= RANGE_ORP_SETPOINT[RANGE.MAX] - ) diff --git a/screenlogicpy/requests/chemistry.py b/screenlogicpy/requests/chemistry.py index 775c490..d5efcf1 100644 --- a/screenlogicpy/requests/chemistry.py +++ b/screenlogicpy/requests/chemistry.py @@ -13,6 +13,7 @@ ALARM_FLAG, ALERT_FLAG, BALANCE_FLAG, + CHEM_RANGE, DOSE_MASK, DOSE_STATE, ) @@ -66,6 +67,8 @@ def decode_chemistry(buff: bytes, data: dict) -> None: ATTR.NAME: "pH Setpoint", ATTR.VALUE: (pHSetpoint / 100), ATTR.UNIT: UNIT.PH, + ATTR.MIN_SETPOINT: CHEM_RANGE.PH_SETPOINT.minimum, + ATTR.MAX_SETPOINT: CHEM_RANGE.PH_SETPOINT.maximum, } orpSetpoint, offset = getSome(">H", buff, offset) # 11 @@ -73,6 +76,8 @@ def decode_chemistry(buff: bytes, data: dict) -> None: ATTR.NAME: "ORP Setpoint", ATTR.VALUE: orpSetpoint, ATTR.UNIT: UNIT.MILLIVOLT, + ATTR.MIN_SETPOINT: CHEM_RANGE.ORP_SETPOINT.minimum, + ATTR.MAX_SETPOINT: CHEM_RANGE.ORP_SETPOINT.maximum, } intellichem_dosing: dict = intellichem.setdefault(GROUP.DOSE_STATUS, {}) @@ -136,10 +141,12 @@ def decode_chemistry(buff: bytes, data: dict) -> None: } cal, offset = getSome(">H", buff, offset) # 28 - intellichem_config[VALUE.CALCIUM_HARNESS] = { + intellichem_config[VALUE.CALCIUM_HARDNESS] = { ATTR.NAME: "Calcium Hardness", ATTR.VALUE: cal, ATTR.UNIT: UNIT.PARTS_PER_MILLION, + ATTR.MIN_SETPOINT: CHEM_RANGE.CALCIUM_HARDNESS.minimum, + ATTR.MAX_SETPOINT: CHEM_RANGE.CALCIUM_HARDNESS.maximum, } cya, offset = getSome(">H", buff, offset) # 30 @@ -147,6 +154,8 @@ def decode_chemistry(buff: bytes, data: dict) -> None: ATTR.NAME: "Cyanuric Acid", ATTR.VALUE: cya, ATTR.UNIT: UNIT.PARTS_PER_MILLION, + ATTR.MIN_SETPOINT: CHEM_RANGE.CYANURIC_ACID.minimum, + ATTR.MAX_SETPOINT: CHEM_RANGE.CYANURIC_ACID.maximum, } alk, offset = getSome(">H", buff, offset) # 32 @@ -154,6 +163,8 @@ def decode_chemistry(buff: bytes, data: dict) -> None: ATTR.NAME: "Total Alkalinity", ATTR.VALUE: alk, ATTR.UNIT: UNIT.PARTS_PER_MILLION, + ATTR.MIN_SETPOINT: CHEM_RANGE.TOTAL_ALKALINITY.minimum, + ATTR.MAX_SETPOINT: CHEM_RANGE.TOTAL_ALKALINITY.maximum, } saltPPM, offset = getSome("B", buff, offset) # 34 @@ -161,6 +172,8 @@ def decode_chemistry(buff: bytes, data: dict) -> None: ATTR.NAME: "Salt/TDS", ATTR.VALUE: (saltPPM * 50), ATTR.UNIT: UNIT.PARTS_PER_MILLION, + ATTR.MIN_SETPOINT: CHEM_RANGE.SALT_TDS.minimum, + ATTR.MAX_SETPOINT: CHEM_RANGE.SALT_TDS.maximum, } # Probe temp unit is Celsius? @@ -186,42 +199,42 @@ def decode_chemistry(buff: bytes, data: dict) -> None: intellichem_alarm[VALUE.FLOW_ALARM] = { ATTR.NAME: "Flow Alarm", - ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.FLOW), + ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.FLOW).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } intellichem_alarm[VALUE.PH_HIGH_ALARM] = { ATTR.NAME: "pH HIGH Alarm", - ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.PH_HIGH), + ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.PH_HIGH).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } intellichem_alarm[VALUE.PH_LOW_ALARM] = { ATTR.NAME: "pH LOW Alarm", - ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.PH_LOW), + ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.PH_LOW).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } intellichem_alarm[VALUE.ORP_HIGH_ALARM] = { ATTR.NAME: "ORP HIGH Alarm", - ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.ORP_HIGH), + ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.ORP_HIGH).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } intellichem_alarm[VALUE.ORP_LOW_ALARM] = { ATTR.NAME: "ORP LOW Alarm", - ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.ORP_LOW), + ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.ORP_LOW).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } intellichem_alarm[VALUE.PH_SUPPLY_ALARM] = { ATTR.NAME: "pH Supply Alarm", - ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.PH_SUPPLY), + ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.PH_SUPPLY).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } intellichem_alarm[VALUE.ORP_SUPPLY_ALARM] = { ATTR.NAME: "ORP Supply Alarm", - ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.ORP_SUPPLY), + ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.ORP_SUPPLY).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } intellichem_alarm[VALUE.PROBE_FAULT_ALARM] = { ATTR.NAME: "Probe Fault", - ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.PROBE_FAULT), + ATTR.VALUE: ON_OFF.from_bool(alarms & ALARM_FLAG.PROBE_FAULT).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } @@ -232,15 +245,15 @@ def decode_chemistry(buff: bytes, data: dict) -> None: intellichem_alert[VALUE.PH_LOCKOUT] = { ATTR.NAME: "pH Lockout", - ATTR.VALUE: ON_OFF.from_bool(alerts & ALERT_FLAG.PH_LOCKOUT), + ATTR.VALUE: ON_OFF.from_bool(alerts & ALERT_FLAG.PH_LOCKOUT).value, } intellichem_alert[VALUE.PH_LIMIT] = { ATTR.NAME: "pH Dose Limit Reached", - ATTR.VALUE: ON_OFF.from_bool(alerts & ALERT_FLAG.PH_LIMIT), + ATTR.VALUE: ON_OFF.from_bool(alerts & ALERT_FLAG.PH_LIMIT).value, } intellichem_alert[VALUE.ORP_LIMIT] = { ATTR.NAME: "ORP Dose Limit Reached", - ATTR.VALUE: ON_OFF.from_bool(alerts & ALERT_FLAG.ORP_LIMIT), + ATTR.VALUE: ON_OFF.from_bool(alerts & ALERT_FLAG.ORP_LIMIT).value, } dose_flags, offset = getSome("B", buff, offset) # 39 (34) @@ -276,14 +289,14 @@ def decode_chemistry(buff: bytes, data: dict) -> None: # SI <= -0.41 intellichem_balance[VALUE.CORROSIVE] = { ATTR.NAME: "SI Corrosive", - ATTR.VALUE: ON_OFF.from_bool(balance_flags & BALANCE_FLAG.CORROSIVE), + ATTR.VALUE: ON_OFF.from_bool(balance_flags & BALANCE_FLAG.CORROSIVE).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } # SI >= +0.53 intellichem_balance[VALUE.SCALING] = { ATTR.NAME: "SI Scaling", - ATTR.VALUE: ON_OFF.from_bool(balance_flags & BALANCE_FLAG.SCALING), + ATTR.VALUE: ON_OFF.from_bool(balance_flags & BALANCE_FLAG.SCALING).value, ATTR.DEVICE_TYPE: DEVICE_TYPE.ALARM, } diff --git a/screenlogicpy/requests/scg.py b/screenlogicpy/requests/scg.py index 935c6bd..0dd6558 100644 --- a/screenlogicpy/requests/scg.py +++ b/screenlogicpy/requests/scg.py @@ -3,7 +3,7 @@ from ..const.common import STATE_TYPE, UNIT from ..const.msg import CODE, COM_MAX_RETRIES from ..const.data import ATTR, DEVICE, GROUP, VALUE -from ..device_const.scg import LIMIT_FOR_BODY, MAX_SC_RUNTIME +from ..device_const.scg import SCG_RANGE from ..device_const.system import BODY_TYPE from .protocol import ScreenLogicProtocol from .request import async_make_request @@ -40,8 +40,8 @@ def decode_scg_config(buff: bytes, data: dict) -> None: ATTR.NAME: "Pool Chlorinator Setpoint", ATTR.VALUE: level1, ATTR.UNIT: UNIT.PERCENT, - ATTR.MIN_SETPOINT: 0, - ATTR.MAX_SETPOINT: LIMIT_FOR_BODY[BODY_TYPE.POOL], + ATTR.MIN_SETPOINT: SCG_RANGE.POOL_SETPOINT.minimum, + ATTR.MAX_SETPOINT: SCG_RANGE.POOL_SETPOINT.maximum, ATTR.STEP: 5, ATTR.BODY_TYPE: BODY_TYPE.POOL.value, } @@ -51,8 +51,8 @@ def decode_scg_config(buff: bytes, data: dict) -> None: ATTR.NAME: "Spa Chlorinator Setpoint", ATTR.VALUE: level2, ATTR.UNIT: UNIT.PERCENT, - ATTR.MIN_SETPOINT: 0, - ATTR.MAX_SETPOINT: LIMIT_FOR_BODY[BODY_TYPE.SPA], + ATTR.MIN_SETPOINT: SCG_RANGE.SPA_SETPOINT.minimum, + ATTR.MAX_SETPOINT: SCG_RANGE.SPA_SETPOINT.maximum, ATTR.STEP: 5, ATTR.BODY_TYPE: BODY_TYPE.SPA.value, } @@ -73,8 +73,8 @@ def decode_scg_config(buff: bytes, data: dict) -> None: ATTR.NAME: "Super Chlorination Timer", ATTR.VALUE: superChlorTimer, ATTR.UNIT: UNIT.HOUR, - ATTR.MIN_SETPOINT: 1, - ATTR.MAX_SETPOINT: MAX_SC_RUNTIME, + ATTR.MIN_SETPOINT: SCG_RANGE.SUPER_CHLOR_RT.minimum, + ATTR.MAX_SETPOINT: SCG_RANGE.SUPER_CHLOR_RT.maximum, ATTR.STEP: 1, } diff --git a/screenlogicpy/requests/status.py b/screenlogicpy/requests/status.py index 054013a..11d2118 100644 --- a/screenlogicpy/requests/status.py +++ b/screenlogicpy/requests/status.py @@ -42,7 +42,7 @@ def decode_pool_status(buff: bytes, data: dict) -> None: freezeMode, offset = getSome("B", buff, offset) # byte offset 4 controller_sensor[VALUE.FREEZE_MODE] = { ATTR.NAME: "Freeze Mode", - ATTR.VALUE: ON_OFF.from_bool(freezeMode & 0x08), + ATTR.VALUE: ON_OFF.from_bool(freezeMode & 0x08).value, } controller_config: dict = controller.setdefault(GROUP.CONFIGURATION, {}) diff --git a/screenlogicpy/validation.py b/screenlogicpy/validation.py new file mode 100644 index 0000000..2520947 --- /dev/null +++ b/screenlogicpy/validation.py @@ -0,0 +1,133 @@ +from .const import BODY_TYPE, COLOR_MODE, HEAT_MODE + + +class DataValidationKey: + # ScreenLogic data + HEAT_MODE = "heat_mode" + HEAT_TEMP = "heat_temp" + BODY = "body" + CIRCUIT = "circuit" + ON_OFF = "on_off_state" + COLOR_MODE = "color_mode" + SCG_SETPOINT = "scg_setpoint" + SC_RUNTIME = "sc_runtime" + PH_SETPOINT = "ph_setpoint" + ORP_SETPOINT = "orp_setpoint" + TOTAL_ALKALINITY = "total_alkalinity" + CALCIUM_HARDNESS = "calcium_hardness" + CYANURIC_ACID = "cyanuric_acid" + SALT_TDS = "salt_tds_ppm" + + # screenlogicpy Settings + MAX_RETRIES = "max_retries" + CLIENT_ID = "client_id" + + +class DataValidation: + DV_KEY = DataValidationKey + + # These shouldn't change between pool configurations + _static_bounds = { + DV_KEY.ON_OFF: {0, 1}, + # Valid ranges per documentation + DV_KEY.SCG_SETPOINT: { + BODY_TYPE.POOL: (0, 100), + BODY_TYPE.SPA: (0, 100), + }, + DV_KEY.SC_RUNTIME: (1, 72), + DV_KEY.ORP_SETPOINT: (400, 800), + DV_KEY.PH_SETPOINT: (7.2, 7.6), + # Valid ranges as settable on IntelliChem controller + DV_KEY.CALCIUM_HARDNESS: (25, 800), + DV_KEY.CYANURIC_ACID: (0, 201), + DV_KEY.SALT_TDS: (500, 6500), + DV_KEY.TOTAL_ALKALINITY: (25, 800), + } + + # Placeholders that should be overridden with actual values from pool config + _config_bounds = { + DV_KEY.BODY: {0, 1}, + DV_KEY.CIRCUIT: {500, 505}, + DV_KEY.COLOR_MODE: set(COLOR_MODE.NAME_FOR_NUM.keys()), + DV_KEY.HEAT_MODE: set(HEAT_MODE.NAME_FOR_NUM.keys()), + DV_KEY.HEAT_TEMP: { + 0: (40, 104), + 1: (40, 104), + }, + } + + # Specific to screenlogicpy + _app_bounds = { + DV_KEY.MAX_RETRIES: (0, 5), + DV_KEY.CLIENT_ID: (32767, 65535), + } + + def __init__(self) -> None: + self._data_bounds = { + **self._static_bounds, + **self._app_bounds, + **self._config_bounds, + } + + def get_bounds(self, key): + if isinstance(key, tuple): + return self._data_bounds[key[0]][key[1]] + elif isinstance(key, str): + return self._data_bounds[key] + else: + raise TypeError(f"Invalid data validation key: {key}") + + def is_valid(self, key, value) -> bool: + bounds = self.get_bounds(key) + if isinstance(bounds, set): + return self._is_valid_set(bounds, value) + elif isinstance(bounds, tuple): + return self._is_valid_range(bounds, value) + return False + + def _is_valid_range(self, bounds, value) -> bool: + if value is None: + return False + bounds_min, bounds_max = bounds + if bounds_min is not None and bounds_max is not None: + return bounds_min <= value <= bounds_max + elif bounds_max is not None: + return value <= bounds_max + elif bounds_min is not None: + return bounds_min <= value + return True + + def _is_valid_set(self, bounds, value) -> bool: + return value in bounds + + def validate(self, key, value): + bounds = self.get_bounds(key) + if not self.is_valid(key, value): + raise ValueError(f"{key} {value} not in {bounds}") + + def clamp(self, key, value): + bounds = self.get_bounds(key) + if not isinstance(bounds, tuple): + raise ValueError(f"{key} not a min/max.") + bounds_min, bounds_max = bounds + if bounds_min is not None and bounds_max is not None: + return max(min(value, bounds_max), bounds_min) + elif bounds_max is not None: + return min(value, bounds_max) + elif bounds_min is not None: + return max(value, bounds_min) + return value + + def update(self, key, new_bounds) -> None: + bounds = self.get_bounds(key) + if not isinstance(new_bounds, type(bounds)): + raise TypeError( + f"New bounds ({type(new_bounds)}) does not match existing bounds ({type(bounds)})" + ) + if isinstance(key, tuple): + self._config_bounds[key[0]][key[1]] = new_bounds + elif isinstance(key, str): + self._config_bounds[key] = new_bounds + else: + raise TypeError(f"Invalid data validation key: {key}") + self._data_bounds.update(self._config_bounds) diff --git a/tests/data_sets.py b/tests/data_sets.py index e7e8fc2..0d6c1d4 100644 --- a/tests/data_sets.py +++ b/tests/data_sets.py @@ -612,24 +612,48 @@ class ScreenLogicResponseCollection: }, }, "configuration": { - "ph_setpoint": {"name": "pH Setpoint", "value": 7.5, "unit": "pH"}, + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.5, + "unit": "pH", + "min_setpoint": 7.2, + "max_setpoint": 7.6, + }, "orp_setpoint": { "name": "ORP Setpoint", "value": 700, "unit": "mV", + "min_setpoint": 400, + "max_setpoint": 800, }, - "calcium_harness": { + "calcium_hardness": { "name": "Calcium Hardness", "value": 740, "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800, + }, + "cya": { + "name": "Cyanuric Acid", + "value": 36, + "unit": "ppm", + "min_setpoint": 0, + "max_setpoint": 201, }, - "cya": {"name": "Cyanuric Acid", "value": 36, "unit": "ppm"}, "total_alkalinity": { "name": "Total Alkalinity", "value": 70, "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800, + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm", + "min_setpoint": 500, + "max_setpoint": 6500, }, - "salt_tds_ppm": {"name": "Salt/TDS", "value": 1000, "unit": "ppm"}, "probe_is_celsius": 0, "flags": 32, }, @@ -777,7 +801,7 @@ class ScreenLogicResponseCollection: "name": "Super Chlorination Timer", "value": 0, "unit": "hr", - "min_setpoint": 1, + "min_setpoint": 0, "max_setpoint": 72, "step": 1, }, @@ -1623,27 +1647,43 @@ class ScreenLogicResponseCollection: "name": "pH Setpoint", "value": 7.5, "unit": "pH", + "min_setpoint": 7.2, + "max_setpoint": 7.6, }, "orp_setpoint": { "name": "ORP Setpoint", "value": 700, "unit": "mV", + "min_setpoint": 400, + "max_setpoint": 800, }, - "calcium_harness": { + "calcium_hardness": { "name": "Calcium Hardness", "value": 740, "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800, + }, + "cya": { + "name": "Cyanuric Acid", + "value": 36, + "unit": "ppm", + "min_setpoint": 0, + "max_setpoint": 201, }, - "cya": {"name": "Cyanuric Acid", "value": 36, "unit": "ppm"}, "total_alkalinity": { "name": "Total Alkalinity", "value": 70, "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800, }, "salt_tds_ppm": { "name": "Salt/TDS", "value": 1000, "unit": "ppm", + "min_setpoint": 500, + "max_setpoint": 6500, }, "probe_is_celsius": 0, "flags": 32, @@ -1797,7 +1837,7 @@ class ScreenLogicResponseCollection: "name": "Super Chlorination Timer", "value": 0, "unit": "hr", - "min_setpoint": 1, + "min_setpoint": 0, "max_setpoint": 72, "step": 1, }, diff --git a/tests/test_cli.py b/tests/test_cli.py index 2598c23..b70a9e0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -170,34 +170,96 @@ async def test_set_color_lights(MockProtocolAdapter, capsys, args, ret, expected @pytest.mark.parametrize( "args, ret, expected", [ - ("set salt-generator 100 20", 0, "50 0"), + ("set salt-generator --pool 50", 0, "50"), + ("set salt-generator --spa 0", 0, "0"), + ("set salt-generator -p 100 -s 0", 0, "50 0"), ( - "-v set scg 20 0", + "-v set scg --pool 55 --spa 25", 0, EXPECTED_VERBOSE_PREAMBLE + "Pool Chlorinator Setpoint: 50 Spa Chlorinator Setpoint: 0", ), - ("set scg * *", 65, "No new Chlorinator values. Nothing to do."), - ("set scg f *", 66, "Invalid Chlorinator value"), + ("set scg", 65, "No new chlorinator values. Nothing to do."), + ("set scg -p sixty", 66, SystemExit), ], ) -async def test_set_scg(MockProtocolAdapter, capsys, args, ret, expected): - await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) +async def test_set_scg_setpoint(MockProtocolAdapter, capsys, args, ret, expected): + if type(expected) == type and issubclass(expected, BaseException): + with pytest.raises(expected): + await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) + else: + await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "args, ret, expected", + [ + ("set super-chlorinate --state 0", 0, ""), + ("set super-chlorinate --time 12", 0, "0"), + ("set super-chlorinate -s 1 -t 72", 0, "0"), + ( + "-v set sc --state 0 --time 12", + 0, + EXPECTED_VERBOSE_PREAMBLE + "Super Chlorination Timer: 0", + ), + ("set sc", 65, "No new chlorinator values. Nothing to do."), + ("set sc -t sixty", 66, SystemExit), + ], +) +async def test_set_scg_super(MockProtocolAdapter, capsys, args, ret, expected): + if type(expected) == type and issubclass(expected, BaseException): + with pytest.raises(expected): + await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) + else: + await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) @pytest.mark.asyncio @pytest.mark.parametrize( "args, ret, expected", [ - ("set chem-data 7.5 700", 0, "7.5 700"), + ("set chemistry-setpoint --ph 7.5", 0, "7.5"), + ("set chemistry-setpoint --orp 700", 0, "700"), + ("set chemistry-setpoint -p 7.5 -o 700", 0, "7.5 700"), ( - "-v set ch 7.6 650", + "-v set cs -p 7.6 -o 650", 0, EXPECTED_VERBOSE_PREAMBLE + "pH Setpoint: 7.5 ORP Setpoint: 700", ), - ("set ch * *", 129, "No new setpoint values. Nothing to do."), - ("set ch f *", 130, "Invalid Chemistry Setpoint value"), + ("set cs", 129, "No new chemistry values. Nothing to do."), + ("set cs -o seven", 2, SystemExit), + ("set cs -o 900", 128, "900 not in range 400-800"), ], ) -async def test_set_chemistry(MockProtocolAdapter, capsys, args, ret, expected): - await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) +async def test_set_chemistry_setpoint(MockProtocolAdapter, capsys, args, ret, expected): + if type(expected) == type and issubclass(expected, BaseException): + with pytest.raises(expected): + await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) + else: + await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "args, ret, expected", + [ + ("set chemistry-value --calcium-hardness 350", 0, "740"), + ("set chemistry-value --cyanuric-acid 55", 0, "36"), + ("set chemistry-value -ta 80 -tds 1000", 0, "70 1000"), + ( + "-v set cv -ch 505 -cya 50", + 0, + EXPECTED_VERBOSE_PREAMBLE + "Calcium Hardness: 740 Cyanuric Acid: 36", + ), + ("set cv", 129, "No new chemistry values. Nothing to do."), + ("set cv -cya seven", 2, SystemExit), + ("set cv -ta 900", 128, "900 not in range 25-800"), + ], +) +async def test_set_chemistry_value(MockProtocolAdapter, capsys, args, ret, expected): + if type(expected) == type and issubclass(expected, BaseException): + with pytest.raises(expected): + await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) + else: + await run_cli_test(MockProtocolAdapter, capsys, args, ret, expected) diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 165409a..8db20c0 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -196,7 +196,7 @@ def test_get_strict(MockConnectedGateway: ScreenLogicGateway): @pytest.mark.asyncio async def test_async_set_circuit( - event_loop: asyncio.AbstractEventLoop, MockConnectedGateway + event_loop: asyncio.AbstractEventLoop, MockConnectedGateway: ScreenLogicGateway ): circuit_id = 505 circuit_state = 1 @@ -294,7 +294,7 @@ async def attempt_request(): @pytest.mark.asyncio async def test_async_set_heat_temp( - event_loop: asyncio.AbstractEventLoop, MockConnectedGateway + event_loop: asyncio.AbstractEventLoop, MockConnectedGateway: ScreenLogicGateway ): body = 0 temp = 88 @@ -315,7 +315,7 @@ async def test_async_set_heat_temp( @pytest.mark.asyncio async def test_async_set_heat_mode( - event_loop: asyncio.AbstractEventLoop, MockConnectedGateway + event_loop: asyncio.AbstractEventLoop, MockConnectedGateway: ScreenLogicGateway ): body = 0 mode = 3 @@ -336,7 +336,7 @@ async def test_async_set_heat_mode( @pytest.mark.asyncio async def test_async_set_color_lights( - event_loop: asyncio.AbstractEventLoop, MockConnectedGateway + event_loop: asyncio.AbstractEventLoop, MockConnectedGateway: ScreenLogicGateway ): mode = 7 color_lights_code = 12556 @@ -356,10 +356,10 @@ async def test_async_set_color_lights( @pytest.mark.asyncio async def test_async_set_scg_config( - event_loop: asyncio.AbstractEventLoop, MockConnectedGateway + event_loop: asyncio.AbstractEventLoop, MockConnectedGateway: ScreenLogicGateway ): - pool_output = 50 - spa_output = 0 + pool_pct = 50 + spa_pct = 0 scg_code = 12576 result = event_loop.create_future() @@ -369,11 +369,39 @@ async def test_async_set_scg_config( return_value=result, ) as mockRequest: gateway = MockConnectedGateway - assert await gateway.async_set_scg_config(pool_output, spa_output) + assert await gateway.async_set_scg_config( + pool_setpoint=pool_pct, spa_setpoint=spa_pct + ) await gateway.async_disconnect() assert mockRequest.call_args.args[0] == scg_code assert mockRequest.call_args.args[1] == struct.pack( - " Date: Sat, 14 Oct 2023 19:08:12 -0700 Subject: [PATCH 02/53] Refactor tests to use json data sets that can be generated from the CLI (#54) * Test Fixture refactor * Initial data sets * Save adapter version debug response * ScreenLogicResponseCollection * Data sets for test_decode * repr tuples in json * Data export CLI option * Test refactor to use json data * Move response handling into individual methods for patching. * Remove unused alt login function * Standardize mock protocol adapter naming * Test login coverage * Simplify process_discovery_response * Test discovery coverage * cleanup * Test data coverage * SLIntEnum cleanup * Remove CLIFormat --- screenlogicpy/cli.py | 65 +- screenlogicpy/const/common.py | 5 +- screenlogicpy/data.py | 128 + screenlogicpy/discovery.py | 61 +- screenlogicpy/gateway.py | 2 +- screenlogicpy/requests/gateway.py | 1 + screenlogicpy/requests/login.py | 13 - tests/adapter.py | 153 ++ tests/conftest.py | 124 +- tests/const_data.py | 30 +- ...-52-build-7360-rel_easytouch2-8_98360.json | 2049 +++++++++++++++++ ...2-build-7380-rel_intellitouch-i53s_60.json | 1768 ++++++++++++++ ...52-build-7380-rel_intellitouch-i73_56.json | 1818 +++++++++++++++ ...-52-build-7360-rel_easytouch2-8_98360.json | 2049 +++++++++++++++++ tests/fake_gateway.py | 179 -- tests/test_cli.py | 498 ++-- tests/test_client.py | 44 +- tests/test_data.py | 254 +- tests/test_decode.py | 75 +- tests/test_discovery.py | 95 +- tests/test_gateway.py | 542 ++--- tests/test_login.py | 165 +- tests/test_protocol.py | 37 +- 23 files changed, 9076 insertions(+), 1079 deletions(-) create mode 100644 screenlogicpy/data.py create mode 100644 tests/adapter.py create mode 100644 tests/data/pool-52-build-7360-rel_easytouch2-8_98360.json create mode 100644 tests/data/pool-52-build-7380-rel_intellitouch-i53s_60.json create mode 100644 tests/data/pool-52-build-7380-rel_intellitouch-i73_56.json create mode 100644 tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json delete mode 100644 tests/fake_gateway.py diff --git a/screenlogicpy/cli.py b/screenlogicpy/cli.py index a17d2c4..c2067ec 100644 --- a/screenlogicpy/cli.py +++ b/screenlogicpy/cli.py @@ -2,6 +2,7 @@ import string import json import argparse +from screenlogicpy import __version__ from screenlogicpy.discovery import async_discover from screenlogicpy.gateway import ScreenLogicGateway from screenlogicpy.const.common import ( @@ -13,36 +14,20 @@ ScreenLogicError, ScreenLogicWarning, ) +from screenlogicpy.data import build_response_collection, export_response_collection from screenlogicpy.device_const.chemistry import CHEM_RANGE +from screenlogicpy.device_const.circuit import INTERFACE from screenlogicpy.device_const.heat import HEAT_MODE from screenlogicpy.device_const.system import BODY_TYPE, COLOR_MODE from screenlogicpy.device_const.scg import SCG_RANGE from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE -def cliFormat(name: str): - table = str.maketrans(" ", "_", string.punctuation) +def file_format(name: str): + table = str.maketrans(" ", "-", string.punctuation) return name.translate(table).lower() -def cliFormatDict(mapping: dict): - return { - cliFormat(key) - if isinstance(key, str) - else key: cliFormat(value) - if isinstance(value, str) - else value - for key, value in mapping.items() - } - - -def optionsFromDict(mapping: dict): - options = [] - for key, value in cliFormatDict(mapping).items(): - options.extend((str(key), str(value))) - return options - - # Entry function async def cli(cli_args): """Handle command line args""" @@ -276,6 +261,18 @@ async def async_set_chem_data( return 0 return 128 + async def async_export_data_collection(): + sl_ver = file_format(__version__) + pa_ver = file_format(gateway.version) + model = file_format(gateway.controller_model) + equip = gateway.equipment_flags.value + filename = f"slpy{sl_ver}_{pa_ver}_{model}_{equip}.json" + response_collection = build_response_collection( + gateway.get_debug(), gateway.get_data() + ) + export_response_collection(response_collection, filename) + return 0 + # Begin Parser Setup async def async_get_json(): print(json.dumps(gateway.get_data(), indent=2)) @@ -309,6 +306,9 @@ async def async_get_json(): "discover", help="Attempt to discover all available ScreenLogic gateways" ) + # pylint: disable=unused-variable + export_parser = subparsers.add_parser("export") # noqa F841 + # Get options get_parser = subparsers.add_parser("get", help="Gets the specified value or state") get_subparsers = get_parser.add_subparsers(dest="get_option") @@ -326,7 +326,7 @@ async def async_get_json(): get_circuit_parser.add_argument(**ARGUMENT_CIRCUIT_NUM) get_circuit_parser.set_defaults(async_func=async_get_circuit) - body_options = BODY_TYPE.parsable() + body_options = BODY_TYPE.parsable_values() ARGUMENT_BODY = { "dest": "body", "metavar": "BODY", @@ -376,7 +376,7 @@ async def async_get_json(): set_subparsers = set_parser.add_subparsers(dest="set_option") set_subparsers.required = True - on_off_options = ON_OFF.parsable() + on_off_options = ON_OFF.parsable_values() set_circuit_parser = set_subparsers.add_parser( "circuit", aliases=["c"], @@ -391,7 +391,7 @@ async def async_get_json(): help=f"State to set. One of {on_off_options}", ) - cl_options = COLOR_MODE.parsable() + cl_options = COLOR_MODE.parsable_values() set_circuit_parser.set_defaults(async_func=async_set_circuit) set_color_light_parser = set_subparsers.add_parser( "color-lights", @@ -413,7 +413,7 @@ async def async_get_json(): help="Set the specified heat mode for the specified body", ) set_heat_mode_parser.add_argument(**ARGUMENT_BODY) - hm_options = HEAT_MODE.parsable() + hm_options = HEAT_MODE.parsable_values() set_heat_mode_parser.add_argument( "mode", metavar="MODE", @@ -606,6 +606,10 @@ async def async_get_json(): if DEVICE.CONTROLLER not in gateway.get_data(): return 1 + if args.action == "export": + result = await async_export_data_collection() + return result + def print_gateway(): verb = "Discovered" if discovered else "Using" print( @@ -619,13 +623,14 @@ def print_circuits(): print("{} {} {}".format("ID".rjust(3), "STATE", "NAME")) print("--------------------------") for id, circuit in gateway.get_data(DEVICE.CIRCUIT).items(): - print( - "{} {} {}".format( - id, - ON_OFF(circuit[ATTR.VALUE]).title.rjust(5), - circuit[ATTR.NAME], + if circuit[ATTR.INTERFACE] != INTERFACE.DONT_SHOW: + print( + "{} {} {}".format( + id, + ON_OFF(circuit[ATTR.VALUE]).title.rjust(5), + circuit[ATTR.NAME], + ) ) - ) def print_heat(): for body in gateway.get_data(DEVICE.BODY).values(): diff --git a/screenlogicpy/const/common.py b/screenlogicpy/const/common.py index 6e83c4a..f2d8936 100644 --- a/screenlogicpy/const/common.py +++ b/screenlogicpy/const/common.py @@ -55,12 +55,11 @@ def parse(cls, value: str, default=0) -> SLIntEnum: return None if default is None else cls(default) @classmethod - def parsable(cls) -> tuple: + def parsable_values(cls) -> tuple: """Return a tuple of all parsable values.""" out = [] for member in cls: - out.append(str(member.value)) - out.append(member.name.lower()) + out.extend([str(member.value), member.name.lower()]) return tuple(out) def _title(self) -> str: diff --git a/screenlogicpy/data.py b/screenlogicpy/data.py new file mode 100644 index 0000000..0414d8b --- /dev/null +++ b/screenlogicpy/data.py @@ -0,0 +1,128 @@ +from dataclasses import asdict, dataclass +import json +from typing import Any + +from .const.common import DATA_REQUEST +from .requests.chemistry import decode_chemistry +from .requests.config import decode_pool_config +from .requests.gateway import decode_version +from .requests.lights import decode_color_update +from .requests.pump import decode_pump_status +from .requests.scg import decode_scg_config +from .requests.status import decode_pool_status + +REQUEST_DECODE_FUNCS = { + DATA_REQUEST.VERSION: decode_version, + DATA_REQUEST.CONFIG: decode_pool_config, + DATA_REQUEST.STATUS: decode_pool_status, + DATA_REQUEST.PUMPS: decode_pump_status, + DATA_REQUEST.CHEMISTRY: decode_chemistry, + DATA_REQUEST.SCG: decode_scg_config, + DATA_REQUEST.KEY_COLOR: decode_color_update, +} + + +@dataclass(frozen=True) +class ScreenLogicResponseSet: + raw: bytes + decoded: dict + + +@dataclass(frozen=True) +class ScreenLogicResponseCollection: + decoded_complete: dict + version: ScreenLogicResponseSet | None = None + config: ScreenLogicResponseSet | None = None + status: ScreenLogicResponseSet | None = None + pumps: list[ScreenLogicResponseSet] = None + chemistry: ScreenLogicResponseSet | None = None + scg: ScreenLogicResponseSet | None = None + color: list[ScreenLogicResponseSet] = None + + +def build_response_collection(raw: dict, data: dict) -> ScreenLogicResponseCollection: + SLResponseColArgs = {} + + for req, dec_func in REQUEST_DECODE_FUNCS.items(): + if raw_resp := raw.get(req): + if isinstance(raw_resp, dict): + resp_sets = [] + for idx, raw_resp_i in raw_resp.items(): + dec_resp_i = {} + dec_func(raw_resp_i, dec_resp_i, idx) + resp_sets.append(ScreenLogicResponseSet(raw_resp_i, dec_resp_i)) + SLResponseColArgs[req] = resp_sets + else: + dec_resp = {} + dec_func(raw_resp, dec_resp) + SLResponseColArgs[req] = ScreenLogicResponseSet(raw_resp, dec_resp) + + return ScreenLogicResponseCollection(data, **SLResponseColArgs) + + +T_KEY = "__type" + + +def int_key_json_decoder(o: dict) -> Any: + if isinstance(o, dict): + strkeys = [] + for key in o.keys(): + if isinstance(key, str): + if key.isdigit(): + strkeys.append(key) + for strkey in strkeys: + o[int(strkey)] = o.pop(strkey) + return o + + +# Patch for color RGB tuples +def value_list_json_decoder(o: dict) -> Any: + if "value" in o: + value = o["value"] + if isinstance(value, list) and len(value) == 3: + o["value"] = tuple(value) + return int_key_json_decoder(o) + + +def response_set_json_decoder(o: dict) -> Any: + if "decoded" in o and "raw" in o: + return ScreenLogicResponseSet(**o) + return value_list_json_decoder(o) + + +def bytes_json_decoder(o: dict) -> Any: + if T_KEY in o: + return eval(o["repr"]) + return response_set_json_decoder(o) + + +def read_sl_data_json(filename: str) -> dict: + with open(filename, "r", encoding="utf-8") as fp: + return json.load(fp, object_hook=bytes_json_decoder) + + +def bytes_json_encoder(o: Any) -> Any: + if isinstance(o, bytes): + return {T_KEY: repr(type(o)), "repr": repr(o)} + return o + + +def write_sl_data_json(filename: str, data: dict) -> None: + with open(filename, "w", encoding="utf-8") as fp: + json.dump( + data, + fp, + default=bytes_json_encoder, + ensure_ascii=False, + indent=2, + ) + + +def import_response_collection(filename: str) -> ScreenLogicResponseCollection: + return ScreenLogicResponseCollection(**read_sl_data_json(filename)) + + +def export_response_collection( + response_collection: ScreenLogicResponseCollection, filename: str +) -> None: + write_sl_data_json(filename, asdict(response_collection)) diff --git a/screenlogicpy/discovery.py b/screenlogicpy/discovery.py index 1beef82..526f050 100644 --- a/screenlogicpy/discovery.py +++ b/screenlogicpy/discovery.py @@ -1,6 +1,5 @@ """Discovery for screenlogic gateways.""" import asyncio -import ipaddress import logging import socket import struct @@ -13,8 +12,9 @@ SL_GATEWAY_TYPE, ScreenLogicError, ) +from .requests.utility import getSome -DISCOVERY_PAYLOAD = struct.pack(" bytes: return struct.pack(fmt, schema, connectionType, clientVersion, passwd, pid) -def create_local_login_message() -> bytes: - schema = 348 - connectionType = 0 - clientVersion = encodeMessageString("Local Config") - passwdPayload = b"\x10\x00\x00\x00\x48\x9e\x60\x3a\xc3\x1d\xb9\xb1\x0c\xc1\x4a\x37\x50\x97\xa8\x22" - mac = encodeMessageString("00-00-00-00-00-00") - pid = 2 - fmt = f" str: diff --git a/tests/adapter.py b/tests/adapter.py new file mode 100644 index 0000000..e3de7d0 --- /dev/null +++ b/tests/adapter.py @@ -0,0 +1,153 @@ +import asyncio +from enum import IntEnum +from dataclasses import dataclass +import struct +from typing import Any + +from screenlogicpy.const.msg import CODE +from screenlogicpy.data import ScreenLogicResponseCollection +from screenlogicpy.requests.utility import encodeMessageString, makeMessage, takeMessage + +from tests.const_data import ( + FAKE_GATEWAY_MAC, +) + + +@dataclass +class SLMessage: + id: int + code: int + data: bytes = b"" + + +class CONNECTION_STATE(IntEnum): + NO_CONNECTION = 0 + CONNECTSERVERHOST = 1 + CHALLENGE = 2 + LOGIN = 3 + + +class FakeTCPProtocolAdapter(asyncio.Protocol): + def __init__(self, responses: ScreenLogicResponseCollection) -> None: + self.responses = responses + self._cs = CONNECTION_STATE.NO_CONNECTION + + self.unconnected_response_map = { + CODE.CHALLENGE_QUERY: self.handle_challenge_request, + CODE.LOCALLOGIN_QUERY: self.handle_logon_request, + } + self.connected_response_map = { + CODE.VERSION_QUERY: self.handle_version_request, + CODE.CTRLCONFIG_QUERY: self.handle_config_request, + CODE.POOLSTATUS_QUERY: self.handle_status_request, + CODE.PUMPSTATUS_QUERY: self.handle_pump_state_request, + CODE.CHEMISTRY_QUERY: self.handle_chemistry_status_request, + CODE.SCGCONFIG_QUERY: self.handle_scg_status_request, + CODE.BUTTONPRESS_QUERY: self.handle_button_press_request, + CODE.LIGHTCOMMAND_QUERY: self.handle_light_command_request, + CODE.SETHEATMODE_QUERY: self.handle_set_heat_mode_request, + CODE.SETHEATTEMP_QUERY: self.handle_set_heat_temp_request, + CODE.SETSCG_QUERY: self.handle_set_scg_config_request, + CODE.SETCHEMDATA_QUERY: self.handle_set_chemistry_config_request, + CODE.ADD_CLIENT_QUERY: self.handle_add_client_request, + CODE.REMOVE_CLIENT_QUERY: self.handle_remove_client_request, + CODE.PING_QUERY: self.handle_ping_request, + } + + def connection_made(self, transport: asyncio.Transport) -> None: + self.transport = transport + + def data_received(self, data: bytes) -> None: + resp: SLMessage + if (resp := self.process_data(data)) is not None: + self.transport.write(makeMessage(resp.id, resp.code, resp.data)) + + def process_data(self, data: bytes) -> bytes: + if self._cs == CONNECTION_STATE.NO_CONNECTION: + if data == b"CONNECTSERVERHOST\r\n\r\n": + self._cs = CONNECTION_STATE.CONNECTSERVERHOST + return None + return self.process_message(SLMessage(*takeMessage(data))) + + def process_message(self, msg: SLMessage) -> bytes: + if self._cs == CONNECTION_STATE.LOGIN: + if (code_handler := self.connected_response_map.get(msg.code)) is not None: + return code_handler(msg) + else: + if ( + logon_handler := self.unconnected_response_map.get(msg.code) + ) is not None: + return logon_handler(msg) + return SLMessage(msg.id, CODE.ERROR_INVALID_REQUEST) + + def handle_challenge_request(self, msg: SLMessage) -> SLMessage: + if self._cs == CONNECTION_STATE.CONNECTSERVERHOST: + self._cs = CONNECTION_STATE.CHALLENGE + return SLMessage( + msg.id, msg.code + 1, encodeMessageString(FAKE_GATEWAY_MAC) + ) + return SLMessage(msg.id, CODE.ERROR_BAD_PARAMETER) + + def handle_logon_request(self, msg: SLMessage) -> SLMessage: + if self._cs == CONNECTION_STATE.CHALLENGE: + self._cs = CONNECTION_STATE.LOGIN + return SLMessage(msg.id, msg.code + 1) + return SLMessage(msg.id, CODE.ERROR_LOGIN_REJECTED) + + def handle_version_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1, self.responses.version.raw) + + def handle_config_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1, self.responses.config.raw) + + def handle_status_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1, self.responses.status.raw) + + def handle_pump_state_request(self, msg: SLMessage) -> SLMessage: + pump = struct.unpack_from(" SLMessage: + return SLMessage(msg.id, msg.code + 1, self.responses.chemistry.raw) + + def handle_scg_status_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1, self.responses.scg.raw) + + def handle_button_press_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + def handle_light_command_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + def handle_set_heat_mode_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + def handle_set_heat_temp_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + def handle_set_scg_config_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + def handle_set_chemistry_config_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + def handle_add_client_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + def handle_remove_client_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + def handle_ping_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + +class FakeUDPProtocolAdapter(asyncio.DatagramProtocol): + def __init__(self, discovery_response: bytes) -> None: + self.discovery_response = discovery_response + + def connection_made(self, transport: asyncio.DatagramTransport) -> None: + self.transport = transport + + def datagram_received(self, data: bytes, addr: tuple[str | Any, int]) -> None: + if struct.unpack("<8b", data) == (1, 0, 0, 0, 0, 0, 0, 0): + self.transport.sendto(self.discovery_response, addr) diff --git a/tests/conftest.py b/tests/conftest.py index d586e87..a79e24c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,23 +1,53 @@ import asyncio +from collections.abc import Callable +import os import pytest_asyncio +import socket +import struct +from unittest.mock import DEFAULT, MagicMock, patch +from screenlogicpy import ScreenLogicGateway +from screenlogicpy.data import ScreenLogicResponseCollection, import_response_collection +from screenlogicpy.discovery import DISCOVERY_PORT +from screenlogicpy.requests.protocol import ScreenLogicProtocol + +from .adapter import FakeTCPProtocolAdapter, FakeUDPProtocolAdapter from .const_data import ( FAKE_GATEWAY_ADDRESS, + FAKE_GATEWAY_CHK, FAKE_GATEWAY_PORT, FAKE_CONNECT_INFO, + FAKE_GATEWAY_MAC, + FAKE_GATEWAY_NAME, + FAKE_GATEWAY_SUB_TYPE, + FAKE_GATEWAY_TYPE, ) -from .fake_gateway import ( - DisconnectingFakeScreenLogicTCPProtocol, - FailingFakeScreenLogicTCPProtocol, - FakeScreenLogicTCPProtocol, -) -from screenlogicpy import ScreenLogicGateway + + +DEFAULT_RESPONSE = "slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json" + + +def load_response_collection(filenames: list[str] | None = None): + dir = "tests/data/" + files = filenames or os.listdir(dir) + resp_colls = [] + for file in files: + resp_colls.append(import_response_collection(f"{dir}{file}")) + return resp_colls @pytest_asyncio.fixture() -async def MockProtocolAdapter(event_loop: asyncio.AbstractEventLoop): +async def response_collection(): + return load_response_collection([DEFAULT_RESPONSE])[0] + + +@pytest_asyncio.fixture() +async def MockProtocolAdapter( + event_loop: asyncio.AbstractEventLoop, + response_collection: ScreenLogicResponseCollection, +): server = await event_loop.create_server( - lambda: FakeScreenLogicTCPProtocol(), + lambda: FakeTCPProtocolAdapter(response_collection), FAKE_GATEWAY_ADDRESS, FAKE_GATEWAY_PORT, reuse_address=True, @@ -25,44 +55,74 @@ async def MockProtocolAdapter(event_loop: asyncio.AbstractEventLoop): async with server: yield server - print("Closing Server") server.close() -@pytest_asyncio.fixture() -async def FailingMockProtocolAdapter(event_loop: asyncio.AbstractEventLoop): - server = await event_loop.create_server( - lambda: FailingFakeScreenLogicTCPProtocol(), - FAKE_GATEWAY_ADDRESS, +@pytest_asyncio.fixture +async def discovery_response() -> bytes: + return struct.pack( + f" bool: + """Initialize minimum attributes needed for tests.""" + if self.is_connected: + return True + + self._ip = ip + self._port = port + self._type = gtype + self._subtype = gsubtype + self._name = name + self._custom_connection_closed_callback = connection_closed_callback + self._mac = FAKE_GATEWAY_MAC + self._protocol = MagicMock(spec=ScreenLogicProtocol, _connected=True) + self._data = data + + return True @pytest_asyncio.fixture() -async def MockConnectedGateway(MockProtocolAdapter): - async with MockProtocolAdapter: +async def MockConnectedGateway(response_collection: ScreenLogicResponseCollection): + with patch.multiple( + ScreenLogicGateway, + async_connect=lambda *args, **kwargs: stub_async_connect( + response_collection.decoded_complete, *args, **kwargs + ), + async_disconnect=DEFAULT, + # get_debug=lambda self: {}, + ): gateway = ScreenLogicGateway() await gateway.async_connect(**FAKE_CONNECT_INFO) assert gateway.is_connected - await gateway.async_update() yield gateway diff --git a/tests/const_data.py b/tests/const_data.py index fe6736c..e56866e 100644 --- a/tests/const_data.py +++ b/tests/const_data.py @@ -51,13 +51,13 @@ EXPECTED_DASHBOARD = """Discovered 'Fake: 00-00-00' at 127.0.0.1:6448 EasyTouch2 8 ************************** -Pool temperature is last 56°F -Pool Heat Set Point: 86°F +Pool temperature is last 69°F +Pool Heat Set Point: 83°F Pool Heat: Off Pool Heat Mode: Off -------------------------- -Spa temperature is last 97°F -Spa Heat Set Point: 97°F +Spa temperature is last 84°F +Spa Heat Set Point: 95°F Spa Heat: Off Spa Heat Mode: Heater -------------------------- @@ -66,22 +66,14 @@ -------------------------- 500 Off Spa 501 Off Waterfall -502 On Pool Light -503 On Spa Light -504 On Cleaner -505 Off Pool Low -506 On Yard Light -507 Off Aux 6 +502 Off Pool Light +503 Off Spa Light +504 Off Cleaner +505 On Pool Low +506 Off Yard Light +507 On Cameras 508 Off Pool High -510 Off Feature 1 -511 Off Feature 2 -512 Off Feature 3 -513 Off Feature 4 -514 Off Feature 5 -515 Off Feature 6 -516 Off Feature 7 -517 Off Feature 8 -519 Off AuxEx +510 Off Spillway **************************""" EXPECTED_VERBOSE_PREAMBLE = """Discovered 'Fake: 00-00-00' at 127.0.0.1:6448 diff --git a/tests/data/pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/pool-52-build-7360-rel_easytouch2-8_98360.json new file mode 100644 index 0000000..45c2ff5 --- /dev/null +++ b/tests/data/pool-52-build-7360-rel_easytouch2-8_98360.json @@ -0,0 +1,2049 @@ +{ + "decoded_complete": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel" + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0, + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 98360, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM", + "HYBRID_HEATER" + ] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.62, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 778, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 3, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 2, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0, + "delay": 0 + }, + "function": 5, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 1 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0, + "delay": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7, + "value": 0 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8, + "value": 1 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0, + "delay": 0 + }, + "function": 0, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9, + "value": 1 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0, + "delay": 0 + }, + "function": 14, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11, + "value": 0 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12, + "value": 0 + } + }, + "pump": { + "0": { + "data": 70, + "type": 3, + "state": { + "name": "Pool Low Pump", + "value": 1 + }, + "watts_now": { + "name": "Pool Low Pump Watts Now", + "value": 1548, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Low Pump RPM Now", + "value": 2887, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Pool Low Pump GPM Now", + "value": 72, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "1": { + "data": 66, + "type": 3, + "state": { + "name": "Waterfall Pump", + "value": 0 + }, + "watts_now": { + "name": "Waterfall Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Waterfall Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Waterfall Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 75, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 95, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 69, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 3, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 7.62, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 778, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 3, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 2, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 75, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 13, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 4, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 22, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 149, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 51, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + }, + "version": { + "raw": { + "__type": "", + "repr": "b'\\x19\\x00\\x00\\x00POOL: 5.2 Build 736.0 Rel\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x0c\\x00\\x00\\x00'" + }, + "decoded": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel" + } + } + } + }, + "config": { + "raw": { + "__type": "", + "repr": "b'd\\x00\\x00\\x00(h(h\\x00\\r\\x00\\x008\\x80\\xff\\xff\\x0e\\x00\\x00\\x00Water Features\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x03\\x00\\x00\\x00Spa\\x00G\\x01\\x01\\x01\\x00\\x00\\x00\\x01\\xd0\\x02\\x00\\x00\\xf5\\x01\\x00\\x00\\t\\x00\\x00\\x00Waterfall\\x00\\x00\\x00U\\x00\\x02\\x00\\x00\\x00\\x00\\x02\\xd0\\x02\\x00\\x00\\xf6\\x01\\x00\\x00\\n\\x00\\x00\\x00Pool Light\\x00\\x00>\\x10\\x03\\x00\\x02\\x00\\x02\\x03\\xd0\\x02\\x00\\x00\\xf7\\x01\\x00\\x00\\t\\x00\\x00\\x00Spa Light\\x00\\x00\\x00I\\x10\\x03\\x00\\x06\\x01\\n\\x04\\xd0\\x02\\x00\\x00\\xf8\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cleaner\\x00\\x15\\x05\\x00\\x00\\x00\\x00\\x00\\x05\\xf0\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x08\\x00\\x00\\x00Pool Low?\\x02\\x00\\x01\\x00\\x00\\x00\\x06\\xd0\\x02\\x00\\x00\\xfa\\x01\\x00\\x00\\n\\x00\\x00\\x00Yard Light\\x00\\x00[\\x07\\x04\\x00\\x00\\x00\\x00\\x07\\xd0\\x02\\x00\\x00\\xfb\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cameras\\x00e\\x00\\x02\\x00\\x00\\x00\\x00\\x08T\\x06\\x00\\x00\\xfc\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x00\\x00\\x00\\x00\\x00\\t\\xd0\\x02\\x00\\x00\\xfe\\x01\\x00\\x00\\x08\\x00\\x00\\x00SpillwayN\\x0e\\x01\\x00\\x00\\x00\\x00\\x0b\\xd0\\x02\\x00\\x00\\xff\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x05\\x00\\x00\\x00\\x00\\x0c\\xd0\\x02\\x00\\x00\\x08\\x00\\x00\\x00\\x05\\x00\\x00\\x00White\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x0b\\x00\\x00\\x00Light Green\\x00\\xa0\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xa0\\x00\\x00\\x00\\x05\\x00\\x00\\x00Green\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00P\\x00\\x00\\x00\\x04\\x00\\x00\\x00Cyan\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xc8\\x00\\x00\\x00\\x04\\x00\\x00\\x00Blued\\x00\\x00\\x00\\x8c\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x08\\x00\\x00\\x00Lavender\\xe6\\x00\\x00\\x00\\x82\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x07\\x00\\x00\\x00Magenta\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\r\\x00\\x00\\x00Light Magenta\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xb4\\x00\\x00\\x00\\xd2\\x00\\x00\\x00FB\\x00\\x00\\x00\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 98360, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM", + "HYBRID_HEATER" + ] + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0 + }, + "function": 5, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0 + }, + "function": 0, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0 + }, + "function": 14, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12 + } + }, + "pump": { + "0": { + "data": 70 + }, + "1": { + "data": 66 + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + } + } + }, + "status": { + "raw": { + "__type": "", + "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00E\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00K\\x00\\x00\\x00\\x00\\x00\\x00\\x00S\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00T\\x00\\x00\\x00\\x00\\x00\\x00\\x00_\\x00\\x00\\x00E\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x02\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01\\n\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfb\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfc\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfe\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x02\\x00\\x00\\n\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.62, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 778, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 3, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 2, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + }, + "configuration": { + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 75, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 95, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 69, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 3, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "501": { + "circuit_id": 501, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "502": { + "circuit_id": 502, + "value": 0, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "configuration": { + "delay": 0 + } + }, + "503": { + "circuit_id": 503, + "value": 0, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "configuration": { + "delay": 0 + } + }, + "504": { + "circuit_id": 504, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "505": { + "circuit_id": 505, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "506": { + "circuit_id": 506, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "507": { + "circuit_id": 507, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "508": { + "circuit_id": 508, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "510": { + "circuit_id": 510, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "511": { + "circuit_id": 511, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + } + } + } + }, + "pumps": [ + { + "raw": { + "__type": "", + "repr": "b'\\x03\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x0c\\x06\\x00\\x00G\\x0b\\x00\\x00\\x00\\x00\\x00\\x00H\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x06\\x00\\x00\\x00?\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00z\\r\\x00\\x00\\x01\\x00\\x00\\x00\\x82\\x00\\x00\\x00K\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "0": { + "type": 3, + "state": { + "name": "Default Pump", + "value": 1 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 1548, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 2887, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 72, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + } + } + } + }, + { + "raw": { + "__type": "", + "repr": "b'\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x8c\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "1": { + "type": 3, + "state": { + "name": "Default Pump", + "value": 0 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + } + } + } + } + ], + "chemistry": { + "raw": { + "__type": "", + "repr": "b'*\\x00\\x00\\x00\\x00\\x02\\xfa\\x03\\n\\x02\\xf8\\x02\\xd0\\x00\\x00\\x00\\r\\x00\\x00\\x00\\x04\\x00\\x16\\x00\\x08\\x03\\x02\\x00\\x03 \\x00-\\x00-\\x14\\x00K\\x00\\x00\\x95 <\\x01\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 7.62, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 778, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 3, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 2, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 75, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 13, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 4, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 22, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 149, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + } + } + }, + "scg": { + "raw": { + "__type": "", + "repr": "b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x003\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 51, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + } + }, + "color": null +} \ No newline at end of file diff --git a/tests/data/pool-52-build-7380-rel_intellitouch-i53s_60.json b/tests/data/pool-52-build-7380-rel_intellitouch-i53s_60.json new file mode 100644 index 0000000..551f85a --- /dev/null +++ b/tests/data/pool-52-build-7380-rel_intellitouch-i53s_60.json @@ -0,0 +1,1768 @@ +{ + "decoded_complete": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 738.0 Rel" + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 0, + "hardware_type": 0, + "controller_data": 32, + "generic_circuit_name": "Water Features", + "circuit_count": 6, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 1, + "remotes": 32, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "IntelliTouch i5+3S" + }, + "equipment": { + "flags": 60, + "list": [ + "CHLORINATOR", + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1" + ] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 79, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 1, + "device_type": "alarm" + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_154": 0, + "unknown_at_offset_155": 0, + "delay": 0 + }, + "function": 5, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Jets", + "configuration": { + "name_index": 45, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_178": 0, + "unknown_at_offset_179": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool", + "configuration": { + "name_index": 60, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_202": 0, + "unknown_at_offset_203": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 1 + } + }, + "pump": { + "0": { + "data": 134, + "type": 2, + "state": { + "name": "Pool Pump", + "value": 1 + }, + "watts_now": { + "name": "Pool Pump Watts Now", + "value": 68, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Pump RPM Now", + "value": 1100, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 1100, + "is_rpm": 1 + }, + "1": { + "device_id": 2, + "setpoint": 2300, + "is_rpm": 1 + }, + "2": { + "device_id": 1, + "setpoint": 2070, + "is_rpm": 1 + }, + "3": { + "device_id": 4, + "setpoint": 2440, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + }, + "1": { + "data": 133, + "type": 2, + "state": { + "name": "Jets Pump", + "value": 0 + }, + "watts_now": { + "name": "Jets Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Jets Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Jets Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 5, + "setpoint": 2800, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "2": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "3": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 80, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 84, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 83, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 103, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 79, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 0, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 0.0, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 0, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 0, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 0, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 0, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 0, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 0 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 0, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "0.000" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 1, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 50, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + }, + "version": null, + "config": { + "raw": { + "__type": "", + "repr": "b'd\\x00\\x00\\x00(h(h\\x00\\x00\\x00 <\\x00\\x00\\x00\\x0e\\x00\\x00\\x00Water Features\\x00\\x00\\x06\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x03\\x00\\x00\\x00Spa\\x00G\\x01\\x01\\x01\\x00\\x00\\x00\\x01\\xd0\\x02\\x00\\x00\\xf5\\x01\\x00\\x00\\n\\x00\\x00\\x00Pool Light\\x00\\x00>\\x10\\x03\\x00\\x00\\x00\\x00\\x02\\xd0\\x02\\x00\\x00\\xf6\\x01\\x00\\x00\\t\\x00\\x00\\x00Spa Light\\x00\\x00\\x00I\\x10\\x03\\x00\\x00\\x00\\x00\\x03\\xd0\\x02\\x00\\x00\\xf7\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cleaner\\x00\\x15\\x05\\x05\\x00\\x00\\x00\\x00\\x04\\xd0\\x02\\x00\\x00\\xf8\\x01\\x00\\x00\\x04\\x00\\x00\\x00Jets-\\x00\\x05\\x00\\x00\\x00\\x00\\x05\\xd0\\x02\\x00\\x00\\xf9\\x01\\x00\\x00\\x04\\x00\\x00\\x00Pool<\\x02\\x00\\x01\\x00\\x00\\x00\\x06\\xd0\\x02\\x00\\x00\\x08\\x00\\x00\\x00\\x05\\x00\\x00\\x00White\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x0b\\x00\\x00\\x00Light Green\\x00\\xa0\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xa0\\x00\\x00\\x00\\x05\\x00\\x00\\x00Green\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00P\\x00\\x00\\x00\\x04\\x00\\x00\\x00Cyan\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xc8\\x00\\x00\\x00\\x04\\x00\\x00\\x00Blued\\x00\\x00\\x00\\x8c\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x08\\x00\\x00\\x00Lavender\\xe6\\x00\\x00\\x00\\x82\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x07\\x00\\x00\\x00Magenta\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\r\\x00\\x00\\x00Light Magenta\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xb4\\x00\\x00\\x00\\xd2\\x00\\x00\\x00\\x86\\x85\\x00\\x00\\x00\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 0, + "hardware_type": 0, + "controller_data": 32, + "generic_circuit_name": "Water Features", + "circuit_count": 6, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 1 + }, + "model": { + "name": "Model", + "value": "IntelliTouch i5+3S" + }, + "equipment": { + "flags": 60, + "list": [ + "CHLORINATOR", + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1" + ] + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1 + }, + "501": { + "circuit_id": 501, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2 + }, + "502": { + "circuit_id": 502, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 3 + }, + "503": { + "circuit_id": 503, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_154": 0, + "unknown_at_offset_155": 0 + }, + "function": 5, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 4 + }, + "504": { + "circuit_id": 504, + "name": "Jets", + "configuration": { + "name_index": 45, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_178": 0, + "unknown_at_offset_179": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5 + }, + "505": { + "circuit_id": 505, + "name": "Pool", + "configuration": { + "name_index": 60, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_202": 0, + "unknown_at_offset_203": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6 + } + }, + "pump": { + "0": { + "data": 134 + }, + "1": { + "data": 133 + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + } + } + }, + "status": { + "raw": { + "__type": "", + "repr": "b'\\x01\\x00\\x00\\x00\\x00 \\x00\\x00\\x00\\x00\\x00\\x00O\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00P\\x00\\x00\\x00\\x00\\x00\\x00\\x00T\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00S\\x00\\x00\\x00\\x00\\x00\\x00\\x00g\\x00\\x00\\x00O\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 79, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 1, + "device_type": "alarm" + } + }, + "configuration": { + "remotes": 32, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 80, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 84, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 83, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 103, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 79, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "501": { + "circuit_id": 501, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "502": { + "circuit_id": 502, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "503": { + "circuit_id": 503, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "504": { + "circuit_id": 504, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "505": { + "circuit_id": 505, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + } + } + } + }, + "pumps": [ + { + "raw": { + "__type": "", + "repr": "b'\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00D\\x00\\x00\\x00L\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x06\\x00\\x00\\x00L\\x04\\x00\\x00\\x01\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\xfc\\x08\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x16\\x08\\x00\\x00\\x01\\x00\\x00\\x00\\x04\\x00\\x00\\x00\\x88\\t\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "0": { + "type": 2, + "state": { + "name": "Default Pump", + "value": 1 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 68, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 1100, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 255, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 1100, + "is_rpm": 1 + }, + "1": { + "device_id": 2, + "setpoint": 2300, + "is_rpm": 1 + }, + "2": { + "device_id": 1, + "setpoint": 2070, + "is_rpm": 1 + }, + "3": { + "device_id": 4, + "setpoint": 2440, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + } + } + } + }, + { + "raw": { + "__type": "", + "repr": "b'\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\xf0\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "1": { + "type": 2, + "state": { + "name": "Default Pump", + "value": 0 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 5, + "setpoint": 2800, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "2": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "3": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + } + } + } + } + ], + "chemistry": { + "raw": { + "__type": "", + "repr": "b'*\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 0, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 0.0, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 0, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 0, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 0, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 0, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 0, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 0 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 0, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "0.000" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + } + } + }, + "scg": { + "raw": { + "__type": "", + "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x002\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "scg": { + "scg_present": 1, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 50, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + } + }, + "color": null +} \ No newline at end of file diff --git a/tests/data/pool-52-build-7380-rel_intellitouch-i73_56.json b/tests/data/pool-52-build-7380-rel_intellitouch-i73_56.json new file mode 100644 index 0000000..8e1dddc --- /dev/null +++ b/tests/data/pool-52-build-7380-rel_intellitouch-i73_56.json @@ -0,0 +1,1818 @@ +{ + "decoded_complete": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 738.0 Rel" + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 1, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 7, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 1, + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "IntelliTouch i7+3" + }, + "equipment": { + "flags": 56, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1" + ] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 100, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_90": 0, + "unknown_at_offset_91": 0, + "delay": 0 + }, + "function": 5, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Jets", + "configuration": { + "name_index": 45, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_114": 0, + "unknown_at_offset_115": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_146": 0, + "unknown_at_offset_147": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 10 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_178": 0, + "unknown_at_offset_179": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool", + "configuration": { + "name_index": 60, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_202": 0, + "unknown_at_offset_203": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 1 + }, + "506": { + "circuit_id": 506, + "name": "Air Blower", + "configuration": { + "name_index": 1, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_234": 0, + "unknown_at_offset_235": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7, + "value": 0 + } + }, + "pump": { + "0": { + "data": 134, + "type": 2, + "state": { + "name": "Pool Pump", + "value": 1 + }, + "watts_now": { + "name": "Pool Pump Watts Now", + "value": 63, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Pump RPM Now", + "value": 1050, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 1050, + "is_rpm": 1 + }, + "1": { + "device_id": 1, + "setpoint": 1850, + "is_rpm": 1 + }, + "2": { + "device_id": 2, + "setpoint": 1500, + "is_rpm": 1 + }, + "3": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + }, + "1": { + "data": 131, + "type": 2, + "state": { + "name": "Jets Pump", + "value": 0 + }, + "watts_now": { + "name": "Jets Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Jets Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Jets Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 3, + "setpoint": 2970, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "2": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "3": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 92, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 85, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 93, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 101, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 0, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 0.0, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 0, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 0, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 0, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 0, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 0, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 0 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 0, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "0.000" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 1 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 50, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + }, + "version": null, + "config": { + "raw": { + "__type": "", + "repr": "b'd\\x00\\x00\\x00(h(h\\x00\\x01\\x00\\x008\\x00\\x00\\x00\\x0e\\x00\\x00\\x00Water Features\\x00\\x00\\x07\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x03\\x00\\x00\\x00Spa\\x00G\\x01\\x01\\x01\\x00\\x00\\x00\\x01\\xd0\\x02\\x00\\x00\\xf5\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cleaner\\x00\\x15\\x05\\x05\\x00\\x00\\x00\\x00\\x02\\xd0\\x02\\x00\\x00\\xf6\\x01\\x00\\x00\\x04\\x00\\x00\\x00Jets-\\x00\\x05\\x00\\x00\\x00\\x00\\x03\\xd0\\x02\\x00\\x00\\xf7\\x01\\x00\\x00\\n\\x00\\x00\\x00Pool Light\\x00\\x00>\\x10\\x03\\x00\\x00\\x00\\n\\x04\\xd0\\x02\\x00\\x00\\xf8\\x01\\x00\\x00\\t\\x00\\x00\\x00Spa Light\\x00\\x00\\x00I\\x10\\x03\\x00\\x00\\x01\\n\\x05\\xd0\\x02\\x00\\x00\\xf9\\x01\\x00\\x00\\x04\\x00\\x00\\x00Pool<\\x02\\x00\\x01\\x00\\x00\\x00\\x06\\xd0\\x02\\x00\\x00\\xfa\\x01\\x00\\x00\\n\\x00\\x00\\x00Air Blower\\x00\\x00\\x01\\x00\\x05\\x00\\x00\\x00\\x00\\x07\\xd0\\x02\\x00\\x00\\x08\\x00\\x00\\x00\\x05\\x00\\x00\\x00White\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x0b\\x00\\x00\\x00Light Green\\x00\\xa0\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xa0\\x00\\x00\\x00\\x05\\x00\\x00\\x00Green\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00P\\x00\\x00\\x00\\x04\\x00\\x00\\x00Cyan\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xc8\\x00\\x00\\x00\\x04\\x00\\x00\\x00Blued\\x00\\x00\\x00\\x8c\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x08\\x00\\x00\\x00Lavender\\xe6\\x00\\x00\\x00\\x82\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x07\\x00\\x00\\x00Magenta\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\r\\x00\\x00\\x00Light Magenta\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xb4\\x00\\x00\\x00\\xd2\\x00\\x00\\x00\\x86\\x83\\x00\\x00\\x00\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 1, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 7, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 1 + }, + "model": { + "name": "Model", + "value": "IntelliTouch i7+3" + }, + "equipment": { + "flags": 56, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1" + ] + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1 + }, + "501": { + "circuit_id": 501, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_90": 0, + "unknown_at_offset_91": 0 + }, + "function": 5, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2 + }, + "502": { + "circuit_id": 502, + "name": "Jets", + "configuration": { + "name_index": 45, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_114": 0, + "unknown_at_offset_115": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 3 + }, + "503": { + "circuit_id": 503, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_146": 0, + "unknown_at_offset_147": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 10 + }, + "device_id": 4 + }, + "504": { + "circuit_id": 504, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_178": 0, + "unknown_at_offset_179": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 0, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 5 + }, + "505": { + "circuit_id": 505, + "name": "Pool", + "configuration": { + "name_index": 60, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_202": 0, + "unknown_at_offset_203": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6 + }, + "506": { + "circuit_id": 506, + "name": "Air Blower", + "configuration": { + "name_index": 1, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_234": 0, + "unknown_at_offset_235": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7 + } + }, + "pump": { + "0": { + "data": 134 + }, + "1": { + "data": 131 + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + } + } + }, + "status": { + "raw": { + "__type": "", + "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00c\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\\\\\x00\\x00\\x00\\x00\\x00\\x00\\x00U\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00]\\x00\\x00\\x00\\x00\\x00\\x00\\x00e\\x00\\x00\\x00c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\n\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 99, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + }, + "configuration": { + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 92, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 85, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 93, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 101, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 99, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "501": { + "circuit_id": 501, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "502": { + "circuit_id": 502, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "503": { + "circuit_id": 503, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 10 + }, + "configuration": { + "delay": 0 + } + }, + "504": { + "circuit_id": 504, + "value": 0, + "color": { + "color_set": 0, + "color_position": 1, + "color_stagger": 10 + }, + "configuration": { + "delay": 0 + } + }, + "505": { + "circuit_id": 505, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "506": { + "circuit_id": 506, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + } + } + } + }, + "pumps": [ + { + "raw": { + "__type": "", + "repr": "b'\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00?\\x00\\x00\\x00\\x1a\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x06\\x00\\x00\\x00\\x1a\\x04\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00:\\x07\\x00\\x00\\x01\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\xdc\\x05\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "0": { + "type": 2, + "state": { + "name": "Default Pump", + "value": 1 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 63, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 1050, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 255, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 1050, + "is_rpm": 1 + }, + "1": { + "device_id": 1, + "setpoint": 1850, + "is_rpm": 1 + }, + "2": { + "device_id": 2, + "setpoint": 1500, + "is_rpm": 1 + }, + "3": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + } + } + } + }, + { + "raw": { + "__type": "", + "repr": "b'\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x9a\\x0b\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "1": { + "type": 2, + "state": { + "name": "Default Pump", + "value": 0 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 3, + "setpoint": 2970, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "2": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "3": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "4": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "5": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "6": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + }, + "7": { + "device_id": 0, + "setpoint": 1000, + "is_rpm": 1 + } + } + } + } + } + } + ], + "chemistry": { + "raw": { + "__type": "", + "repr": "b'*\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 0, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 0, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.0, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 0, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 0.0, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 0, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 0, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 0, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 0, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 0, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 0 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 0, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 0, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 0, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "0.000" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + } + } + }, + "scg": { + "raw": { + "__type": "", + "repr": "b'\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x002\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 1 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 50, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + } + }, + "color": null +} \ No newline at end of file diff --git a/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json new file mode 100644 index 0000000..6e64ed5 --- /dev/null +++ b/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json @@ -0,0 +1,2049 @@ +{ + "decoded_complete": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel" + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0, + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 98360, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM", + "HYBRID_HEATER" + ] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 60, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.67, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 799, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.01, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 6, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 7, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0, + "delay": 0 + }, + "function": 5, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 1 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0, + "delay": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7, + "value": 0 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8, + "value": 1 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0, + "delay": 0 + }, + "function": 0, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9, + "value": 0 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0, + "delay": 0 + }, + "function": 14, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11, + "value": 0 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12, + "value": 0 + } + }, + "pump": { + "0": { + "data": 70, + "type": 3, + "state": { + "name": "Pool Low Pump", + "value": 1 + }, + "watts_now": { + "name": "Pool Low Pump Watts Now", + "value": 1289, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Low Pump RPM Now", + "value": 2749, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Pool Low Pump GPM Now", + "value": 62, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "1": { + "data": 66, + "type": 3, + "state": { + "name": "Waterfall Pump", + "value": 0 + }, + "watts_now": { + "name": "Waterfall Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Waterfall Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Waterfall Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 95, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 60, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 3, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 7.67, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 799, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 6, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 7, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.01, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 30, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 4, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 52, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 149, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 51, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + }, + "version": { + "raw": { + "__type": "", + "repr": "b'\\x19\\x00\\x00\\x00POOL: 5.2 Build 736.0 Rel\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x0c\\x00\\x00\\x00'" + }, + "decoded": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel" + } + } + } + }, + "config": { + "raw": { + "__type": "", + "repr": "b'd\\x00\\x00\\x00(h(h\\x00\\r\\x00\\x008\\x80\\xff\\xff\\x0e\\x00\\x00\\x00Water Features\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x03\\x00\\x00\\x00Spa\\x00G\\x01\\x01\\x01\\x00\\x00\\x00\\x01\\xd0\\x02\\x00\\x00\\xf5\\x01\\x00\\x00\\t\\x00\\x00\\x00Waterfall\\x00\\x00\\x00U\\x00\\x02\\x00\\x00\\x00\\x00\\x02\\xd0\\x02\\x00\\x00\\xf6\\x01\\x00\\x00\\n\\x00\\x00\\x00Pool Light\\x00\\x00>\\x10\\x03\\x00\\x02\\x00\\x02\\x03\\xd0\\x02\\x00\\x00\\xf7\\x01\\x00\\x00\\t\\x00\\x00\\x00Spa Light\\x00\\x00\\x00I\\x10\\x03\\x00\\x06\\x01\\n\\x04\\xd0\\x02\\x00\\x00\\xf8\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cleaner\\x00\\x15\\x05\\x00\\x00\\x00\\x00\\x00\\x05\\xf0\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x08\\x00\\x00\\x00Pool Low?\\x02\\x00\\x01\\x00\\x00\\x00\\x06\\xd0\\x02\\x00\\x00\\xfa\\x01\\x00\\x00\\n\\x00\\x00\\x00Yard Light\\x00\\x00[\\x07\\x04\\x00\\x00\\x00\\x00\\x07\\xd0\\x02\\x00\\x00\\xfb\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cameras\\x00e\\x00\\x02\\x00\\x00\\x00\\x00\\x08T\\x06\\x00\\x00\\xfc\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x00\\x00\\x00\\x00\\x00\\t\\xd0\\x02\\x00\\x00\\xfe\\x01\\x00\\x00\\x08\\x00\\x00\\x00SpillwayN\\x0e\\x01\\x00\\x00\\x00\\x00\\x0b\\xd0\\x02\\x00\\x00\\xff\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x05\\x00\\x00\\x00\\x00\\x0c\\xd0\\x02\\x00\\x00\\x08\\x00\\x00\\x00\\x05\\x00\\x00\\x00White\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x0b\\x00\\x00\\x00Light Green\\x00\\xa0\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xa0\\x00\\x00\\x00\\x05\\x00\\x00\\x00Green\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00P\\x00\\x00\\x00\\x04\\x00\\x00\\x00Cyan\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xc8\\x00\\x00\\x00\\x04\\x00\\x00\\x00Blued\\x00\\x00\\x00\\x8c\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x08\\x00\\x00\\x00Lavender\\xe6\\x00\\x00\\x00\\x82\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x07\\x00\\x00\\x00Magenta\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\r\\x00\\x00\\x00Light Magenta\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xb4\\x00\\x00\\x00\\xd2\\x00\\x00\\x00FB\\x00\\x00\\x00\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 98360, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM", + "HYBRID_HEATER" + ] + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0 + }, + "function": 5, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0 + }, + "function": 0, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0 + }, + "function": 14, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12 + } + }, + "pump": { + "0": { + "data": 70 + }, + "1": { + "data": 66 + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + } + } + }, + "status": { + "raw": { + "__type": "", + "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00<\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00E\\x00\\x00\\x00\\x00\\x00\\x00\\x00S\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00T\\x00\\x00\\x00\\x00\\x00\\x00\\x00_\\x00\\x00\\x00<\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x02\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01\\n\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfb\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfc\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfe\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x02\\x00\\x00\\x1f\\x03\\x00\\x00\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x06\\x00\\x00\\x00\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 60, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.67, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 799, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.01, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 6, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 7, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + }, + "configuration": { + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 95, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 60, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 3, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "501": { + "circuit_id": 501, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "502": { + "circuit_id": 502, + "value": 0, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "configuration": { + "delay": 0 + } + }, + "503": { + "circuit_id": 503, + "value": 0, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "configuration": { + "delay": 0 + } + }, + "504": { + "circuit_id": 504, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "505": { + "circuit_id": 505, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "506": { + "circuit_id": 506, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "507": { + "circuit_id": 507, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "508": { + "circuit_id": 508, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "510": { + "circuit_id": 510, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "511": { + "circuit_id": 511, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + } + } + } + }, + "pumps": [ + { + "raw": { + "__type": "", + "repr": "b'\\x03\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\t\\x05\\x00\\x00\\xbd\\n\\x00\\x00\\x00\\x00\\x00\\x00>\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x06\\x00\\x00\\x00?\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00z\\r\\x00\\x00\\x01\\x00\\x00\\x00\\x82\\x00\\x00\\x00K\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "0": { + "type": 3, + "state": { + "name": "Default Pump", + "value": 1 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 1289, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 2749, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 62, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + } + } + } + }, + { + "raw": { + "__type": "", + "repr": "b'\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x8c\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "1": { + "type": 3, + "state": { + "name": "Default Pump", + "value": 0 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + } + } + } + } + ], + "chemistry": { + "raw": { + "__type": "", + "repr": "b'*\\x00\\x00\\x00\\x00\\x02\\xff\\x03\\x1f\\x02\\xf8\\x02\\xd0\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x04\\x004\\x00\\x08\\x06\\x07\\xff\\x03 \\x00-\\x00-\\x14\\x00E\\x00\\x00\\x95 <\\x01\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 7.67, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 799, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 6, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 7, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.01, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH" + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV" + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm" + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm" + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm" + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm" + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 30, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 4, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 52, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 149, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + } + } + }, + "scg": { + "raw": { + "__type": "", + "repr": "b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x003\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 51, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + } + }, + "color": null +} \ No newline at end of file diff --git a/tests/fake_gateway.py b/tests/fake_gateway.py deleted file mode 100644 index 6b4c8a8..0000000 --- a/tests/fake_gateway.py +++ /dev/null @@ -1,179 +0,0 @@ -""" Fake ScreenLogic gateway """ -import asyncio -import random -import struct -import time - -from screenlogicpy.const.msg import CODE, HEADER_LENGTH -from screenlogicpy.requests.utility import takeMessage, makeMessage, encodeMessageString -from tests.const_data import ( - ASYNC_SL_RESPONSES, - FAKE_GATEWAY_ADDRESS, - FAKE_GATEWAY_CHK, - FAKE_GATEWAY_MAC, - FAKE_GATEWAY_NAME, - FAKE_GATEWAY_PORT, - FAKE_GATEWAY_SUB_TYPE, - FAKE_GATEWAY_TYPE, -) - - -def expected_resp(req_code, resp_data=b""): - return 0, req_code + 1, resp_data - - -def error_resp(req_code): - if req_code == CODE.LOCALLOGIN_QUERY: - return 0, CODE.ERROR_LOGIN_REJECTED, b"" - else: - return 0, CODE.ERROR_BAD_PARAMETER, b"" - - -class CONNECTION_STAGE: - NO_CONNECTION = 0 - CONNECTSERVERHOST = 1 - CHALLENGE = 2 - LOGIN = 3 - - -class FakeScreenLogicTCPProtocol(asyncio.Protocol): - def connection_made(self, transport: asyncio.Transport) -> None: - self.transport = transport - self.connected = True - self._connection_stage = CONNECTION_STAGE.NO_CONNECTION - self._buff = bytearray() - - def data_received(self, data: bytes) -> None: - for response in self.process_request(data): - if response is not None: - self.transport.write(response) - - def connection_lost(self, exc: Exception) -> None: - return super().connection_lost(exc) - - def process_request(self, data): - if self._connection_stage == CONNECTION_STAGE.NO_CONNECTION: - if data == b"CONNECTSERVERHOST\r\n\r\n": - self._connection_stage = CONNECTION_STAGE.CONNECTSERVERHOST - return [] - - def complete_messages(data: bytes) -> list[tuple[int, int, bytes]]: - """Return only complete ScreenLogic messages.""" - - self._buff.extend(data) - complete = [] - while len(self._buff) >= HEADER_LENGTH: - dataLen = struct.unpack_from("= totalLen: - out = bytearray() - for _ in range(totalLen): - out.append(self._buff.pop(0)) - complete.append(takeMessage(bytes(out))) - else: - break - return complete - - return [self.process_message(message) for message in complete_messages(data)] - - def process_message(self, message: tuple[int, int, bytes]) -> bytes: - time.sleep(0.1) - if self._connection_stage == CONNECTION_STAGE.LOGIN: - return self.process_connected_messages(message) - else: - return self.process_logon_messages(message) - - def process_logon_messages(self, message: tuple[int, int, bytes]) -> bytes: - messageID, messageCode, _ = message - if ( - messageCode == CODE.CHALLENGE_QUERY - and self._connection_stage == CONNECTION_STAGE.CONNECTSERVERHOST - ): - self._connection_stage = CONNECTION_STAGE.CHALLENGE - return makeMessage( - messageID, - CODE.CHALLENGE_QUERY + 1, - encodeMessageString(FAKE_GATEWAY_MAC), - ) - elif ( - messageCode == CODE.LOCALLOGIN_QUERY - and self._connection_stage == CONNECTION_STAGE.CHALLENGE - ): - self._connection_stage = CONNECTION_STAGE.LOGIN - return makeMessage(messageID, CODE.LOCALLOGIN_QUERY + 1, b"") - else: - self.transport.close() - - def process_connected_messages(self, message: tuple[int, int, bytes]) -> bytes: - messageID, messageCode, _ = message - if ( - self._connection_stage == CONNECTION_STAGE.LOGIN - and messageCode in ASYNC_SL_RESPONSES - ): - return makeMessage( - messageID, messageCode + 1, ASYNC_SL_RESPONSES[messageCode] - ) - else: - self.transport.close() - - def fake_async_message( - self, message_id: int, message_code: int, message_data: bytes = b"" - ) -> None: - self.transport.write(makeMessage(message_id, message_code, message_data)) - - -class FailingFakeScreenLogicTCPProtocol(FakeScreenLogicTCPProtocol): - def process_message(self, message: tuple[int, int, bytes]) -> bytes: - call_max = 75 - messageID, messageCode, _ = message - call_min = messageID if messageID < call_max else call_max - fail = random.randint(call_min, call_max) - if fail > 60: - if fail > 70: - if fail >= 75: - self.transport.close() - else: - if messageCode == CODE.LOCALLOGIN_QUERY: - return makeMessage(messageID, CODE.ERROR_LOGIN_REJECTED) - else: - return makeMessage(messageID, CODE.ERROR_BAD_PARAMETER) - else: - return None - else: - return super().process_message(message) - - -class DisconnectingFakeScreenLogicTCPProtocol(FakeScreenLogicTCPProtocol): - should_close = False - - def process_connected_messages(self, message: tuple[int, int, bytes]) -> bytes: - if self.should_close: - self.should_close = False - self.transport.close() - messageID, messageCode, _ = message - if messageCode == 1111: - self.should_close = True - return makeMessage(messageID, 1112) - return super().process_connected_messages(message) - - -class FakeScreenLogicUDPProtocol(asyncio.DatagramProtocol): - def connection_made(self, transport: asyncio.DatagramTransport): - self.transport = transport - - def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: - if struct.unpack("<8b", data) == (1, 0, 0, 0, 0, 0, 0, 0): - ip1, ip2, ip3, ip4 = FAKE_GATEWAY_ADDRESS.split(".") - response = struct.pack( - f"\x00\x00\x00\xff\x00\x00\x00\x06\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00H\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00z\r\x00\x00\x01\x00\x00\x00\x82\x00\x00\x00K\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00" + +TEST_RAW_PUMP_1 = b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x02\x00\x00\x00\x8c\n\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00" + +TEST_DECODED_VERSION = { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel", + } + } +} + +TEST_DECODED_PUMP_0 = { + "pump": { + 0: { + "type": 3, + "state": {"name": "Default Pump", "value": 1}, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 1289, + "unit": "W", + "device_type": "power", + "state_type": "measurement", + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 2749, + "unit": "rpm", + "state_type": "measurement", + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 62, + "unit": "gpm", + "state_type": "measurement", + }, + "unknown_at_offset_24": 255, + "preset": { + 0: {"device_id": 6, "setpoint": 63, "is_rpm": 0}, + 1: {"device_id": 9, "setpoint": 72, "is_rpm": 0}, + 2: {"device_id": 1, "setpoint": 3450, "is_rpm": 1}, + 3: {"device_id": 130, "setpoint": 75, "is_rpm": 0}, + 4: {"device_id": 12, "setpoint": 72, "is_rpm": 0}, + 5: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 6: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 7: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + }, + } + } +} + +TEST_DECODED_PUMP_1 = { + "pump": { + 1: { + "type": 3, + "state": {"name": "Default Pump", "value": 0}, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement", + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement", + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement", + }, + "unknown_at_offset_24": 255, + "preset": { + 0: {"device_id": 2, "setpoint": 2700, "is_rpm": 1}, + 1: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 2: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 3: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 4: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 5: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 6: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 7: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + }, + } + } +} + +TEST_DATA = { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel", + } + }, + "pump": { + 0: { + "type": 3, + "state": {"name": "Default Pump", "value": 1}, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 1289, + "unit": "W", + "device_type": "power", + "state_type": "measurement", + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 2749, + "unit": "rpm", + "state_type": "measurement", + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 62, + "unit": "gpm", + "state_type": "measurement", + }, + "unknown_at_offset_24": 255, + "preset": { + 0: {"device_id": 6, "setpoint": 63, "is_rpm": 0}, + 1: {"device_id": 9, "setpoint": 72, "is_rpm": 0}, + 2: {"device_id": 1, "setpoint": 3450, "is_rpm": 1}, + 3: {"device_id": 130, "setpoint": 75, "is_rpm": 0}, + 4: {"device_id": 12, "setpoint": 72, "is_rpm": 0}, + 5: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 6: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 7: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + }, + }, + 1: { + "type": 3, + "state": {"name": "Default Pump", "value": 0}, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement", + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement", + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement", + }, + "unknown_at_offset_24": 255, + "preset": { + 0: {"device_id": 2, "setpoint": 2700, "is_rpm": 1}, + 1: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 2: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 3: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 4: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 5: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 6: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + 7: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, + }, + }, + }, +} +TEST_LAST = { + "version": TEST_RAW_VERSION, + "pumps": { + 0: TEST_RAW_PUMP_0, + 1: TEST_RAW_PUMP_1, + }, +} -@pytest.mark.parametrize( - "collection", - TEST_DATA_COLLECTIONS, +TEST_RC = ScreenLogicResponseCollection( + TEST_DATA, + version=ScreenLogicResponseSet(TEST_RAW_VERSION, TEST_DECODED_VERSION), + pumps=[ + ScreenLogicResponseSet(TEST_RAW_PUMP_0, TEST_DECODED_PUMP_0), + ScreenLogicResponseSet(TEST_RAW_PUMP_1, TEST_DECODED_PUMP_1), + ], ) -def test_validate_complete(collection: ScreenLogicResponseCollection): - data = {} - if collection.version: - decode_version(collection.version.raw, data) - decode_pool_config(collection.config.raw, data) - decode_pool_status(collection.status.raw, data) - for idx, pump in enumerate(collection.pumps): - decode_pump_status(pump.raw, data, idx) - if collection.chemistry: - decode_chemistry(collection.chemistry.raw, data) - if collection.scg: - decode_scg_config(collection.scg.raw, data) + + +def test_data_build_response_collection(): + rc = build_response_collection(TEST_LAST, TEST_DATA) + assert rc == TEST_RC + + +def test_data_import_export_response_collection(): + written: str = "" + + def write(data): + nonlocal written + written += data + + mo: MagicMock = mock_open() + handle = mo() + handle.write.side_effect = write + with patch("screenlogicpy.data.open", mo): + export_response_collection(TEST_RC, "filename") + mo.assert_called_with("filename", "w", encoding="utf-8") + + assert written + + with patch("screenlogicpy.data.open", mock_open(read_data=written)) as mo2: + rc = import_response_collection("filename") + mo2.assert_called_once_with("filename", "r", encoding="utf-8") + assert rc == TEST_RC diff --git a/tests/test_decode.py b/tests/test_decode.py index 2c07267..59d8c9f 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1,6 +1,4 @@ -from dataclasses import astuple -import pytest - +from screenlogicpy.data import ScreenLogicResponseCollection from screenlogicpy.requests.config import decode_pool_config from screenlogicpy.requests.status import decode_pool_status from screenlogicpy.requests.pump import decode_pump_status @@ -9,82 +7,57 @@ from screenlogicpy.requests.utility import makeMessage, takeMessages from screenlogicpy.requests.gateway import decode_version -from .data_sets import TEST_DATA_COLLECTIONS, TESTING_DATA_COLLECTION as TDC - -@pytest.mark.parametrize( - "buffer, expected", - [astuple(col.version) for col in TEST_DATA_COLLECTIONS if col.version], -) -def test_decode_version(buffer, expected): +def test_decode_version(response_collection: ScreenLogicResponseCollection): data = {} - decode_version(buffer, data) + decode_version(response_collection.version.raw, data) - assert data == expected + assert data == response_collection.version.decoded -@pytest.mark.parametrize( - "buffer, expected", - [astuple(col.config) for col in TEST_DATA_COLLECTIONS if col.config], -) -def test_decode_config(buffer, expected): +def test_decode_config(response_collection: ScreenLogicResponseCollection): data = {} - decode_pool_config(buffer, data) + decode_pool_config(response_collection.config.raw, data) - assert data == expected + assert data == response_collection.config.decoded -@pytest.mark.parametrize( - "buffer, expected", - [astuple(col.status) for col in TEST_DATA_COLLECTIONS if col.status], -) -def test_decode_status(buffer, expected): +def test_decode_status(response_collection: ScreenLogicResponseCollection): data = {} - decode_pool_status(buffer, data) + decode_pool_status(response_collection.status.raw, data) - assert data == expected + assert data == response_collection.status.decoded -@pytest.mark.parametrize( - "buffer, expected", - [astuple(pump) for col in TEST_DATA_COLLECTIONS for pump in col.pumps if col.pumps], -) -def test_decode_pump(buffer, expected): - data = {} - decode_pump_status(buffer, data, 0) +def test_decode_pump(response_collection: ScreenLogicResponseCollection): + for pump_num, pump_response in enumerate(response_collection.pumps): + data = {} + decode_pump_status(pump_response.raw, data, pump_num) - assert data == expected + assert data == pump_response.decoded -@pytest.mark.parametrize( - "buffer, expected", - [astuple(col.chemistry) for col in TEST_DATA_COLLECTIONS if col.chemistry], -) -def test_decode_chemistry(buffer, expected): +def test_decode_chemistry(response_collection: ScreenLogicResponseCollection): data = {} - decode_chemistry(buffer, data) + decode_chemistry(response_collection.chemistry.raw, data) - assert data == expected + assert data == response_collection.chemistry.decoded -@pytest.mark.parametrize( - "buffer, expected", - [astuple(col.scg) for col in TEST_DATA_COLLECTIONS if col.scg], -) -def test_decode_scg(buffer, expected): +def test_decode_scg(response_collection: ScreenLogicResponseCollection): data = {} - decode_scg_config(buffer, data) + decode_scg_config(response_collection.scg.raw, data) - assert data == expected + assert data == response_collection.scg.decoded -def test_takeMessages(): +def test_takeMessages(response_collection: ScreenLogicResponseCollection): mID1 = 27 mCD1 = 12593 - mDT1 = TDC.chemistry.raw + mDT1 = response_collection.chemistry.raw mID2 = 28 mCD2 = 12573 - mDT2 = TDC.scg.raw + mDT2 = response_collection.scg.raw joined = makeMessage(mID1, mCD1, mDT1) + makeMessage(mID2, mCD2, mDT2) messages = takeMessages(joined) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 71058a3..4fc5ea2 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -1,37 +1,28 @@ import pytest -import socket import struct +from typing import Any +from unittest.mock import patch +from screenlogicpy import ScreenLogicError +from screenlogicpy.const.common import ( + SL_GATEWAY_IP, + SL_GATEWAY_NAME, + SL_GATEWAY_PORT, + SL_GATEWAY_SUBTYPE, + SL_GATEWAY_TYPE, +) from screenlogicpy.discovery import async_discover, process_discovery_response + +from .adapter import FakeUDPProtocolAdapter from .const_data import ( FAKE_CONNECT_INFO, FAKE_GATEWAY_ADDRESS, FAKE_GATEWAY_CHK, - FAKE_GATEWAY_DISCOVERY_PORT, FAKE_GATEWAY_NAME, FAKE_GATEWAY_PORT, FAKE_GATEWAY_TYPE, FAKE_GATEWAY_SUB_TYPE, ) -from .fake_gateway import FakeScreenLogicUDPProtocol - - -@pytest.mark.asyncio -async def test_async_discovery(event_loop): - - _udp_sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) - _udp_sock.bind(("", FAKE_GATEWAY_DISCOVERY_PORT)) - - transport, protocol = await event_loop.create_datagram_endpoint( - lambda: FakeScreenLogicUDPProtocol(), - sock=_udp_sock, - ) - - hosts = await async_discover() - - transport.close() - - assert len(hosts) > 0 def test_process_discovery_response(): @@ -50,3 +41,65 @@ def test_process_discovery_response(): ) data = process_discovery_response(response) assert data == FAKE_CONNECT_INFO + + +def test_discovery_process_discovery_response_error_chk(): + ip1, ip2, ip3, ip4 = FAKE_GATEWAY_ADDRESS.split(".") + response = struct.pack( + f" 0 + + host = hosts[0] + + assert host[SL_GATEWAY_IP] == FAKE_GATEWAY_ADDRESS + assert host[SL_GATEWAY_PORT] == FAKE_GATEWAY_PORT + assert host[SL_GATEWAY_TYPE] == FAKE_GATEWAY_TYPE + assert host[SL_GATEWAY_SUBTYPE] == FAKE_GATEWAY_SUB_TYPE + assert host[SL_GATEWAY_NAME] == FAKE_GATEWAY_NAME + + +@pytest.mark.asyncio +async def test_discovery_async_discover_error( + MockDiscoveryAdapter: FakeUDPProtocolAdapter, + caplog, +): + response = struct.pack( + " None: + if struct.unpack("<8b", data) == (1, 0, 0, 0, 0, 0, 0, 0): + self.transport.sendto(response, addr) + + with patch.object( + FakeUDPProtocolAdapter, "datagram_received", patch_datagram_received + ): + await async_discover() + assert "WARNING screenlogicpy.discovery:discovery.py" in caplog.text diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 8db20c0..f2aadf6 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -1,13 +1,11 @@ import asyncio import pytest -import struct from unittest.mock import call, patch from screenlogicpy import ScreenLogicGateway -from screenlogicpy.const.common import ScreenLogicRequestError +from screenlogicpy.client import ClientManager from screenlogicpy.const.data import ATTR, DEVICE, GROUP, VALUE -from screenlogicpy.const.msg import CODE -from screenlogicpy.requests.request import async_make_request +from screenlogicpy.data import ScreenLogicResponseCollection from .const_data import ( FAKE_CONNECT_INFO, @@ -16,58 +14,169 @@ FAKE_GATEWAY_NAME, FAKE_GATEWAY_PORT, ) -from screenlogicpy.requests.utility import encodeMessageString - -from .data_sets import TESTING_DATA_COLLECTION as TDC -from .fake_gateway import error_resp, expected_resp - -TEST_GATEWAY_VERSION = TDC.decoded_complete[DEVICE.ADAPTER][VALUE.FIRMWARE][ATTR.VALUE] -TEST_CONTROLLER_MODEL = TDC.decoded_complete[DEVICE.CONTROLLER][VALUE.MODEL][ATTR.VALUE] -TEST_EQUIPMENT_FLAGS = TDC.decoded_complete[DEVICE.CONTROLLER][GROUP.EQUIPMENT][ - VALUE.FLAGS -] @pytest.mark.asyncio -async def test_gateway(MockConnectedGateway): +async def test_mock_gateway( + MockConnectedGateway, response_collection: ScreenLogicResponseCollection +): + gateway = MockConnectedGateway + assert gateway.ip == FAKE_GATEWAY_ADDRESS assert gateway.port == FAKE_GATEWAY_PORT assert gateway.name == FAKE_GATEWAY_NAME assert gateway.mac == FAKE_GATEWAY_MAC - assert gateway.version == TEST_GATEWAY_VERSION - assert gateway.controller_model == TEST_CONTROLLER_MODEL - assert gateway.equipment_flags == TEST_EQUIPMENT_FLAGS + assert ( + gateway.version + == response_collection.decoded_complete[DEVICE.ADAPTER][VALUE.FIRMWARE][ + ATTR.VALUE + ] + ) + assert ( + gateway.controller_model + == response_collection.decoded_complete[DEVICE.CONTROLLER][VALUE.MODEL][ + ATTR.VALUE + ] + ) + assert ( + gateway.equipment_flags + == response_collection.decoded_complete[DEVICE.CONTROLLER][GROUP.EQUIPMENT][ + VALUE.FLAGS + ] + ) + data = gateway.get_data() - await gateway.async_disconnect() - assert data - # diff = DeepDiff(data, EXPECTED_COMPLETE_DATA) - # print(diff) - assert data == TDC.decoded_complete + assert data == response_collection.decoded_complete @pytest.mark.asyncio -async def test_gateway_connect(MockProtocolAdapter): - async with MockProtocolAdapter: - with pytest.raises(TypeError): - _ = ScreenLogicGateway(**FAKE_CONNECT_INFO) +async def test_gateway_connect_on_create(): + with pytest.raises(TypeError): + _ = ScreenLogicGateway(**FAKE_CONNECT_INFO) @pytest.mark.asyncio -async def test_gateway_late_connect(MockProtocolAdapter): +async def test_gateway_async_connect_and_disconnect( + MockProtocolAdapter: asyncio.Server, + response_collection: ScreenLogicResponseCollection, +): async with MockProtocolAdapter: - gateway = ScreenLogicGateway() - await gateway.async_connect(**FAKE_CONNECT_INFO) - assert gateway.ip == FAKE_GATEWAY_ADDRESS - assert gateway.port == FAKE_GATEWAY_PORT - assert gateway.name == FAKE_GATEWAY_NAME - assert gateway.mac == FAKE_GATEWAY_MAC - assert gateway.version == TEST_GATEWAY_VERSION - assert gateway.controller_model == TEST_CONTROLLER_MODEL - assert gateway.equipment_flags == TEST_EQUIPMENT_FLAGS - assert gateway.is_connected - await gateway.async_disconnect() + with patch( + "screenlogicpy.gateway.ClientManager", + spec=ClientManager, + ) as client_manager: + + gateway = ScreenLogicGateway() + await gateway.async_connect(**FAKE_CONNECT_INFO) + + client_mgr_inst = client_manager.return_value + assert gateway.is_connected + assert gateway.ip == FAKE_GATEWAY_ADDRESS + assert gateway.port == FAKE_GATEWAY_PORT + assert gateway.name == FAKE_GATEWAY_NAME + assert gateway.mac == FAKE_GATEWAY_MAC + assert gateway.version == "POOL: 5.2 Build 736.0 Rel" + assert gateway.controller_model == "EasyTouch2 8" + assert gateway.equipment_flags == 98360 + assert gateway.temperature_unit == "°F" + await gateway.async_disconnect() + + assert not gateway.is_connected + client_mgr_inst.attach.assert_awaited_once() + client_mgr_inst.async_unsubscribe_gateway.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_gateway_get_status( + MockConnectedGateway: ScreenLogicGateway, + response_collection: ScreenLogicResponseCollection, +): + + gateway = MockConnectedGateway + + with patch( + "screenlogicpy.requests.status.async_make_request", + return_value=response_collection.status.raw, + ) as mock_request: + + await gateway.async_get_status() + + mock_request.assert_awaited_once_with( + gateway._protocol, 12526, b"\x00\x00\x00\x00", 1 + ) + assert gateway.get_debug()["status"] == response_collection.status.raw + + +@pytest.mark.asyncio +async def test_gateway_get_pumps( + MockConnectedGateway: ScreenLogicGateway, + response_collection: ScreenLogicResponseCollection, +): + + gateway = MockConnectedGateway + + with patch( + "screenlogicpy.requests.pump.async_make_request", + side_effect=[ + response_collection.pumps[0].raw, + response_collection.pumps[1].raw, + ], + ) as mock_request: + + await gateway.async_get_pumps() + + mock_request.assert_has_awaits( + [ + call(gateway._protocol, 12584, b"\x00\x00\x00\x00\x00\x00\x00\x00", 1), + call(gateway._protocol, 12584, b"\x00\x00\x00\x00\x01\x00\x00\x00", 1), + ] + ) + assert gateway.get_debug()["pumps"][0] == response_collection.pumps[0].raw + assert gateway.get_debug()["pumps"][1] == response_collection.pumps[1].raw + + +@pytest.mark.asyncio +async def test_gateway_get_chemistry( + MockConnectedGateway: ScreenLogicGateway, + response_collection: ScreenLogicResponseCollection, +): + + gateway = MockConnectedGateway + + with patch( + "screenlogicpy.requests.chemistry.async_make_request", + return_value=response_collection.chemistry.raw, + ) as mock_request: + + await gateway.async_get_chemistry() + + mock_request.assert_awaited_once_with( + gateway._protocol, 12592, b"\x00\x00\x00\x00", 1 + ) + assert gateway.get_debug()["chemistry"] == response_collection.chemistry.raw + + +@pytest.mark.asyncio +async def test_gateway_get_scg( + MockConnectedGateway: ScreenLogicGateway, + response_collection: ScreenLogicResponseCollection, +): + + gateway = MockConnectedGateway + + with patch( + "screenlogicpy.requests.scg.async_make_request", + return_value=response_collection.scg.raw, + ) as mock_request: + + await gateway.async_get_scg() + + mock_request.assert_awaited_once_with( + gateway._protocol, 12572, b"\x00\x00\x00\x00", 1 + ) + assert gateway.get_debug()["scg"] == response_collection.scg.raw @pytest.mark.parametrize( @@ -77,7 +186,7 @@ async def test_gateway_late_connect(MockProtocolAdapter): ("controller", "sensor", "air_temperature"), { "name": "Air Temperature", - "value": 57, + "value": 60, "unit": "°F", "device_type": "temperature", "state_type": "measurement", @@ -115,7 +224,7 @@ async def test_gateway_late_connect(MockProtocolAdapter): ), ], ) -def test_get_data(MockConnectedGateway: ScreenLogicGateway, path, expected): +def test_gateway_get_data(MockConnectedGateway: ScreenLogicGateway, path, expected): assert MockConnectedGateway.get_data(*path) == expected @@ -128,10 +237,10 @@ def test_get_data(MockConnectedGateway: ScreenLogicGateway, path, expected): ), ( ("controller", "sensor", "air_temperature"), - 57, + 60, ), ( - ("circuit", 502), + ("circuit", 505), 1, ), ( @@ -144,7 +253,7 @@ def test_get_data(MockConnectedGateway: ScreenLogicGateway, path, expected): ), ], ) -def test_get_value(MockConnectedGateway: ScreenLogicGateway, path, expected): +def test_gateway_get_value(MockConnectedGateway: ScreenLogicGateway, path, expected): assert MockConnectedGateway.get_value(*path) == expected @@ -173,11 +282,11 @@ def test_get_value(MockConnectedGateway: ScreenLogicGateway, path, expected): ), ], ) -def test_get_name(MockConnectedGateway: ScreenLogicGateway, path, expected): +def test_gateway_get_name(MockConnectedGateway: ScreenLogicGateway, path, expected): assert MockConnectedGateway.get_name(*path) == expected -def test_get_strict(MockConnectedGateway: ScreenLogicGateway): +def test_gateway_get_any_strict(MockConnectedGateway: ScreenLogicGateway): with pytest.raises(KeyError): MockConnectedGateway.get_data( "intellichem", "alarm", "does_not_exist", strict=True @@ -195,314 +304,113 @@ def test_get_strict(MockConnectedGateway: ScreenLogicGateway): @pytest.mark.asyncio -async def test_async_set_circuit( - event_loop: asyncio.AbstractEventLoop, MockConnectedGateway: ScreenLogicGateway -): - circuit_id = 505 - circuit_state = 1 - button_code = 12530 +async def test_gateway_async_set_circuit(MockConnectedGateway: ScreenLogicGateway): + """Test setting circuit state.""" + + gateway = MockConnectedGateway - result = event_loop.create_future() - result.set_result(expected_resp(button_code)) with patch( - "screenlogicpy.requests.button.ScreenLogicProtocol.await_send_message", - return_value=result, + "screenlogicpy.requests.button.async_make_request", + return_value=b"", ) as mockRequest: - gateway = MockConnectedGateway - assert await gateway.async_set_circuit(circuit_id, circuit_state) - await gateway.async_disconnect() - assert mockRequest.call_args.args[0] == button_code - assert mockRequest.call_args.args[1] == struct.pack( - " SLMessage: + return SLMessage(msg.id, CODE.ERROR_LOGIN_REJECTED) + + with patch.object( + FakeTCPProtocolAdapter, "handle_logon_request", patch_handle_login_request + ), pytest.raises(ScreenLogicError) as sle: + await async_connect_to_gateway( + FAKE_GATEWAY_ADDRESS, FAKE_GATEWAY_PORT, max_retries=0 + ) + assert ( + sle.value.msg + == "Failed to logon to gateway: Login Rejected for request code: 27, request: b'\\\\\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x00\\x00\\x00Android\\x00\\x10\\x00\\x00\\x000000000000000000\\x00\\x02\\x00\\x00\\x00' after 1 attempts" + ) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 673a354..6101a12 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -5,58 +5,39 @@ from screenlogicpy.requests.protocol import ScreenLogicProtocol from screenlogicpy.requests.utility import makeMessage -from .data_sets import TEST_DATA_COLLECTIONS - @pytest.mark.asyncio async def test_async_data_received(event_loop): CODE = 1196 MESSAGE = b"success" - data = {} - - callback_triggered: asyncio.Future - callback_triggered = event_loop.create_future() - - async def callback(message, target_data): - target_data["result"] = message - callback_triggered.set_result(True) - protocol = ScreenLogicProtocol(event_loop) - protocol.register_async_message_callback(CODE, callback, data) + fut = protocol._futures.create(0) payload = makeMessage(0, CODE, MESSAGE) protocol.data_received(payload) - await callback_triggered - - assert data.get("result") == MESSAGE + assert fut.result() == (0, CODE, MESSAGE) @pytest.mark.asyncio async def test_async_large_data_received(event_loop): CODE = MSG_CODE.CTRLCONFIG_QUERY + 1 - MESSAGE = TEST_DATA_COLLECTIONS[2].config.raw - - data = {} - - callback_triggered: asyncio.Future - callback_triggered = event_loop.create_future() - - async def callback(message, target_data): - target_data["result"] = message - callback_triggered.set_result(True) + MESSAGE = b"d\x00\x00\x00(h(h\x00\x02\x00\xa00\x00\x00\x00\x0e\x00\x00\x00Water Features\x00\x00\x1b\x00\x00\x00\xf4\x01\x00\x00\x03\x00\x00\x00Spa\x00G\x01\x01\x01\x00\x00\x00\x01\xd0\x02\x00\x00\xf5\x01\x00\x00\x07\x00\x00\x00Cleaner\x00\x15\x05\x00\x00\x00\x00\x00\x02\xd0\x02\x00\x00\xf6\x01\x00\x00\x04\x00\x00\x00Jets-\x00\x01\x00\x00\x00\x00\x03\xd0\x02\x00\x00\xf7\x01\x00\x00\r\x00\x00\x00Water Feature\x00\x00\x00S\x00\x02\x00\x00\x00\x00\x04\xd0\x02\x00\x00\xf8\x01\x00\x00\x0b\x00\x00\x00Fiber Optic\x00\x1d\x0b\x04\x00\x00\x00\x00\x05\xd0\x02\x00\x00\xf9\x01\x00\x00\x04\x00\x00\x00Pool<\x02\x00\x01\x00\x00\x00\x06\xd0\x02\x00\x00\xfa\x01\x00\x00\x0b\x00\x00\x00Color Wheel\x00\x16\x0c\x03\x00\x00\x00\n\x07\xd0\x02\x00\x00\xfb\x01\x00\x00\x0b\x00\x00\x00Color Wheel\x00\x16\x0c\x03\x00\x00\x00\n\x08\xd0\x02\x00\x00\xfc\x01\x00\x00\x05\x00\x00\x00Aux 7\x00\x00\x00\x08\x00\x05\x00\x00\x00\x00\t\xd0\x02\x00\x00\xfd\x01\x00\x00\x05\x00\x00\x00Aux 8\x00\x00\x00\t\x00\x05\x00\x00\x00\x00\n\xd0\x02\x00\x00\xfe\x01\x00\x00\t\x00\x00\x00Spa Light\x00\x00\x00I\n\x03\x00\x00\x00\x00\x0b\xd0\x02\x00\x00\xff\x01\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\x07\x04\x00\x00\x00\x00\x0c\xd0\x02\x00\x00\x00\x02\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\t\x03\x00\x00\x00\x00\r\xd0\x02\x00\x00\x01\x02\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\t\x03\x00\x00\x00\x00\x0e\xd0\x02\x00\x00\x02\x02\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\t\x03\x00\x00\x00\x00\x0f\xd0\x02\x00\x00\x03\x02\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\t\x03\x00\x00\x00\x00\x10\xd0\x02\x00\x00\x04\x02\x00\x00\t\x00\x00\x00Waterfall\x00\x00\x00U\x00\x02\x00\x00\x00\x00\x11\xd0\x02\x00\x00\x05\x02\x00\x00\x05\x00\x00\x00Aux 8\x00\x00\x00\t\x00\x05\x00\x00\x00\x00\x12\xd0\x02\x00\x00\x06\x02\x00\x00\x05\x00\x00\x00Aux 9\x00\x00\x00\n\x00\x05\x00\x00\x00\x00\x13\xd0\x02\x00\x00\x07\x02\x00\x00\x06\x00\x00\x00Aux 10\x00\x00\x0b\x00\x05\x00\x00\x00\x00\x14\xd0\x02\x00\x00\x08\x02\x00\x00\x08\x00\x00\x00Upr Pool^\x00\x02\x00\x00\x00\x00\x15\xd0\x02\x00\x00\t\x02\x00\x00\x0b\x00\x00\x00Upr Cleaner\x00_\x00\x00\x00\x00\x00\x00\x16\xd0\x02\x00\x00\n\x02\x00\x00\x0b\x00\x00\x00Upr Wtrfall\x00`\x00\x02\x00\x00\x00\x00\x17\xd0\x02\x00\x00\x0b\x02\x00\x00\t\x00\x00\x00Upr Light\x00\x00\x00a\x07\x04\x00\x00\x00\x00\x18\xd0\x02\x00\x00\x0c\x02\x00\x00\x05\x00\x00\x00Aux 5\x00\x00\x00\x06\x00\x05\x00\x00\x00\x00\x19\xd0\x02\x00\x00\x1c\x02\x00\x00\r\x00\x00\x00Spa Waterfall\x00\x00\x00M\x0e\x02\x00\x00\x00\x00)\xd0\x02\x00\x00\x1d\x02\x00\x00\x05\x00\x00\x00Slide\x00\x00\x00E\x00\x02\x00\x00\x00\x00*\xd0\x02\x00\x00\x08\x00\x00\x00\x05\x00\x00\x00White\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\x0b\x00\x00\x00Light Green\x00\xa0\x00\x00\x00\xff\x00\x00\x00\xa0\x00\x00\x00\x05\x00\x00\x00Green\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00P\x00\x00\x00\x04\x00\x00\x00Cyan\x00\x00\x00\x00\xff\x00\x00\x00\xc8\x00\x00\x00\x04\x00\x00\x00Blued\x00\x00\x00\x8c\x00\x00\x00\xff\x00\x00\x00\x08\x00\x00\x00Lavender\xe6\x00\x00\x00\x82\x00\x00\x00\xff\x00\x00\x00\x07\x00\x00\x00Magenta\x00\xff\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\r\x00\x00\x00Light Magenta\x00\x00\x00\xff\x00\x00\x00\xb4\x00\x00\x00\xd2\x00\x00\x00\x86\xaa\x00\x00\x00\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00" protocol = ScreenLogicProtocol(event_loop) - protocol.register_async_message_callback(CODE, callback, data) + fut = protocol._futures.create(0) payload = makeMessage(0, CODE, MESSAGE) protocol.data_received(payload[:1024]) protocol.data_received(payload[1024:]) - await callback_triggered + await fut - assert data.get("result") == MESSAGE + msgID, msgCODE, msgDATA = fut.result() + assert msgID == 0 + assert msgCODE == CODE + assert msgDATA == MESSAGE @pytest.mark.asyncio From c4af0bd2ef91c2ceb67544b293008ec92b3a3702 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 15 Oct 2023 09:20:25 -0700 Subject: [PATCH 03/53] Post-kwargs-merge test cleanup --- ...-52-build-7360-rel_easytouch2-8_98360.json | 220 ++++++------------ tests/test_cli.py | 26 ++- tests/test_gateway.py | 11 +- 3 files changed, 94 insertions(+), 163 deletions(-) diff --git a/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json index 6e64ed5..476f79a 100644 --- a/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json +++ b/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json @@ -32,67 +32,35 @@ "color": [ { "name": "White", - "value": [ - 255, - 255, - 255 - ] + "value": [255, 255, 255] }, { "name": "Light Green", - "value": [ - 160, - 255, - 160 - ] + "value": [160, 255, 160] }, { "name": "Green", - "value": [ - 0, - 255, - 80 - ] + "value": [0, 255, 80] }, { "name": "Cyan", - "value": [ - 0, - 255, - 200 - ] + "value": [0, 255, 200] }, { "name": "Blue", - "value": [ - 100, - 140, - 255 - ] + "value": [100, 140, 255] }, { "name": "Lavender", - "value": [ - 230, - 130, - 255 - ] + "value": [230, 130, 255] }, { "name": "Magenta", - "value": [ - 255, - 0, - 128 - ] + "value": [255, 0, 128] }, { "name": "Light Magenta", - "value": [ - 255, - 180, - 210 - ] + "value": [255, 180, 210] } ], "interface_tab_flags": 127, @@ -121,12 +89,7 @@ "name": "Controller State", "value": 1, "device_type": "enum", - "enum_options": [ - "Unknown", - "Ready", - "Sync", - "Service" - ] + "enum_options": ["Unknown", "Ready", "Sync", "Service"] }, "freeze_mode": { "name": "Freeze Mode", @@ -604,12 +567,7 @@ "name": "Pool Heat", "value": 0, "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Heater", - "Both" - ] + "enum_options": ["Off", "Solar", "Heater", "Both"] }, "heat_setpoint": { "name": "Pool Heat Set Point", @@ -652,12 +610,7 @@ "name": "Spa Heat", "value": 0, "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Heater", - "Both" - ] + "enum_options": ["Off", "Solar", "Heater", "Both"] }, "heat_setpoint": { "name": "Spa Heat Set Point", @@ -729,32 +682,44 @@ "ph_setpoint": { "name": "pH Setpoint", "value": 7.6, - "unit": "pH" + "unit": "pH", + "max_setpoint": 7.6, + "min_setpoint": 7.2 }, "orp_setpoint": { "name": "ORP Setpoint", "value": 720, - "unit": "mV" + "unit": "mV", + "max_setpoint": 800, + "min_setpoint": 400 }, - "calcium_harness": { + "calcium_hardness": { "name": "Calcium Hardness", "value": 800, - "unit": "ppm" + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 }, "cya": { "name": "Cyanuric Acid", "value": 45, - "unit": "ppm" + "unit": "ppm", + "max_setpoint": 201, + "min_setpoint": 0 }, "total_alkalinity": { "name": "Total Alkalinity", "value": 45, - "unit": "ppm" + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 }, "salt_tds_ppm": { "name": "Salt/TDS", "value": 1000, - "unit": "ppm" + "unit": "ppm", + "max_setpoint": 6500, + "min_setpoint": 500 }, "probe_is_celsius": 0, "flags": 32 @@ -793,21 +758,13 @@ "name": "pH Dosing State", "value": 1, "device_type": "enum", - "enum_options": [ - "Dosing", - "Mixing", - "Monitoring" - ] + "enum_options": ["Dosing", "Mixing", "Monitoring"] }, "orp_dosing_state": { "name": "ORP Dosing State", "value": 2, "device_type": "enum", - "enum_options": [ - "Dosing", - "Mixing", - "Monitoring" - ] + "enum_options": ["Dosing", "Mixing", "Monitoring"] } }, "alarm": { @@ -926,7 +883,7 @@ "name": "Super Chlorination Timer", "value": 0, "unit": "hr", - "min_setpoint": 1, + "min_setpoint": 0, "max_setpoint": 72, "step": 1 } @@ -980,67 +937,35 @@ "color": [ { "name": "White", - "value": [ - 255, - 255, - 255 - ] + "value": [255, 255, 255] }, { "name": "Light Green", - "value": [ - 160, - 255, - 160 - ] + "value": [160, 255, 160] }, { "name": "Green", - "value": [ - 0, - 255, - 80 - ] + "value": [0, 255, 80] }, { "name": "Cyan", - "value": [ - 0, - 255, - 200 - ] + "value": [0, 255, 200] }, { "name": "Blue", - "value": [ - 100, - 140, - 255 - ] + "value": [100, 140, 255] }, { "name": "Lavender", - "value": [ - 230, - 130, - 255 - ] + "value": [230, 130, 255] }, { "name": "Magenta", - "value": [ - 255, - 0, - 128 - ] + "value": [255, 0, 128] }, { "name": "Light Magenta", - "value": [ - 255, - 180, - 210 - ] + "value": [255, 180, 210] } ], "interface_tab_flags": 127, @@ -1312,12 +1237,7 @@ "name": "Controller State", "value": 1, "device_type": "enum", - "enum_options": [ - "Unknown", - "Ready", - "Sync", - "Service" - ] + "enum_options": ["Unknown", "Ready", "Sync", "Service"] }, "freeze_mode": { "name": "Freeze Mode", @@ -1404,12 +1324,7 @@ "name": "Pool Heat", "value": 0, "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Heater", - "Both" - ] + "enum_options": ["Off", "Solar", "Heater", "Both"] }, "heat_setpoint": { "name": "Pool Heat Set Point", @@ -1450,12 +1365,7 @@ "name": "Spa Heat", "value": 0, "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Heater", - "Both" - ] + "enum_options": ["Off", "Solar", "Heater", "Both"] }, "heat_setpoint": { "name": "Spa Heat Set Point", @@ -1831,32 +1741,44 @@ "ph_setpoint": { "name": "pH Setpoint", "value": 7.6, - "unit": "pH" + "unit": "pH", + "max_setpoint": 7.6, + "min_setpoint": 7.2 }, "orp_setpoint": { "name": "ORP Setpoint", "value": 720, - "unit": "mV" + "unit": "mV", + "max_setpoint": 800, + "min_setpoint": 400 }, - "calcium_harness": { + "calcium_hardness": { "name": "Calcium Hardness", "value": 800, - "unit": "ppm" + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 }, "cya": { "name": "Cyanuric Acid", "value": 45, - "unit": "ppm" + "unit": "ppm", + "max_setpoint": 201, + "min_setpoint": 0 }, "total_alkalinity": { "name": "Total Alkalinity", "value": 45, - "unit": "ppm" + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 }, "salt_tds_ppm": { "name": "Salt/TDS", "value": 1000, - "unit": "ppm" + "unit": "ppm", + "max_setpoint": 6500, + "min_setpoint": 500 }, "probe_is_celsius": 0, "flags": 32 @@ -1895,21 +1817,13 @@ "name": "pH Dosing State", "value": 1, "device_type": "enum", - "enum_options": [ - "Dosing", - "Mixing", - "Monitoring" - ] + "enum_options": ["Dosing", "Mixing", "Monitoring"] }, "orp_dosing_state": { "name": "ORP Dosing State", "value": 2, "device_type": "enum", - "enum_options": [ - "Dosing", - "Mixing", - "Monitoring" - ] + "enum_options": ["Dosing", "Mixing", "Monitoring"] } }, "alarm": { @@ -2036,7 +1950,7 @@ "name": "Super Chlorination Timer", "value": 0, "unit": "hr", - "min_setpoint": 1, + "min_setpoint": 0, "max_setpoint": 72, "step": 1 } @@ -2046,4 +1960,4 @@ } }, "color": null -} \ No newline at end of file +} diff --git a/tests/test_cli.py b/tests/test_cli.py index 5b10d7b..c904b29 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -241,15 +241,19 @@ async def test_set_color_lights( @pytest.mark.parametrize( "arguments, return_code, expected_output", [ - ("set salt-generator 100 20", 0, "51 0"), + ("set salt-generator --pool 100 --spa 20", 0, "51 0"), ( - "-v set scg 20 0", + "-v set scg -p 20 -s 0", 0, EXPECTED_VERBOSE_PREAMBLE + "Pool Chlorinator Setpoint: 51 Spa Chlorinator Setpoint: 0", ), - ("set scg * *", 65, "No new Chlorinator values. Nothing to do."), - ("set scg f *", 66, "Invalid Chlorinator value"), + ("set scg -p 50", 0, "51"), + ("set scg -s 20", 0, "0"), + ("set scg", 65, "No new chlorinator values. Nothing to do."), + ("set super-chlorinate --state 1 --time 24", 0, "0"), + ("set sc -s 0", 0, ""), + ("set sc -t 12", 0, "0"), ], ) async def test_set_scg( @@ -266,14 +270,20 @@ async def test_set_scg( @pytest.mark.parametrize( "arguments, return_code, expected_output", [ - ("set chem-data 7.5 700", 0, "7.6 720"), + ("set chemistry-setpoint --ph 7.5 --orp 700", 0, "7.6 720"), ( - "-v set ch 7.6 650", + "-v set cs -p 7.6 -o 650", 0, EXPECTED_VERBOSE_PREAMBLE + "pH Setpoint: 7.6 ORP Setpoint: 720", ), - ("set ch * *", 129, "No new setpoint values. Nothing to do."), - ("set ch f *", 130, "Invalid Chemistry Setpoint value"), + ("set cs -p 7.2", 0, "7.6"), + ("set cs -o 650", 0, "720"), + ("set cs", 129, "No new chemistry values. Nothing to do."), + ( + "set chemistry-value --calcium-hardness 351 --total-alkalinity 80 --cyanuric-acid 45 --total-dissolved-solids 1000", + 0, + "800 45 45 1000", + ), ], ) async def test_set_chemistry( diff --git a/tests/test_gateway.py b/tests/test_gateway.py index f2aadf6..8cca2a5 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -386,7 +386,7 @@ async def test_gateway_async_set_scg_config(MockConnectedGateway: ScreenLogicGat return_value=b"", ) as mockRequest: - assert await gateway.async_set_scg_config(50, 0) + assert await gateway.async_set_scg_config(pool_setpoint=50, spa_setpoint=0) mockRequest.assert_awaited_once_with( gateway._protocol, @@ -406,7 +406,14 @@ async def test_gateway_async_set_chem_data(MockConnectedGateway: ScreenLogicGate return_value=b"", ) as mockRequest: - assert await gateway.async_set_chem_data(7.5, 700, 300, 80, 45, 1000) + assert await gateway.async_set_chem_data( + ph_setpoint=7.5, + orp_setpoint=700, + calcium_hardness=300, + total_alkalinity=80, + cya=45, + salt_tds_ppm=1000, + ) mockRequest.assert_awaited_once_with( gateway._protocol, From b3e28782b98bd183c27bcc939a73d8998439deef Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Tue, 24 Oct 2023 00:54:03 -0700 Subject: [PATCH 04/53] Initial request unit test --- tests/test_request.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/test_request.py diff --git a/tests/test_request.py b/tests/test_request.py new file mode 100644 index 0000000..5ee720d --- /dev/null +++ b/tests/test_request.py @@ -0,0 +1,31 @@ +import asyncio +import pytest +from unittest.mock import patch + +from screenlogicpy import ScreenLogicGateway, ScreenLogicRequestError +from screenlogicpy.const.msg import CODE +from screenlogicpy.requests.request import async_make_request + +from .adapter import FakeTCPProtocolAdapter, SLMessage +from .const_data import FAKE_CONNECT_INFO + + +@pytest.mark.asyncio +async def test_request_connection_lost( + event_loop: asyncio.AbstractEventLoop, MockProtocolAdapter: asyncio.Server +): + def disconnecting_ping(self: FakeTCPProtocolAdapter, msg: SLMessage): + self.transport.abort() + + with patch.object( + FakeTCPProtocolAdapter, "handle_ping_request", disconnecting_ping + ): + async with MockProtocolAdapter: + gateway = ScreenLogicGateway() + await gateway.async_connect(**FAKE_CONNECT_INFO) + assert gateway.is_connected + with pytest.raises( + ScreenLogicRequestError, + match="Unable to make request. No active connection", + ): + await async_make_request(gateway._protocol, CODE.PING_QUERY) From 7acb035133d6b106974e56542597cb12aac96849 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Thu, 2 Nov 2023 23:27:27 -0700 Subject: [PATCH 05/53] V0.10.0 Spec (#62) * Spec v0.10.0, Target python 3.10 * Prep v0.10.0 documentation --- .github/workflows/python-package.yml | 45 ++++++------ README.md | 102 +++++++++++++++++---------- screenlogicpy/__init__.py | 2 +- setup.py | 5 +- 4 files changed, 90 insertions(+), 64 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index a187afe..20e42e4 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,35 +5,34 @@ name: Python package on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: build: - runs-on: ubuntu-latest strategy: matrix: - python-version: [3.9, "3.10", 3.11] + python-version: ["3.10", 3.11] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install flake8 pytest pytest-cov pytest-asyncio async_timeout - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test and check coverage with pytest - run: | - pytest --cov=screenlogicpy --cov-report term-missing + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest pytest-cov pytest-asyncio async_timeout + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test and check coverage with pytest + run: | + pytest --cov=screenlogicpy --cov-report term-missing diff --git a/README.md b/README.md index 0a997b0..d7aff71 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ $ pip install screenlogicpy * _Changed in v0.5.0: The screenlogicpy library has moved over to using asyncio for all network I/O. Relevant methods now require the `async`/`await` syntax._ * _New in v0.8.0: Support for Python 3.8 and 3.9 is being phased out across future releases. Version 0.8.x will be the last versions to support Python 3.8._ -* _**New in v0.9.0**: Support for Python 3.8 has been removed. Support for Python 3.9 is being phased out across future releases. Version 0.9.x will be the last versions to support Python 3.9._ +* _New in v0.9.0: Support for Python 3.8 has been removed. Support for Python 3.9 is being phased out across future releases. Version 0.9.x will be the last versions to support Python 3.9._ +* _**New in v0.10.0**: Support for Python 3.9 has been removed._ The `ScreenLogicGateway` class is the primary interface. @@ -275,29 +276,28 @@ success = await gateway.async_set_color_lights(light_command) ## Setting chlorinator output levels -Chlorinator output levels can be set with `async_set_scg_config()`. The method takes only key-word arguments to set specific values: `pool_output` and `spa_output`. +### ScreenLogicGateway.**async_set_scg_config**(_pool_setpoint=None, spa_setpoint=None_ ) + +Chlorinator output levels can be set with `async_set_scg_config()`. `async_set_scg_config` takes up to two keyword arguments, `pool_setpoint` and `spa_setpoint`, both `int`. ```python -success = await gateway.async_set_scg_config(pool_output=pool_pct, spa_output=spa_pct) +success = await gateway.async_set_scg_config(pool_setpoint=pool_output, spa_setpoint=spa_output) ``` * _New in v0.5.0._ -* _**Changed in v0.9.0**: All values are settable individually and only as key-word args._ +* _**Changed in v0.10.0.** `async_set_scg_config` now accepts keyword arguments only. This allows the caller to specify only the value(s) they want to update, retaining all other existing values._ ## Setting IntelliChem Chemistry values -Chemistry values used in the IntelliChem system can be set with `async_set_chem_data()`. This method takes only key-word arguments to set specific values: `ph_setpoint`, `orp_setpoint`, `calcium_harness`, `total_alkalinity`, `cya`, and `salt_tds_ppm`. - -`ph_setpoint` is a `float` and the rest are `int`. +Chemistry values used in the IntelliChem system can be set with `async_set_chem_data()`. `async_set_chem_data` takes up to six keyword arguments, `ph_setpoint`, `orp_setpoint`, `calcium_hardness`, `total_alkalinity`, `cyanuric_acid`, and `salt_tds_ppm`. `ph_setpoint` is a `float` and the rest are `int`. ```python -success = await gateway.async_set_chem_data(ph_setpoint=ph, orp_setpoint=orp, calcium_harness=ch, total_alkalinity=ta, cya=cya, salt_tds_ppm=salt) +success = await gateway.async_set_chem_data(ph_setpoint=ph, orp_setpoint=orp, calcium_hardness=cal, total_alkalinity=alk, cyanuric_acid=cya, salt_tds_ppm=salt) ``` -All values are accessible only as key-word arguments. - * _New in v0.6.0._ -* _**Changed in v0.9.0**: All values are settable individually and only as key-word args._ +* _**Changed in v0.10.0:** `async_set_chem_data` now accepts keyword arguments only. This allows the caller to specify only the value(s) they want to update, retaining all other existing values._ + ## Handling unsolicited messages @@ -548,44 +548,72 @@ Sets a color mode for all color-capable lights configured on the pool controller #### set `salt-generator, scg` ```shell -screenlogicpy set salt-generator [--pool OUTPUT] [--spa OUTPUT] + +screenlogicpy set salt-generator [--pool POOL_PCT] [--spa SPA_PCT] ``` -Sets the chlorinator output levels for the pool and/or spa. Takes one or both of two optional arguments: (`-p`, `--pool`) and (`-s`, `--spa`). `OUTPUT` is an `int` between `0`-`100` for both settings. -**Note:** Pentair treats the spa output level as a percentage of the pool's output level. For example, if the pool output level is set to 80, and the spa output level is set to 50, the actual spa output would be 40%. +Sets the chlorinator output levels for the pool and/or spa via two optional arguments: -* _New in v0.5.0._ -* _**Changed in v0.9.0**: Values are now settable individually via optional arguments._ +##### set salt-generator `-p, --pool` -#### set `super-chlorinate, sup` +Specify the output level for the pool as an `int` between `0`-`100`. -```shel -screenlogicpy set super-chlorinate [--state STATE] [--time HOURS] -``` +##### set salt-generator `-s, --spa` -Starts or stops super chlorination and/or sets the runtime in hours. Takes one or both optional arguments: (`-s`, `--state`) and (`-t`, `--time`). STATE can be an `int` or `string` representing the desired [state](#state). HOURS is an `int` the range of `0`-`72`. +Specify the output level for the spa as an `int` between `0`-`100`. -#### set `chem-setpoint, csp` +**Note:** Pentair treats spa output level as a percentage of the pool's output level. + +* _New in v0.5.0._ +* _**Changed in v0.10.0:** Pool and spa arguments are now optional. Users may specify one or the other, or both._ + +#### set `chemistry-setpoint, cs` ```shell -screenlogicpy set chem-setpoint [--ph PH_SETPOINT] [--orp ORP_SETPOINT] +screenlogicpy set chemistry-setpoint [--ph PH] [--orp ORP] ``` -Sets the pH and/or ORP set points for the IntelliChem system. Takes one or both optional arguments: (`-p`, `--ph`) and (`-o`, `--orp`). `PH_SETPOINT` can be a `float` between `7.2`-`7.6`. `ORP_SETPOINT` can be an `int` between `400`-`800`. -**Note:** +Sets the pH and/or ORP set points for the IntelliChem system via two optional arguments: + +##### set chemistry-setpoint `-p, --ph` + +Specify the target pH value for Intellichem to maintain as a `float` between `7.2`-`7.6`. + +##### set chemistry-setpoint `-o, --orp` -* _**New in v0.9.0**: pH and ORP settings are now here._ +Specify the target ORP value for Intellichem to maintain as an `int` between `400`-`800`. -#### set `chem-data, cd` + +**Note:** `chemistry-setpoint` replaces the previous chem-data. + +* _**New in v0.10.0.**_ + +#### set `chemistry-value, cv` ```shell -screenlogicpy set chem-data [--ch CALCIUM_HARDNESS] [--ta TOTAL_ALKALINITY] [--cya CYANURIC_ACID] [--salt SALT_or_TDS] +screenlogicpy set chemistry-value [--calcium-hardness CALCIUM] [--total-alkalinity TA] [--cyanuric-acid CYA] [--total-dissolved-solids TDS] ``` -Sets various chemistry values used in LSI calculations within the IntelliChem system. Takes one or more optional arguments: (`-c`, `--ch`), (`-t`, `--ta`), (`-y`, `--cya`), and (`-s`, `--salt`). +Sets values used in the calculation of Saturation Index in the IntelliChem system via four optional arguments: + +##### set chemistry-setpoint `-ch, --calcium-hardness` + +Specify the calcium hardness value for Saturation Index calculation as an `int` between `X`-`X`. + +##### set chemistry-setpoint `-ta, --total-alkalinity` + +Specify the total alkalinity value for Saturation Index calculation as an `int` between `X`-`X`. + +##### set chemistry-setpoint `-cya, --cyanuric-acid` + +Specify the cyanuric acid value for Saturation Index calculation as an `int` between `X`-`X`. + +##### set chemistry-setpoint `-tds, --total-dissolved-solids` + +Specify the total dissolved solids value for Saturation Index calculation as an `int` between `X`-`X`. + +* _**New in v0.10.0.**_ -* _New in v0.6.0._ -* _**Changed in v0.9.0**: `chem-data` now sets values pertaining to LSI calculation in the IntelliChem system. pH and ORP settings have moved to the `chem-setpoint` sub-command._ # Reference @@ -643,17 +671,17 @@ Sets various chemistry values used in LSI calculations within the IntelliChem sy ## Supported Subscribable Messages -`screenlogicpy` includes functionality to automatically decode these messages and update it's data accordingly. Other message codes can be subscribed to, but the consuming application will need to implement any processing of the incoming message. +`screenlogicpy` includes functionality to automatically decode these messages and update its data accordingly. Other message codes can be subscribed to, but the consuming application will need to implement any processing of the incoming message. ```python from screenlogicpy.const import CODE ``` -|Message Code|Imported CONST|Description| -|------------|--------------|-----------| -|`12500`|`CODE.STATUS_CHANGED`|Sent when basic status changes. Air/water temp, heater state, circuit state, basic chemistry (if available).| -|`12504`|`CODE.COLOR_UPDATE`|Sent repeatedly during a color lights color mode transition.| -|`12505`|`CODE.CHEMISTRY_CHANGED`|Sent when a change occurs to the state of an attached IntelliChem controller.| +| Message Code | Imported CONST | Description | +|--------------|-------------------------|-------------------------------------------------------------------------------------------------------------| +| `12500` | `CODE.STATUS_CHANGED` | Sent when basic status changes. Air/water temp, heater state, circuit state, basic chemistry (if available).| +| `12504` | `CODE.COLOR_UPDATE` | Sent repeatedly during a color lights color mode transition. | +| `12505` | `CODE.CHEMISTRY_CHANGED`| Sent when a change occurs to the state of an attached IntelliChem controller. | --- diff --git a/screenlogicpy/__init__.py b/screenlogicpy/__init__.py index c63decb..bce60b5 100644 --- a/screenlogicpy/__init__.py +++ b/screenlogicpy/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.9.4" +__version__ = "0.10.0" # flake8: noqa F401 from screenlogicpy.gateway import ScreenLogicGateway from screenlogicpy.const.common import ScreenLogicError, ScreenLogicRequestError diff --git a/setup.py b/setup.py index 4fc460a..904c942 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="screenlogicpy", - version="0.9.4", + version="0.10.0", author="Kevin Worrel", author_email="kevinworrel@yahoo.com", description="Interface for Pentair ScreenLogic connected pool controllers over IP via Python", @@ -15,14 +15,13 @@ url="https://github.com/dieselrabbit/screenlogicpy", packages=setuptools.find_packages(include=["screenlogicpy", "screenlogicpy.*"]), classifiers=[ - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", "Development Status :: 3 - Alpha", ], - python_requires=">=3.9", + python_requires=">=3.10", install_requires=[ "async_timeout>=3.0.0", ], From bb2ec8382e19db32bd95fa7d4f18f9635213c23c Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Thu, 2 Nov 2023 23:36:35 -0700 Subject: [PATCH 06/53] Set python 3.10 --- .vscode/settings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b8235d8..27cd513 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,6 @@ { "python.testing.pytestArgs": [ - "tests", - "-vv" + "tests" ], "python.testing.unittestEnabled": false, "python.testing.nosetestsEnabled": false, @@ -13,5 +12,6 @@ "capsys", "intellichem", "screenlogicpy" - ] + ], + "python.defaultInterpreterPath": "~\\AppData\\Local\\Programs\\Python\\Python310\\python.exe" } \ No newline at end of file From 62de358a92a0c94ae5f85e3740311a787e8e11e0 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 8 Nov 2023 23:43:14 -0800 Subject: [PATCH 07/53] Fix array padding --- screenlogicpy/requests/utility.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/screenlogicpy/requests/utility.py b/screenlogicpy/requests/utility.py index 356aac3..cc5fbf3 100644 --- a/screenlogicpy/requests/utility.py +++ b/screenlogicpy/requests/utility.py @@ -110,15 +110,13 @@ def getString(buff, offset) -> tuple[str, int]: def getArray(buff, offset): - itemCount, offset = getSome("I", buff, offset) + itemCount, aStart = getSome("I", buff, offset) items = [0 for x in range(itemCount)] for i in range(itemCount): - items[i], offset = getSome("B", buff, offset) - offsetPad = 0 - if itemCount % 4 != 0: - offsetPad = (4 - itemCount % 4) % 4 - offset += offsetPad - return items, offset + items[i], offset = getSome("B", buff, aStart + i) + short = itemCount % 4 + paddedLen = itemCount if short == 0 else (itemCount + 4) - short + return items, aStart + paddedLen def getTemperatureUnit(data: dict): From 55b0eb1e45a32da9a9edd1e8c1ed8f2576b7b6d7 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 8 Nov 2023 23:43:34 -0800 Subject: [PATCH 08/53] pytest -vv --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 27cd513..88ee0e8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "python.testing.pytestArgs": [ - "tests" + "tests", + "-vv" ], "python.testing.unittestEnabled": false, "python.testing.nosetestsEnabled": false, From 5579c804c2d581c5472516c9aa903b5ab4f52ebd Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 8 Nov 2023 23:43:59 -0800 Subject: [PATCH 09/53] Fix decode_config --- tests/test_decode.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_decode.py b/tests/test_decode.py index 12522a2..59d8c9f 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -21,10 +21,6 @@ def test_decode_config(response_collection: ScreenLogicResponseCollection): assert data == response_collection.config.decoded - decode_pool_config(buffer, data) - - assert data == expected - def test_decode_status(response_collection: ScreenLogicResponseCollection): data = {} From 5422fdf55ddd4d3a9a01954bd5987bb5adcedb69 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 8 Nov 2023 23:44:31 -0800 Subject: [PATCH 10/53] Add firmware and time query codes --- screenlogicpy/const/msg.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/screenlogicpy/const/msg.py b/screenlogicpy/const/msg.py index b38c968..d191aa5 100644 --- a/screenlogicpy/const/msg.py +++ b/screenlogicpy/const/msg.py @@ -17,6 +17,9 @@ class CODE: LOCALLOGIN_QUERY = 27 ERROR_INVALID_REQUEST = 30 ERROR_BAD_PARAMETER = 31 # Actually bad parameter? + FIRMWARE_QUERY = 8058 + GET_TIME_QUERY = 8110 + SET_TIME_QUERY = 8112 VERSION_QUERY = 8120 WEATHER_FORECAST_CHANGED = 9806 WEATHER_FORECAST_QUERY = 9807 From 0ade141e1a010f4b5276f9ad55b321c7ddfab846 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 8 Nov 2023 23:45:01 -0800 Subject: [PATCH 11/53] Update scg setpoint step to 1 --- ...slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json index 476f79a..9069fde 100644 --- a/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json +++ b/tests/data/slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json @@ -867,7 +867,7 @@ "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 0 }, "spa_setpoint": { @@ -876,7 +876,7 @@ "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 1 }, "super_chlor_timer": { @@ -1934,7 +1934,7 @@ "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 0 }, "spa_setpoint": { @@ -1943,7 +1943,7 @@ "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 1 }, "super_chlor_timer": { From c5ec5f5bf9a26879b1f9f25a1d4f831a2f4d021f Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sat, 11 Nov 2023 01:04:52 -0800 Subject: [PATCH 12/53] Exception refactor (#63) * New exception classes * Raise exception on unexpected response * Raise exceptions for undesirable responses * Fix condition typo * Standardize if statement * Exception refactor * Use new exceptions * Update for new exceptions * Replace deleted debug output * Update ping for new exception * Raise connection error when future canceled and connection is closed * Test updates --- screenlogicpy/cli.py | 86 ++++++++++++----------------- screenlogicpy/const/common.py | 12 ++++ screenlogicpy/gateway.py | 51 ++++++++--------- screenlogicpy/requests/button.py | 11 ++-- screenlogicpy/requests/chemistry.py | 13 +++-- screenlogicpy/requests/heat.py | 21 ++++--- screenlogicpy/requests/lights.py | 11 ++-- screenlogicpy/requests/login.py | 38 +++++-------- screenlogicpy/requests/ping.py | 11 ++-- screenlogicpy/requests/request.py | 39 +++++++++---- screenlogicpy/requests/scg.py | 12 ++-- tests/test_gateway.py | 12 ++-- tests/test_login.py | 31 +++++------ tests/test_request.py | 11 ++-- 14 files changed, 192 insertions(+), 167 deletions(-) diff --git a/screenlogicpy/cli.py b/screenlogicpy/cli.py index c2067ec..9f6b358 100644 --- a/screenlogicpy/cli.py +++ b/screenlogicpy/cli.py @@ -11,8 +11,7 @@ SL_GATEWAY_NAME, SL_GATEWAY_PORT, SLIntEnum, - ScreenLogicError, - ScreenLogicWarning, + ScreenLogicException, ) from screenlogicpy.data import build_response_collection, export_response_collection from screenlogicpy.device_const.chemistry import CHEM_RANGE @@ -69,10 +68,8 @@ async def async_set_circuit(): if circuit_id not in gateway.get_data(DEVICE.CIRCUIT): print(f"Invalid circuit number: {args.circuit_num}") return 4 - if await gateway.async_set_circuit(circuit_id, state): - await gateway.async_update() - else: - return 4 + await gateway.async_set_circuit(circuit_id, state) + await gateway.async_update() print( vFormat( gateway.get_data(DEVICE.CIRCUIT, circuit_id), @@ -94,10 +91,8 @@ async def async_get_heat_mode(): async def async_set_heat_mode(): body = BODY_TYPE.parse(args.body).value mode = HEAT_MODE.parse(args.mode).value - if await gateway.async_set_heat_mode(body, mode): - await gateway.async_update() - else: - return 8 + await gateway.async_set_heat_mode(body, mode) + await gateway.async_update() print( vFormat( gateway.get_data(DEVICE.BODY, body, VALUE.HEAT_MODE), @@ -118,10 +113,8 @@ async def async_get_heat_temp(): async def async_set_heat_temp(): body = BODY_TYPE.parse(args.body).value if args.temp != -1: - if await gateway.async_set_heat_temp(body, int(args.temp)): - await gateway.async_update() - else: - return 16 + await gateway.async_set_heat_temp(body, int(args.temp)) + await gateway.async_update() print( vFormat( gateway.get_data(DEVICE.BODY, body, VALUE.HEAT_SETPOINT), @@ -152,12 +145,9 @@ async def async_set_color_light(): mode = COLOR_MODE.parse(args.mode).value if mode is None: mode = int(args.mode) - if await gateway.async_set_color_lights(mode): - print( - f"Set color mode to {COLOR_MODE(mode).title}" if args.verbose else mode - ) - return 0 - return 32 + await gateway.async_set_color_lights(mode) + print(f"Set color mode to {COLOR_MODE(mode).title}" if args.verbose else mode) + return 0 async def async_set_scg_setpoint(): return await async_set_scg_config(pool=args.pool, spa=args.spa) @@ -190,19 +180,18 @@ async def async_set_scg_config( VALUE.SUPER_CHLOR_TIMER: time, } - if await gateway.async_set_scg_config(**kwargs): - # await asyncio.sleep(3) - await gateway.async_get_scg() - new_scg_config_data = gateway.get_data(DEVICE.SCG, GROUP.CONFIGURATION) - print( - *[ - vFormat(new_scg_config_data[key]) - for key, value in kwargs.items() - if key in new_scg_config_data and value is not None - ] - ) - return 0 - return 64 + await gateway.async_set_scg_config(**kwargs) + # await asyncio.sleep(3) + await gateway.async_get_scg() + new_scg_config_data = gateway.get_data(DEVICE.SCG, GROUP.CONFIGURATION) + print( + *[ + vFormat(new_scg_config_data[key]) + for key, value in kwargs.items() + if key in new_scg_config_data and value is not None + ] + ) + return 0 async def async_set_chem_setpoint(): return await async_set_chem_data(ph=args.ph, orp=args.orp) @@ -245,21 +234,18 @@ async def async_set_chem_data( VALUE.CYA: cyanuric_acid, VALUE.SALT_TDS_PPM: total_dissolved_solids, } - if await gateway.async_set_chem_data(**kwargs): - # await asyncio.sleep(3) - await gateway.async_get_chemistry() - new_chem_config_data = gateway.get_data( - DEVICE.INTELLICHEM, GROUP.CONFIGURATION - ) - print( - *[ - vFormat(new_chem_config_data[key]) - for key, value in kwargs.items() - if key in new_chem_config_data and value is not None - ] - ) - return 0 - return 128 + await gateway.async_set_chem_data(**kwargs) + # await asyncio.sleep(3) + await gateway.async_get_chemistry() + new_chem_config_data = gateway.get_data(DEVICE.INTELLICHEM, GROUP.CONFIGURATION) + print( + *[ + vFormat(new_chem_config_data[key]) + for key, value in kwargs.items() + if key in new_chem_config_data and value is not None + ] + ) + return 0 async def async_export_data_collection(): sl_ver = file_format(__version__) @@ -681,6 +667,6 @@ def print_dashboard(): await gateway.async_disconnect() return result - except (ScreenLogicError, ScreenLogicWarning) as err: + except ScreenLogicException as err: print(err) - return 128 + return -1 diff --git a/screenlogicpy/const/common.py b/screenlogicpy/const/common.py index f2d8936..4315b5f 100644 --- a/screenlogicpy/const/common.py +++ b/screenlogicpy/const/common.py @@ -39,6 +39,18 @@ class ScreenLogicRequestError(ScreenLogicException): pass +class ScreenLogicConnectionError(ScreenLogicException): + pass + + +class ScreenLogicResponseError(ScreenLogicException): + pass + + +class ScreenLogicLoginError(ScreenLogicException): + pass + + class SLIntEnum(IntEnum): @classmethod def parse(cls, value: str, default=0) -> SLIntEnum: diff --git a/screenlogicpy/gateway.py b/screenlogicpy/gateway.py index 55f1612..22b5d23 100644 --- a/screenlogicpy/gateway.py +++ b/screenlogicpy/gateway.py @@ -8,7 +8,9 @@ DATA_REQUEST, ON_OFF, ScreenLogicError, + ScreenLogicConnectionError, ScreenLogicRequestError, + ScreenLogicResponseError, ) from .const.msg import COM_MAX_RETRIES from .device_const.chemistry import CHEM_RANGE as cr @@ -164,14 +166,13 @@ async def async_disconnect(self, force=False): await self._protocol.async_close(force) - async def async_update(self) -> bool: + async def async_update(self) -> None: """ Update all ScreenLogic data. - - Try to reconnect to the ScreenLogic protocol adapter if needed. """ - if not await self.async_connect() or not self._data: - return False + + if not self._data: + raise ScreenLogicError("Internal data missing") _LOGGER.debug("Beginning update of all data") await self.async_get_status() @@ -179,7 +180,6 @@ async def async_update(self) -> bool: await self.async_get_chemistry() await self.async_get_scg() _LOGGER.debug("Update complete") - return True async def async_get_config(self): """Request pool configuration data.""" @@ -305,7 +305,7 @@ async def async_set_circuit(self, circuitID: int, circuitState: int): if not self._is_valid_circuit_state(circuitState): raise ValueError(f"Invalid circuitState: {circuitState}") - return await self._async_connected_request( + await self._async_connected_request( async_request_pool_button_press, circuitID, circuitState ) @@ -316,9 +316,7 @@ async def async_set_heat_temp(self, body: int, temp: int): if not self._is_valid_heattemp(body, temp): raise ValueError(f"Invalid temp ({temp}) for body ({body})") - return await self._async_connected_request( - async_request_set_heat_setpoint, body, temp - ) + await self._async_connected_request(async_request_set_heat_setpoint, body, temp) async def async_set_heat_mode(self, body: int, mode: int): """Set the heating mode for the specified body.""" @@ -327,16 +325,14 @@ async def async_set_heat_mode(self, body: int, mode: int): if not self._is_valid_heatmode(mode): raise ValueError(f"Invalid mode: {mode}") - return await self._async_connected_request( - async_request_set_heat_mode, body, mode - ) + await self._async_connected_request(async_request_set_heat_mode, body, mode) async def async_set_color_lights(self, light_command: int): """Set the light show mode for all capable lights.""" if not self._is_valid_color_mode(light_command): raise ValueError(f"Invalid light_command: {light_command}") - return await self._async_connected_request( + await self._async_connected_request( async_request_pool_lights_command, light_command ) @@ -381,7 +377,7 @@ async def async_set_scg_config( except (KeyError, ValueError) as ex: raise ScreenLogicError(ex.args[0]) from ex - return await self._async_connected_request( + await self._async_connected_request( async_request_set_scg_config, pool_setpoint, spa_setpoint, @@ -442,7 +438,7 @@ async def async_set_chem_data( ph_setpoint = int(ph_setpoint * 100) - return await self._async_connected_request( + await self._async_connected_request( async_request_set_chem_data, ph_setpoint, orp_setpoint, @@ -487,7 +483,9 @@ def remove_async_message_handler(self, message_code: int): if self._protocol: self._protocol.remove_async_message_callback(message_code) - async def async_send_message(self, message_code: int, message: bytes = b""): + async def async_send_message( + self, message_code: int, message: bytes = b"" + ) -> bytes: """Send a message to the ScreenLogic protocol adapter.""" _LOGGER.debug(f"User requesting {message_code}") return await self._async_connected_request( @@ -506,17 +504,20 @@ async def _async_connected_request( kwargs["max_retries"] = self._max_retries async def attempt_request(): - if await self.async_connect(): - return await async_method(self._protocol, *args, **kwargs) - - raise ScreenLogicError( - f"Not connected and unable to connect to protocol adapter to complete request: {async_method.func_name}" - ) + if not await self.async_connect(): + raise ScreenLogicConnectionError( + f"Not connected and unable to connect to protocol adapter to complete request: {async_method.func_name}" + ) + return await async_method(self._protocol, *args, **kwargs) try: return await attempt_request() - except ScreenLogicRequestError as re: - _LOGGER.debug("%s. Attempting to reconnect", re.msg) + except ( + ScreenLogicConnectionError, + ScreenLogicRequestError, + ScreenLogicResponseError, + ) as sle: + _LOGGER.debug("%s. Attempting to reconnect", sle.msg) await self.async_disconnect(True) await asyncio.sleep(reconnect_delay) try: diff --git a/screenlogicpy/requests/button.py b/screenlogicpy/requests/button.py index 1f37474..e9f41a4 100644 --- a/screenlogicpy/requests/button.py +++ b/screenlogicpy/requests/button.py @@ -1,5 +1,6 @@ import struct +from ..const.common import ScreenLogicResponseError from ..const.msg import CODE from .protocol import ScreenLogicProtocol from .request import async_make_request @@ -8,12 +9,14 @@ async def async_request_pool_button_press( protocol: ScreenLogicProtocol, circuit_id: int, circuit_state: int, max_retries: int ) -> bool: - return ( - await async_make_request( + if ( + response := await async_make_request( protocol, CODE.BUTTONPRESS_QUERY, struct.pack(" bool: - return ( - await async_make_request( + if ( + response := await async_make_request( protocol, CODE.SETHEATTEMP_QUERY, struct.pack(" bool: - return ( - await async_make_request( + if ( + response := await async_make_request( protocol, CODE.SETHEATMODE_QUERY, struct.pack(" bool: - return ( - await async_make_request( + if ( + response := await async_make_request( protocol, CODE.LIGHTCOMMAND_QUERY, struct.pack(" bool: _LOGGER.debug("Logging in") - try: - return ( - await async_make_request( - protocol, CODE.LOCALLOGIN_QUERY, create_login_message(), max_retries - ) - is not None + return ( + await async_make_request( + protocol, CODE.LOCALLOGIN_QUERY, create_login_message(), max_retries ) - except ScreenLogicRequestError as re: - raise ScreenLogicError(f"Failed to logon to gateway: {re.msg}") from re + is not None + ) async def async_connect_to_gateway( diff --git a/screenlogicpy/requests/ping.py b/screenlogicpy/requests/ping.py index cc9b327..9aa431b 100644 --- a/screenlogicpy/requests/ping.py +++ b/screenlogicpy/requests/ping.py @@ -1,10 +1,13 @@ +from ..const.common import ScreenLogicResponseError from ..const.msg import CODE from .protocol import ScreenLogicProtocol from .request import async_make_request async def async_request_ping(protocol: ScreenLogicProtocol, max_retries: int) -> bytes: - return ( - await async_make_request(protocol, CODE.PING_QUERY, max_retries=max_retries) - == b"" - ) + if ( + response := await async_make_request( + protocol, CODE.PING_QUERY, max_retries=max_retries + ) + ) != b"": + raise ScreenLogicResponseError(f"Ping failed. Unexpected response: {response}") diff --git a/screenlogicpy/requests/request.py b/screenlogicpy/requests/request.py index 97fb49e..2f73412 100644 --- a/screenlogicpy/requests/request.py +++ b/screenlogicpy/requests/request.py @@ -1,7 +1,12 @@ import asyncio import logging -from ..const import ScreenLogicRequestError +from ..const.common import ( + ScreenLogicConnectionError, + ScreenLogicLoginError, + ScreenLogicRequestError, + ScreenLogicResponseError, +) from ..const.msg import CODE, COM_MAX_RETRIES, COM_RETRY_WAIT, COM_TIMEOUT from .protocol import ScreenLogicProtocol from .utility import asyncio_timeout @@ -17,7 +22,7 @@ async def async_make_request( ) -> bytes: for attempt in range(0, max_retries + 1): if not protocol.is_connected: - raise ScreenLogicRequestError( + raise ScreenLogicConnectionError( "Unable to make request. No active connection" ) @@ -26,11 +31,15 @@ async def async_make_request( async with asyncio_timeout(COM_TIMEOUT): await request except asyncio.TimeoutError: - error_message = ( + last_error = ScreenLogicConnectionError( f"Timeout waiting for response to message code '{requestCode}'" ) except asyncio.CancelledError: - _LOGGER.debug(f"Future for request '{requestCode}' was canceled!") + _LOGGER.debug(f"Future for request '{requestCode}' was canceled") + if not protocol.is_connected: + raise ScreenLogicConnectionError( + f"Request '{requestCode}' canceled. Connection was closed" + ) return if not request.cancelled(): @@ -39,23 +48,29 @@ async def async_make_request( if responseCode == requestCode + 1: return responseData elif responseCode == CODE.ERROR_LOGIN_REJECTED: - error_message = f"Login Rejected for request code: {requestCode}, request: {requestData}" + raise ScreenLogicLoginError( + f"Login Rejected for request code: {requestCode}, request: {requestData}" + ) elif responseCode == CODE.ERROR_INVALID_REQUEST: - error_message = f"Invalid Request for request code: {requestCode}, request: {requestData}" + last_error = ScreenLogicRequestError( + f"Invalid Request for request code: {requestCode}, request: {requestData}" + ) elif responseCode == CODE.ERROR_BAD_PARAMETER: - error_message = f"Bad Parameter for request code: {requestCode}, request: {requestData}" + last_error = ScreenLogicRequestError( + f"Bad Parameter for request code: {requestCode}, request: {requestData}" + ) else: - error_message = f"Unexpected response code '{responseCode}' for request code: {requestCode}, request: {requestData}" + last_error = ScreenLogicResponseError( + f"Unexpected response code '{responseCode}' for request code: {requestCode}, request: {requestData}" + ) if attempt == max_retries: - raise ScreenLogicRequestError( - f"{error_message} after {max_retries + 1} attempts" - ) + raise last_error retry_delay = COM_RETRY_WAIT * (attempt + 1) _LOGGER.debug( - error_message + ". Will retry %i more time(s) in %i seconds", + last_error.msg + ". Will retry %i more time(s) in %i seconds", max_retries - attempt, retry_delay, ) diff --git a/screenlogicpy/requests/scg.py b/screenlogicpy/requests/scg.py index 82bbef6..203850e 100644 --- a/screenlogicpy/requests/scg.py +++ b/screenlogicpy/requests/scg.py @@ -1,6 +1,6 @@ import struct -from ..const.common import STATE_TYPE, UNIT, ON_OFF +from ..const.common import STATE_TYPE, UNIT, ON_OFF, ScreenLogicResponseError from ..const.msg import CODE, COM_MAX_RETRIES from ..const.data import ATTR, DEVICE, GROUP, VALUE from ..device_const.scg import SCG_RANGE @@ -87,12 +87,14 @@ async def async_request_set_scg_config( super_time: int = 0, max_retries: int = COM_MAX_RETRIES, ) -> bool: - return ( - await async_make_request( + if ( + response := await async_make_request( protocol, CODE.SETSCG_QUERY, struct.pack(" SLMessage: with patch.object( FakeTCPProtocolAdapter, "handle_logon_request", patch_handle_login_request - ), pytest.raises(ScreenLogicError) as sle: + ), pytest.raises(ScreenLogicLoginError) as sle: await async_connect_to_gateway( FAKE_GATEWAY_ADDRESS, FAKE_GATEWAY_PORT, max_retries=0 ) assert ( sle.value.msg - == "Failed to logon to gateway: Login Rejected for request code: 27, request: b'\\\\\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x00\\x00\\x00Android\\x00\\x10\\x00\\x00\\x000000000000000000\\x00\\x02\\x00\\x00\\x00' after 1 attempts" + == "Login Rejected for request code: 27, request: b'\\\\\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x00\\x00\\x00Android\\x00\\x10\\x00\\x00\\x000000000000000000\\x00\\x02\\x00\\x00\\x00'" ) diff --git a/tests/test_request.py b/tests/test_request.py index 5ee720d..b98ae4b 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -2,9 +2,10 @@ import pytest from unittest.mock import patch -from screenlogicpy import ScreenLogicGateway, ScreenLogicRequestError +from screenlogicpy import ScreenLogicGateway +from screenlogicpy.const.common import ScreenLogicConnectionError from screenlogicpy.const.msg import CODE -from screenlogicpy.requests.request import async_make_request +from screenlogicpy.requests.ping import async_request_ping from .adapter import FakeTCPProtocolAdapter, SLMessage from .const_data import FAKE_CONNECT_INFO @@ -25,7 +26,7 @@ def disconnecting_ping(self: FakeTCPProtocolAdapter, msg: SLMessage): await gateway.async_connect(**FAKE_CONNECT_INFO) assert gateway.is_connected with pytest.raises( - ScreenLogicRequestError, - match="Unable to make request. No active connection", + ScreenLogicConnectionError, + match="Request '16' canceled. Connection was closed", ): - await async_make_request(gateway._protocol, CODE.PING_QUERY) + await async_request_ping(gateway._protocol, 1) From acc7d6901fc2085e859a2579f348a8c1683a1eba Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:04:01 -0800 Subject: [PATCH 13/53] Refine connection logic, add firmware response --- tests/adapter.py | 153 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 33 deletions(-) diff --git a/tests/adapter.py b/tests/adapter.py index e3de7d0..b658e61 100644 --- a/tests/adapter.py +++ b/tests/adapter.py @@ -4,9 +4,15 @@ import struct from typing import Any -from screenlogicpy.const.msg import CODE +from screenlogicpy.const.msg import CODE, HEADER_LENGTH from screenlogicpy.data import ScreenLogicResponseCollection -from screenlogicpy.requests.utility import encodeMessageString, makeMessage, takeMessage +from screenlogicpy.requests.utility import ( + encodeMessageString, + makeMessage, + takeMessage, + getSome, + getString, +) from tests.const_data import ( FAKE_GATEWAY_MAC, @@ -22,15 +28,16 @@ class SLMessage: class CONNECTION_STATE(IntEnum): NO_CONNECTION = 0 - CONNECTSERVERHOST = 1 - CHALLENGE = 2 - LOGIN = 3 + PRIMED = 1 + CONNECTED = 2 class FakeTCPProtocolAdapter(asyncio.Protocol): def __init__(self, responses: ScreenLogicResponseCollection) -> None: self.responses = responses self._cs = CONNECTION_STATE.NO_CONNECTION + self._buff = bytearray() + print("TCP up") self.unconnected_response_map = { CODE.CHALLENGE_QUERY: self.handle_challenge_request, @@ -38,6 +45,7 @@ def __init__(self, responses: ScreenLogicResponseCollection) -> None: } self.connected_response_map = { CODE.VERSION_QUERY: self.handle_version_request, + CODE.FIRMWARE_QUERY: self.handle_firmware_request, CODE.CTRLCONFIG_QUERY: self.handle_config_request, CODE.POOLSTATUS_QUERY: self.handle_status_request, CODE.PUMPSTATUS_QUERY: self.handle_pump_state_request, @@ -56,47 +64,107 @@ def __init__(self, responses: ScreenLogicResponseCollection) -> None: def connection_made(self, transport: asyncio.Transport) -> None: self.transport = transport + print("TCP Connected.") def data_received(self, data: bytes) -> None: resp: SLMessage + print("Pentair data received!") + if (resp := self.process_data(data)) is not None: + print(f"Sent msg {resp.code}, {resp.data}") self.transport.write(makeMessage(resp.id, resp.code, resp.data)) def process_data(self, data: bytes) -> bytes: if self._cs == CONNECTION_STATE.NO_CONNECTION: if data == b"CONNECTSERVERHOST\r\n\r\n": - self._cs = CONNECTION_STATE.CONNECTSERVERHOST + self._cs = CONNECTION_STATE.PRIMED return None - return self.process_message(SLMessage(*takeMessage(data))) + # Adapter not primed. Go away. + self.transport.close() + + def complete_messages(data: bytes) -> list[tuple[int, int, bytes]]: + """Return only complete ScreenLogic messages.""" + + # Some pool configurations can require SL messages larger than can + # come through in a single call to data_received(), so lets wait until + # we have at least enough data to make a complete message before we + # process and pass it on. Conversely, multiple SL messages may come in + # a single call to data_received() so we collect all complete messages + # before sending on. + + self._buff.extend(data) + complete = [] + while len(self._buff) >= HEADER_LENGTH: + dataLen = struct.unpack_from("= nextMsgLen: + out = bytearray() + for _ in range(nextMsgLen): + out.append(self._buff.pop(0)) + complete.append(takeMessage(bytes(out))) + else: + break + if len(self._buff) > 0: + print( + f"Returned all messages with {len(self._buff)} bytes in the buffer" + ) + print(f"Buffer: {self._buff}") + return complete + + for message in complete_messages(data): + print(f"Received msg {message[1]}, {message[2]}") + return self.process_message(SLMessage(*message)) def process_message(self, msg: SLMessage) -> bytes: - if self._cs == CONNECTION_STATE.LOGIN: - if (code_handler := self.connected_response_map.get(msg.code)) is not None: - return code_handler(msg) - else: + if self._cs == CONNECTION_STATE.CONNECTED: + if ( + connected_handler := self.connected_response_map.get(msg.code) + ) is not None: + return connected_handler(msg) + + if self._cs >= CONNECTION_STATE.PRIMED: if ( - logon_handler := self.unconnected_response_map.get(msg.code) + primed_handler := self.unconnected_response_map.get(msg.code) ) is not None: - return logon_handler(msg) - return SLMessage(msg.id, CODE.ERROR_INVALID_REQUEST) + return primed_handler(msg) + else: + return SLMessage(msg.id, CODE.ERROR_INVALID_REQUEST) + + self.transport.close() def handle_challenge_request(self, msg: SLMessage) -> SLMessage: - if self._cs == CONNECTION_STATE.CONNECTSERVERHOST: - self._cs = CONNECTION_STATE.CHALLENGE - return SLMessage( - msg.id, msg.code + 1, encodeMessageString(FAKE_GATEWAY_MAC) - ) - return SLMessage(msg.id, CODE.ERROR_BAD_PARAMETER) + return SLMessage( + msg.id, + msg.code + 1, + encodeMessageString(FAKE_GATEWAY_MAC).ljust(28, b"\x00"), + ) def handle_logon_request(self, msg: SLMessage) -> SLMessage: - if self._cs == CONNECTION_STATE.CHALLENGE: - self._cs = CONNECTION_STATE.LOGIN + schema, connType, clientType, passWord, pid = decode_login(msg.data) + if pid == 2: + self._cs = CONNECTION_STATE.CONNECTED return SLMessage(msg.id, msg.code + 1) return SLMessage(msg.id, CODE.ERROR_LOGIN_REJECTED) def handle_version_request(self, msg: SLMessage) -> SLMessage: return SLMessage(msg.id, msg.code + 1, self.responses.version.raw) + def handle_firmware_request(self, msg: SLMessage) -> SLMessage: + return SLMessage( + msg.id, + msg.code + 1, + b"\x50\x00\x00\x00\x50\x00\x00\x00\x11\x00\x00\x00\x50\x65\x6e\x74" + b"\x61\x69\x72\x3a\x20\x41\x41\x2d\x42\x42\x2d\x43\x43\x00\x00\x00" + b"\x05\x00\x00\x00\x39\x30\x30\x30\x31\x00\x00\x00\x84\x2b\x05\x00" + b"\xa4\xeb\x24\x00\xf9\xff\xff\xff\x19\x00\x00\x00\x50\x4f\x4f\x4c" + b"\x3a\x20\x35\x2e\x32\x20\x42\x75\x69\x6c\x64\x20\x37\x33\x36\x2e" + b"\x30\x20\x52\x65\x6c\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x01\x00\x00\x00\x30\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x52\x61\x62\x62" + b"\x69\x74\x20\x43\x6f\x72\x65\x00\x05\x00\x00\x00\x42\x52\x49\x43" + b"\x4b\x00\x00\x00", + ) + def handle_config_request(self, msg: SLMessage) -> SLMessage: return SLMessage(msg.id, msg.code + 1, self.responses.config.raw) @@ -104,8 +172,8 @@ def handle_status_request(self, msg: SLMessage) -> SLMessage: return SLMessage(msg.id, msg.code + 1, self.responses.status.raw) def handle_pump_state_request(self, msg: SLMessage) -> SLMessage: - pump = struct.unpack_from(" SLMessage: return SLMessage(msg.id, msg.code + 1, self.responses.chemistry.raw) @@ -114,40 +182,59 @@ def handle_scg_status_request(self, msg: SLMessage) -> SLMessage: return SLMessage(msg.id, msg.code + 1, self.responses.scg.raw) def handle_button_press_request(self, msg: SLMessage) -> SLMessage: - return SLMessage(msg.id, msg.code + 1) + return default_response(msg) def handle_light_command_request(self, msg: SLMessage) -> SLMessage: - return SLMessage(msg.id, msg.code + 1) + return default_response(msg) def handle_set_heat_mode_request(self, msg: SLMessage) -> SLMessage: - return SLMessage(msg.id, msg.code + 1) + return default_response(msg) def handle_set_heat_temp_request(self, msg: SLMessage) -> SLMessage: - return SLMessage(msg.id, msg.code + 1) + return default_response(msg) def handle_set_scg_config_request(self, msg: SLMessage) -> SLMessage: - return SLMessage(msg.id, msg.code + 1) + return default_response(msg) def handle_set_chemistry_config_request(self, msg: SLMessage) -> SLMessage: - return SLMessage(msg.id, msg.code + 1) + return default_response(msg) def handle_add_client_request(self, msg: SLMessage) -> SLMessage: - return SLMessage(msg.id, msg.code + 1) + return default_response(msg) def handle_remove_client_request(self, msg: SLMessage) -> SLMessage: - return SLMessage(msg.id, msg.code + 1) + return default_response(msg) def handle_ping_request(self, msg: SLMessage) -> SLMessage: - return SLMessage(msg.id, msg.code + 1) + return default_response(msg) class FakeUDPProtocolAdapter(asyncio.DatagramProtocol): def __init__(self, discovery_response: bytes) -> None: self.discovery_response = discovery_response + print("UDP up)") def connection_made(self, transport: asyncio.DatagramTransport) -> None: + print("Connection") self.transport = transport def datagram_received(self, data: bytes, addr: tuple[str | Any, int]) -> None: + print("Received datagram.") if struct.unpack("<8b", data) == (1, 0, 0, 0, 0, 0, 0, 0): + print("Pentair discovery!", addr, self.discovery_response) self.transport.sendto(self.discovery_response, addr) + + +def default_response(msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1) + + +def decode_login(buff: bytes) -> tuple[bytes]: + schema, offset = getSome("I", buff, 0) + connType, offset = getSome("I", buff, offset) + clientType, offset = getString(buff, offset) + passWord, offset = getString(buff, offset) + offset += 1 + pid, offset = getSome("I", buff, offset) + + return schema, connType, clientType, passWord, pid From fd191d3f5bbc02bc6c8225e545980c4644209cb3 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sat, 11 Nov 2023 15:11:51 -0800 Subject: [PATCH 14/53] Handle bad pump number requests --- tests/adapter.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/adapter.py b/tests/adapter.py index b658e61..caeca77 100644 --- a/tests/adapter.py +++ b/tests/adapter.py @@ -172,7 +172,13 @@ def handle_status_request(self, msg: SLMessage) -> SLMessage: return SLMessage(msg.id, msg.code + 1, self.responses.status.raw) def handle_pump_state_request(self, msg: SLMessage) -> SLMessage: - pump_num, _ = getSome("I", msg.data, 4) + try: + pump_num, _ = getSome("I", msg.data, 4) + if not 0 <= pump_num <= 7: + raise ValueError("Invalid pump number") + except Exception as ex: + print(ex) + return SLMessage(msg.id, CODE.ERROR_BAD_PARAMETER) return SLMessage(msg.id, msg.code + 1, self.responses.pumps[pump_num].raw) def handle_chemistry_status_request(self, msg: SLMessage) -> SLMessage: From e50941aafce3575b5224cd0c3a7fd603ca697d56 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sat, 11 Nov 2023 18:55:36 -0800 Subject: [PATCH 15/53] Refactor data processing --- tests/adapter.py | 93 +++++++++++++++++++++--------------------------- 1 file changed, 40 insertions(+), 53 deletions(-) diff --git a/tests/adapter.py b/tests/adapter.py index caeca77..b24ba63 100644 --- a/tests/adapter.py +++ b/tests/adapter.py @@ -67,70 +67,56 @@ def connection_made(self, transport: asyncio.Transport) -> None: print("TCP Connected.") def data_received(self, data: bytes) -> None: - resp: SLMessage print("Pentair data received!") - - if (resp := self.process_data(data)) is not None: - print(f"Sent msg {resp.code}, {resp.data}") - self.transport.write(makeMessage(resp.id, resp.code, resp.data)) + self.process_data(data) def process_data(self, data: bytes) -> bytes: if self._cs == CONNECTION_STATE.NO_CONNECTION: if data == b"CONNECTSERVERHOST\r\n\r\n": + print("Adapter primmed!") self._cs = CONNECTION_STATE.PRIMED return None # Adapter not primed. Go away. self.transport.close() - def complete_messages(data: bytes) -> list[tuple[int, int, bytes]]: - """Return only complete ScreenLogic messages.""" - - # Some pool configurations can require SL messages larger than can - # come through in a single call to data_received(), so lets wait until - # we have at least enough data to make a complete message before we - # process and pass it on. Conversely, multiple SL messages may come in - # a single call to data_received() so we collect all complete messages - # before sending on. - - self._buff.extend(data) - complete = [] - while len(self._buff) >= HEADER_LENGTH: - dataLen = struct.unpack_from("= nextMsgLen: - out = bytearray() - for _ in range(nextMsgLen): - out.append(self._buff.pop(0)) - complete.append(takeMessage(bytes(out))) - else: - break - if len(self._buff) > 0: - print( - f"Returned all messages with {len(self._buff)} bytes in the buffer" - ) - print(f"Buffer: {self._buff}") - return complete - - for message in complete_messages(data): - print(f"Received msg {message[1]}, {message[2]}") - return self.process_message(SLMessage(*message)) - - def process_message(self, msg: SLMessage) -> bytes: - if self._cs == CONNECTION_STATE.CONNECTED: - if ( - connected_handler := self.connected_response_map.get(msg.code) - ) is not None: - return connected_handler(msg) - - if self._cs >= CONNECTION_STATE.PRIMED: - if ( - primed_handler := self.unconnected_response_map.get(msg.code) - ) is not None: - return primed_handler(msg) + self._buff.extend(data) + + # Take complete messages + while len(self._buff) >= HEADER_LENGTH: + dataLen = struct.unpack_from("= nextMsgLen: + out = bytearray() + for _ in range(nextMsgLen): + out.append(self._buff.pop(0)) + self.process_message(SLMessage(*takeMessage(bytes(out)))) + print(f"{len(self._buff)} left in buffer") else: - return SLMessage(msg.id, CODE.ERROR_INVALID_REQUEST) + break + + def process_message(self, msg: SLMessage) -> None: + print(f"Received msg {msg.code}, {msg.data}") + + def get_response(msg: SLMessage) -> SLMessage | None: + if self._cs == CONNECTION_STATE.CONNECTED: + if ( + connected_handler := self.connected_response_map.get(msg.code) + ) is not None: + return connected_handler(msg) + + if self._cs >= CONNECTION_STATE.PRIMED: + if ( + primed_handler := self.unconnected_response_map.get(msg.code) + ) is not None: + return primed_handler(msg) + else: + return SLMessage(msg.id, CODE.ERROR_INVALID_REQUEST) - self.transport.close() + self.transport.close() + + if (resp := get_response(msg)) is not None: + print(f"Sent msg {resp.code}, {resp.data}") + self.transport.write(makeMessage(resp.id, resp.code, resp.data)) def handle_challenge_request(self, msg: SLMessage) -> SLMessage: return SLMessage( @@ -240,7 +226,8 @@ def decode_login(buff: bytes) -> tuple[bytes]: connType, offset = getSome("I", buff, offset) clientType, offset = getString(buff, offset) passWord, offset = getString(buff, offset) - offset += 1 + if schema == 348: + offset += 1 pid, offset = getSome("I", buff, offset) return schema, connType, clientType, passWord, pid From c31624dc2e0aba43a0f97ca5542a5ec4e7295dc6 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Tue, 14 Nov 2023 01:04:12 -0800 Subject: [PATCH 16/53] Time/Date sync (#64) * Time utilities; Support SL utf-16 string encoding. * Update autoDST to int * Support getting and setting date+time * Identified tz_adjust * v0.9.0 updates * Time utilities; Support SL utf-16 string encoding. * CLI support * Update .gitignore * Fix Hardness * New exception handling * Remove duplicate getTime * Update kwargs format * Datetime command functions * Updated response collection for v0.10.0 * Test date_time cli * Handle date_time requests * SCG state uses value * auto_dst as dict * Include date_time in data export * Zero out seconds in encoded time * Correct aDST to int * Flip success logic * Set current date-time if no parameters specified * Updated for no parameter date-time * Fix auto_dst dict path --- .gitignore | 1 + notes/8058_sys_data.txt | 4 +- notes/8110_get_datetime.txt | 15 +- notes/8112_set_datetime.txt | 12 +- screenlogicpy/cli.py | 94 +- screenlogicpy/const/common.py | 1 + screenlogicpy/const/data.py | 3 + screenlogicpy/const/msg.py | 4 +- screenlogicpy/data.py | 3 + screenlogicpy/gateway.py | 40 +- screenlogicpy/requests/__init__.py | 1 + screenlogicpy/requests/datetime.py | 54 + screenlogicpy/requests/scg.py | 2 +- screenlogicpy/requests/utility.py | 52 +- tests/adapter.py | 67 + tests/conftest.py | 2 +- ...-52-build-7360-rel_easytouch2-8_98360.json | 1983 +++++++++++++++++ tests/test_cli.py | 34 + tests/test_data.py | 2 + 19 files changed, 2346 insertions(+), 28 deletions(-) create mode 100644 screenlogicpy/requests/datetime.py create mode 100644 tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json diff --git a/.gitignore b/.gitignore index 43a0c52..f95b056 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,4 @@ dmypy.json notes/notes.txt notes/circuit_names*.txt scratchpad/local_* +notes/8060_set_sys_data.txt \ No newline at end of file diff --git a/notes/8058_sys_data.txt b/notes/8058_sys_data.txt index e98f462..8248cc3 100644 --- a/notes/8058_sys_data.txt +++ b/notes/8058_sys_data.txt @@ -1,4 +1,4 @@ 13 00|7b 1f|94 00 00 00|50 00 00 00|50 00 00 00|11 00 00 00|50 65 6e 74 61 69 72 3a 20 XX XX 2d XX XX 2d XX XX 00 00 00|05 00 00 00|XX XX XX XX XX 00 00 00|XX XX XX XX|XX XX XX XX|fb ff ff ff|19 00 00 00|50 4f 4f 4c 3a 20 35 2e 32 20 42 75 69 6c 64 20 37 33 36 2e 30 20 52 65 6c 00 00 00|01 00 00 00|00 00 00 00|00 00 00 00|01 00 00 00|30 00 00 00|00 00 00 00|00 00 00 00|00 00 00 00|0b 00 00 00|52 61 62 62 69 74 20 43 6f 72 65 00|05 00 00 00|42 52 49 43 4b 00 00 00 -msgID|msgCD|msgLength | | |aptrNameLen|adapterName |zipLen |zipCode |latitudeN |longitudeW?| |versionLen |version | | | | | | | | |coreLen |core |styleLen |style -13 8058 148 |80 |80 |17 |P e n t a i r : X X - X X - X X |5 |X X X X X XX/1000 XX/1000 25 P O O L : 5 . 2 B u i l d 7 3 6 . 0 R e l 1 0 0 1 48 0 0 0 11 R a b b i t C o r e |5 |B R I C K \ No newline at end of file +msgID|msgCD|msgLength | | |aptrNameLen|adapterName |zipLen |zipCode |latitudeN |longitudeW?|tz_adjust |versionLen |version | | | | | | | | |coreLen |core |styleLen |style +13 8058 148 |80 |80 |17 |P e n t a i r : X X - X X - X X |5 |X X X X X XX/1000 XX/1000 -5 25 P O O L : 5 . 2 B u i l d 7 3 6 . 0 R e l 1 0 0 1 48 0 0 0 11 R a b b i t C o r e |5 |B R I C K \ No newline at end of file diff --git a/notes/8110_get_datetime.txt b/notes/8110_get_datetime.txt index ad8e525..1512b30 100644 --- a/notes/8110_get_datetime.txt +++ b/notes/8110_get_datetime.txt @@ -1,3 +1,12 @@ -e7 07|01 00|04 00|1b 00|0e 00|13 00|1f 00|00 00|01 00|00 00 -year |month|dayOw|date |hour |min |sec |ms |aDST |isDST? -2023 |1 |4 |27 |14 |19 |31 |0 |1 |? \ No newline at end of file +Request: +b"" + +Response: +e7 07|01 00|04 00|1b 00|0e 00|13 00|1f 00|00 00|01 00 00 00 +year |month|dayOw|date |hour |min |sec |ms |autoDST | +2023 |1 |4 |27 |14 |19 |31 |0 |1 | + + +e7 07 03 00 06 00 0c 00 0a 00 22 00 1b 00 00 00 01 00 00 00 +year |month|dayOw|date |hour |min |sec |ms |autoDST | +2023 |3 |6 |12 |10 |34 |27 |0 |1 | \ No newline at end of file diff --git a/notes/8112_set_datetime.txt b/notes/8112_set_datetime.txt index 91282a7..9d23468 100644 --- a/notes/8112_set_datetime.txt +++ b/notes/8112_set_datetime.txt @@ -1,10 +1,10 @@ -e7 07|01 00|04 00|1b 00|0e 00|13 00|00 00|cc 01|00 00|00 00 -year |month|dayOw|date |hour |min |sec |ms |aDST |isDST? -2023 |1 |4 |27 |14 |19 |00 |460 |0 |? +e7 07|01 00|04 00|1b 00|0e 00|13 00|00 00|cc 01|00 00 00 00 +year |month|dayOw|date |hour |min |sec |ms |aDST +2023 |1 |4 |27 |14 |19 |00 |460 |0 -e7 07|01 00|04 00|1b 00|0e 00|15 00|00 00|1d 03|01 00|00 00 -year |month|dayOw|date |hour |min |sec |ms |aDST |isDST? -2023 |1 |4 |27 |14 |21 |00 |797 |1 |? +e7 07|01 00|04 00|1b 00|0e 00|15 00|00 00|1d 03|01 00 00 00 +year |month|dayOw|date |hour |min |sec |ms |aDST +2023 |1 |4 |27 |14 |21 |00 |797 |1 dayOw Monday = 0 diff --git a/screenlogicpy/cli.py b/screenlogicpy/cli.py index 9f6b358..5ce4ea4 100644 --- a/screenlogicpy/cli.py +++ b/screenlogicpy/cli.py @@ -1,7 +1,10 @@ +import argparse +import asyncio +from datetime import datetime +import json import logging import string -import json -import argparse + from screenlogicpy import __version__ from screenlogicpy.discovery import async_discover from screenlogicpy.gateway import ScreenLogicGateway @@ -247,6 +250,53 @@ async def async_set_chem_data( ) return 0 + async def async_get_date_time(): + format = args.format + await gateway.async_get_datetime() + timestamp = gateway.get_data( + DEVICE.CONTROLLER, GROUP.DATE_TIME, VALUE.TIMESTAMP, strict=True + ) + if format is None: + print(datetime.fromtimestamp(timestamp)) + else: + print(datetime.fromtimestamp(timestamp).strftime(format)) + return 0 + + async def async_get_auto_dst(): + await gateway.async_get_datetime() + print( + vFormat( + gateway.get_data(DEVICE.CONTROLLER, GROUP.DATE_TIME, VALUE.AUTO_DST) + ) + ) + return 0 + + async def async_set_date_time(): + date_time = ( + datetime.fromisoformat(args.date_time) + if args.date_time is not None + else None + ) + auto_dst = args.auto_dst + + if all( + ( + date_time is None, + auto_dst is None, + ) + ): + date_time = datetime.now() + + await gateway.async_get_datetime() + await gateway.async_set_date_time(date_time=date_time, auto_dst=auto_dst) + await asyncio.sleep(0.5) + await gateway.async_get_datetime() + timestamp = gateway.get_data( + DEVICE.CONTROLLER, GROUP.DATE_TIME, VALUE.TIMESTAMP, strict=True + ) + print(f"Controller time now: {datetime.fromtimestamp(timestamp)}") + return 0 + async def async_export_data_collection(): sl_ver = file_format(__version__) pa_ver = file_format(gateway.version) @@ -259,11 +309,11 @@ async def async_export_data_collection(): export_response_collection(response_collection, filename) return 0 - # Begin Parser Setup async def async_get_json(): print(json.dumps(gateway.get_data(), indent=2)) return 0 + # Begin Parser Setup option_parser = argparse.ArgumentParser( prog="screenlogicpy", description="Interface for Pentair Screenlogic gateway" ) @@ -350,9 +400,20 @@ async def async_get_json(): get_current_temp_parser.add_argument(**ARGUMENT_BODY) get_current_temp_parser.set_defaults(async_func=async_get_current_temp) - get_json_parser = get_subparsers.add_parser( - "json", aliases=["j"], help="Return the full data dict as JSON" + get_date_time_parser = get_subparsers.add_parser("date-time", aliases=["dt"]) + get_date_time_parser.add_argument( + "-f", + "--format", + default=None, + type=str, + help="Optional format string to format the datetime value", ) + get_date_time_parser.set_defaults(async_func=async_get_date_time) + + get_auto_dst_parser = get_subparsers.add_parser("auto-dst", aliases=["dst"]) + get_auto_dst_parser.set_defaults(async_func=async_get_auto_dst) + + get_json_parser = get_subparsers.add_parser("json", aliases=["j"]) get_json_parser.set_defaults(async_func=async_get_json) # Set options @@ -533,6 +594,29 @@ async def async_get_json(): ) set_chem_data_parser.set_defaults(async_func=async_set_chem_value) + set_date_time_parser = set_subparsers.add_parser( + "date-time", + aliases=["dt"], + help="Sets pool controller date/time to host's date and time.", + ) + set_date_time_parser.add_argument( + "-dt", + "--date-time", + default=None, + metavar="ISO_8601", + type=str, + help="Specify a datetime with an ISO 8601 formatted string", + ) + set_date_time_parser.add_argument( + "-dst", + "--auto-dst", + default=None, + choices=on_off_options, + type=str, + help="Automatic adjustment of system time for Daylight Saving Time", + ) + set_date_time_parser.set_defaults(async_func=async_set_date_time) + args = option_parser.parse_args(cli_args) if args.verbose == 2: diff --git a/screenlogicpy/const/common.py b/screenlogicpy/const/common.py index 4315b5f..459f902 100644 --- a/screenlogicpy/const/common.py +++ b/screenlogicpy/const/common.py @@ -105,6 +105,7 @@ def parse_check(self, string: str) -> int | float: class DATA_REQUEST: CHEMISTRY = "chemistry" + DATE_TIME = "date_time" KEY_COLOR = "color" CONFIG = "config" PUMPS = "pumps" diff --git a/screenlogicpy/const/data.py b/screenlogicpy/const/data.py index d817509..b601913 100644 --- a/screenlogicpy/const/data.py +++ b/screenlogicpy/const/data.py @@ -43,6 +43,7 @@ class GROUP: COLOR = "color" COLOR_LIGHTS = "color_lights" CONFIGURATION = "configuration" + DATE_TIME = "date_time" DOSE_STATUS = "dose_status" EQUIPMENT = "equipment" SENSOR = "sensor" @@ -52,6 +53,7 @@ class GROUP: class VALUE: ACTIVE_ALERT = "active_alert" AIR_TEMPERATURE = "air_temperature" + AUTO_DST = "auto_dst" CALCIUM_HARDNESS = "calcium_hardness" CIRCUIT_COUNT = "circuit_count" CLEANER_DELAY = "cleaner_delay" @@ -123,6 +125,7 @@ class VALUE: STATE = "state" SUPER_CHLOR_TIMER = "super_chlor_timer" SUPER_CHLORINATE = "super_chlorinate" + TIMESTAMP = "timestamp" TOTAL_ALKALINITY = "total_alkalinity" TYPE = "type" WATTS_NOW = "watts_now" diff --git a/screenlogicpy/const/msg.py b/screenlogicpy/const/msg.py index d191aa5..57b75ee 100644 --- a/screenlogicpy/const/msg.py +++ b/screenlogicpy/const/msg.py @@ -18,8 +18,8 @@ class CODE: ERROR_INVALID_REQUEST = 30 ERROR_BAD_PARAMETER = 31 # Actually bad parameter? FIRMWARE_QUERY = 8058 - GET_TIME_QUERY = 8110 - SET_TIME_QUERY = 8112 + GET_DATETIME_QUERY = 8110 + SET_DATETIME_QUERY = 8112 VERSION_QUERY = 8120 WEATHER_FORECAST_CHANGED = 9806 WEATHER_FORECAST_QUERY = 9807 diff --git a/screenlogicpy/data.py b/screenlogicpy/data.py index 0414d8b..abde356 100644 --- a/screenlogicpy/data.py +++ b/screenlogicpy/data.py @@ -5,6 +5,7 @@ from .const.common import DATA_REQUEST from .requests.chemistry import decode_chemistry from .requests.config import decode_pool_config +from .requests.datetime import decode_date_time from .requests.gateway import decode_version from .requests.lights import decode_color_update from .requests.pump import decode_pump_status @@ -19,6 +20,7 @@ DATA_REQUEST.CHEMISTRY: decode_chemistry, DATA_REQUEST.SCG: decode_scg_config, DATA_REQUEST.KEY_COLOR: decode_color_update, + DATA_REQUEST.DATE_TIME: decode_date_time, } @@ -38,6 +40,7 @@ class ScreenLogicResponseCollection: chemistry: ScreenLogicResponseSet | None = None scg: ScreenLogicResponseSet | None = None color: list[ScreenLogicResponseSet] = None + date_time: ScreenLogicResponseSet | None = None def build_response_collection(raw: dict, data: dict) -> ScreenLogicResponseCollection: diff --git a/screenlogicpy/gateway.py b/screenlogicpy/gateway.py index 22b5d23..eac686a 100644 --- a/screenlogicpy/gateway.py +++ b/screenlogicpy/gateway.py @@ -1,5 +1,6 @@ """Describes a ScreenLogicGateway class for interacting with a Pentair ScreenLogic system.""" import asyncio +from datetime import datetime import logging from typing import Awaitable, Callable @@ -19,12 +20,14 @@ from .const.data import ATTR, DEVICE, GROUP, VALUE from .requests import ( async_connect_to_gateway, + async_request_date_time, async_request_gateway_version, async_request_pool_button_press, async_request_pool_config, async_request_pool_lights_command, async_request_pool_status, async_request_pump_status, + async_request_set_date_time, async_request_set_heat_mode, async_request_set_heat_setpoint, async_request_chemistry, @@ -224,9 +227,13 @@ async def async_get_scg(self): ): self._last[DATA_REQUEST.SCG] = last_raw - # def get_data(self) -> dict: - # """Return the data.""" - # return self._data + async def async_get_datetime(self): + """Request the current date and time from the controller.""" + _LOGGER.debug("Requesting date/time") + if last_raw := await self._async_connected_request( + async_request_date_time, self._data, reconnect_delay=1 + ): + self._last[DATA_REQUEST.DATE_TIME] = last_raw def get_data(self, *keypath, strict: bool = False): """ @@ -448,6 +455,33 @@ async def async_set_chem_data( salt_tds_ppm, ) + async def async_set_date_time( + self, + *, + date_time: datetime | None = None, + auto_dst: int | None = None, + ): + """Set date and time settings on the controller.""" + if date_time is None and auto_dst is None: + raise ValueError("No date/time values to set") + + DATETIME_CONFIG = (DEVICE.CONTROLLER, GROUP.DATE_TIME) + + if date_time is None: + date_time = datetime.fromtimestamp( + self.get_data(*DATETIME_CONFIG, VALUE.TIMESTAMP, strict=True) + ) + if auto_dst is None: + auto_dst = self.get_value(*DATETIME_CONFIG, VALUE.AUTO_DST, strict=True) + + return await self._async_connected_request( + async_request_set_date_time, date_time, auto_dst + ) + + async def async_synchronize_date_time(self): + """Set the date and time on the controller to the current system time.""" + return await self.async_set_date_time(date_time=datetime.now()) + async def async_subscribe_client( self, callback: Callable[..., any], code: int ) -> Callable: diff --git a/screenlogicpy/requests/__init__.py b/screenlogicpy/requests/__init__.py index fe84eab..0557a2a 100644 --- a/screenlogicpy/requests/__init__.py +++ b/screenlogicpy/requests/__init__.py @@ -3,6 +3,7 @@ from .chemistry import async_request_chemistry, async_request_set_chem_data from .client import async_request_add_client, async_request_remove_client from .config import async_request_pool_config +from .datetime import async_request_date_time, async_request_set_date_time from .equipment import async_request_equipment_config from .gateway import async_request_gateway_version from .heat import async_request_set_heat_mode, async_request_set_heat_setpoint diff --git a/screenlogicpy/requests/datetime.py b/screenlogicpy/requests/datetime.py new file mode 100644 index 0000000..a76510a --- /dev/null +++ b/screenlogicpy/requests/datetime.py @@ -0,0 +1,54 @@ +import datetime + + +import struct + +from ..const.common import ScreenLogicResponseError +from ..const.data import DEVICE, GROUP, VALUE +from ..const.msg import CODE +from .protocol import ScreenLogicProtocol +from .request import async_make_request +from .utility import getSome, getTime, encodeMessageTime + + +async def async_request_date_time( + protocol: ScreenLogicProtocol, data: dict, max_retries: int = None +) -> bytes: + if result := await async_make_request( + protocol, CODE.GET_DATETIME_QUERY, max_retries=max_retries + ): + decode_date_time(result, data) + return result + + +def decode_date_time(buffer: bytes, data: dict): + controller: dict = data.setdefault(DEVICE.CONTROLLER, {}) + date_time: dict = controller.setdefault(GROUP.DATE_TIME, {}) + + dt, offset = getTime(buffer, 0) + date_time[VALUE.TIMESTAMP] = dt.timestamp() + + auto_dst, offset = getSome("I", buffer, offset) + date_time[VALUE.AUTO_DST] = { + "name": "Automatic Daylight Saving Time", + "value": auto_dst, + } + + +async def async_request_set_date_time( + protocol: ScreenLogicProtocol, + date_time: datetime, + auto_dst: int, + max_retries: int = None, +) -> bool: + if ( + response := await async_make_request( + protocol, + CODE.SET_DATETIME_QUERY, + encodeMessageTime(date_time) + struct.pack(" None: state, offset = getSome("I", buff, offset) # 4 scg_sensor[VALUE.STATE] = { ATTR.NAME: "Chlorinator", - ATTR.VALUE: ON_OFF.from_bool(state & 0x01), + ATTR.VALUE: ON_OFF.from_bool(state & 0x01).value, } scg_config: dict = scg.setdefault(GROUP.CONFIGURATION, {}) diff --git a/screenlogicpy/requests/utility.py b/screenlogicpy/requests/utility.py index cc5fbf3..589fefb 100644 --- a/screenlogicpy/requests/utility.py +++ b/screenlogicpy/requests/utility.py @@ -1,3 +1,4 @@ +from datetime import datetime import struct import sys from typing import Any @@ -54,22 +55,48 @@ def takeMessages(data: bytes) -> list[tuple[int, int, bytes]]: ) from err -def encodeMessageString(string) -> bytes: - data = string.encode() +def encodeMessageString(string: str, utf_16: bool = False) -> bytes: + encoding = "utf-16" if utf_16 else "utf-8" + data = string.encode(encoding) length = len(data) over = length % 4 pad = (4 - over) if over > 0 else 0 # pad string to multiple of 4 fmt = f" str: + encoding = "utf-8" size = struct.unpack_from(" datetime: + year, month, _, day, hour, minute, second, millisecond = struct.unpack("<8H", data) + return datetime(year, month, day, hour, minute, second, millisecond * 1000) + + def getSome(format, buff, offset) -> tuple[Any, int]: fmt = format if format.startswith(">") else f"<{format}" newoffset = offset + struct.calcsize(fmt) @@ -98,15 +125,18 @@ def getValueAt(buff, offset, want, **kwargs): def getString(buff, offset) -> tuple[str, int]: fmtLen = " tuple[datetime, int]: + fmt = "<8H" + year, month, _, day, hour, minute, second, millisecond = struct.unpack_from( + fmt, buff, offset + ) + new_offset = offset + struct.calcsize(fmt) + return ( + datetime(year, month, day, hour, minute, second, millisecond * 1000), + new_offset, + ) + + def getTemperatureUnit(data: dict): return ( UNIT.CELSIUS diff --git a/tests/adapter.py b/tests/adapter.py index b24ba63..1354948 100644 --- a/tests/adapter.py +++ b/tests/adapter.py @@ -1,6 +1,7 @@ import asyncio from enum import IntEnum from dataclasses import dataclass +from datetime import datetime import struct from typing import Any @@ -8,6 +9,7 @@ from screenlogicpy.data import ScreenLogicResponseCollection from screenlogicpy.requests.utility import ( encodeMessageString, + encodeMessageTime, makeMessage, takeMessage, getSome, @@ -60,6 +62,9 @@ def __init__(self, responses: ScreenLogicResponseCollection) -> None: CODE.ADD_CLIENT_QUERY: self.handle_add_client_request, CODE.REMOVE_CLIENT_QUERY: self.handle_remove_client_request, CODE.PING_QUERY: self.handle_ping_request, + CODE.GET_DATETIME_QUERY: self.handle_date_time_request, + CODE.SET_DATETIME_QUERY: self.handle_set_date_time_request, + CODE.EQUIPMENT_QUERY: self.handle_equipment_request, } def connection_made(self, transport: asyncio.Transport) -> None: @@ -200,6 +205,19 @@ def handle_remove_client_request(self, msg: SLMessage) -> SLMessage: def handle_ping_request(self, msg: SLMessage) -> SLMessage: return default_response(msg) + def handle_date_time_request(self, msg: SLMessage) -> SLMessage: + return SLMessage( + msg.id, + msg.code + 1, + encodeMessageTime(datetime.now()) + struct.pack(" SLMessage: + return default_response(msg) + + def handle_equipment_request(self, msg: SLMessage) -> SLMessage: + return SLMessage(msg.id, msg.code + 1, EQUIP_CONFIG) + class FakeUDPProtocolAdapter(asyncio.DatagramProtocol): def __init__(self, discovery_response: bytes) -> None: @@ -231,3 +249,52 @@ def decode_login(buff: bytes) -> tuple[bytes]: pid, offset = getSome("I", buff, offset) return schema, connType, clientType, passWord, pid + + +EQUIP_CONFIG = ( + b"\x0d\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\xa0\x00\x00" + b"\x01\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00" + b"\x00\x00\x00\x00\x41\x48\x00\x00\x18\x00\x00\x00\x03\x00\x00\x00" + b"\x05\x00\xff\xff\xff\x00\x00\x00\x00\x00\x05\x06\x07\x08\x09\x0a" + b"\x01\x02\x03\x04\x2c\x00\x00\x00\x00\x00\x00\x00\x05\x06\x07\x08" + b"\x09\x0a\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x01\x02\x03\x04\x03\x00\x00\x00\x05\x00\x46\x00\x02\x00\x00\x00" + b"\x10\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00" + b"\x00\x00\x00\x00\x0c\x00\x00\x00\x03\x04\x07\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x68\x01\x00\x00\x40\x00\x00\x04\x06\x3f\x09\x48" + b"\x01\x0d\x82\x4b\x0c\x48\x00\x1e\x00\x1e\x00\x1e\x00\xc4\x7a\x7a" + b"\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00" + b"\x00\x00\x00\x00\x00\x40\x0f\x02\x01\x02\x0a\x00\x1e\x00\x1e\x00" + b"\x1e\x00\x1e\x00\x1e\x00\x1e\x00\x1e\x00\x8c\x00\x00\x00\x00\x00" + b"\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x16\x00\x00\x00\x00\x3f\x00\x00\x00\x00\x20\x20\x20\x20\x20\x20" + b"\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x00\x00\x10\x00\x00\x00" + b"\x00\x00\x00\x00\x3e\x00\x32\x01\x01\x90\x0d\x7a\x0f\x82\x00\x00" +) diff --git a/tests/conftest.py b/tests/conftest.py index a79e24c..4e81fd6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ ) -DEFAULT_RESPONSE = "slpy091_pool-52-build-7360-rel_easytouch2-8_98360.json" +DEFAULT_RESPONSE = "slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json" def load_response_collection(filenames: list[str] | None = None): diff --git a/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json new file mode 100644 index 0000000..3fda797 --- /dev/null +++ b/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json @@ -0,0 +1,1983 @@ +{ + "decoded_complete": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel" + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [255, 255, 255] + }, + { + "name": "Light Green", + "value": [160, 255, 160] + }, + { + "name": "Green", + "value": [0, 255, 80] + }, + { + "name": "Cyan", + "value": [0, 255, 200] + }, + { + "name": "Blue", + "value": [100, 140, 255] + }, + { + "name": "Lavender", + "value": [230, 130, 255] + }, + { + "name": "Magenta", + "value": [255, 0, 128] + }, + { + "name": "Light Magenta", + "value": [255, 180, 210] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0, + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 98360, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM", + "HYBRID_HEATER" + ] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": ["Unknown", "Ready", "Sync", "Service"] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 60, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.67, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 799, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.01, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 6, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 7, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + }, + "date_time":{ + "timestamp": 1699835040.0, + "auto_dst":{ + "name": "Automatic Daylight Saving Time", + "value": 1 + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0, + "delay": 0 + }, + "function": 5, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 1 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0, + "delay": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7, + "value": 0 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8, + "value": 1 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0, + "delay": 0 + }, + "function": 0, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9, + "value": 0 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0, + "delay": 0 + }, + "function": 14, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11, + "value": 0 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12, + "value": 0 + } + }, + "pump": { + "0": { + "data": 70, + "type": 3, + "state": { + "name": "Pool Low Pump", + "value": 1 + }, + "watts_now": { + "name": "Pool Low Pump Watts Now", + "value": 1289, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Low Pump RPM Now", + "value": 2749, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Pool Low Pump GPM Now", + "value": 62, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "1": { + "data": 66, + "type": 3, + "state": { + "name": "Waterfall Pump", + "value": 0 + }, + "watts_now": { + "name": "Waterfall Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Waterfall Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Waterfall Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 95, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 60, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 3, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 7.67, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 799, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 6, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 7, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.01, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH", + "max_setpoint": 7.6, + "min_setpoint": 7.2 + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV", + "max_setpoint": 800, + "min_setpoint": 400 + }, + "calcium_hardness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm", + "max_setpoint": 201, + "min_setpoint": 0 + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm", + "max_setpoint": 6500, + "min_setpoint": 500 + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 30, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 4, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 52, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 149, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 1, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 51, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 1, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 1, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 0, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + }, + "version": { + "raw": { + "__type": "", + "repr": "b'\\x19\\x00\\x00\\x00POOL: 5.2 Build 736.0 Rel\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x0c\\x00\\x00\\x00'" + }, + "decoded": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel" + } + } + } + }, + "config": { + "raw": { + "__type": "", + "repr": "b'd\\x00\\x00\\x00(h(h\\x00\\r\\x00\\x008\\x80\\xff\\xff\\x0e\\x00\\x00\\x00Water Features\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x03\\x00\\x00\\x00Spa\\x00G\\x01\\x01\\x01\\x00\\x00\\x00\\x01\\xd0\\x02\\x00\\x00\\xf5\\x01\\x00\\x00\\t\\x00\\x00\\x00Waterfall\\x00\\x00\\x00U\\x00\\x02\\x00\\x00\\x00\\x00\\x02\\xd0\\x02\\x00\\x00\\xf6\\x01\\x00\\x00\\n\\x00\\x00\\x00Pool Light\\x00\\x00>\\x10\\x03\\x00\\x02\\x00\\x02\\x03\\xd0\\x02\\x00\\x00\\xf7\\x01\\x00\\x00\\t\\x00\\x00\\x00Spa Light\\x00\\x00\\x00I\\x10\\x03\\x00\\x06\\x01\\n\\x04\\xd0\\x02\\x00\\x00\\xf8\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cleaner\\x00\\x15\\x05\\x00\\x00\\x00\\x00\\x00\\x05\\xf0\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x08\\x00\\x00\\x00Pool Low?\\x02\\x00\\x01\\x00\\x00\\x00\\x06\\xd0\\x02\\x00\\x00\\xfa\\x01\\x00\\x00\\n\\x00\\x00\\x00Yard Light\\x00\\x00[\\x07\\x04\\x00\\x00\\x00\\x00\\x07\\xd0\\x02\\x00\\x00\\xfb\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cameras\\x00e\\x00\\x02\\x00\\x00\\x00\\x00\\x08T\\x06\\x00\\x00\\xfc\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x00\\x00\\x00\\x00\\x00\\t\\xd0\\x02\\x00\\x00\\xfe\\x01\\x00\\x00\\x08\\x00\\x00\\x00SpillwayN\\x0e\\x01\\x00\\x00\\x00\\x00\\x0b\\xd0\\x02\\x00\\x00\\xff\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x05\\x00\\x00\\x00\\x00\\x0c\\xd0\\x02\\x00\\x00\\x08\\x00\\x00\\x00\\x05\\x00\\x00\\x00White\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x0b\\x00\\x00\\x00Light Green\\x00\\xa0\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xa0\\x00\\x00\\x00\\x05\\x00\\x00\\x00Green\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00P\\x00\\x00\\x00\\x04\\x00\\x00\\x00Cyan\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xc8\\x00\\x00\\x00\\x04\\x00\\x00\\x00Blued\\x00\\x00\\x00\\x8c\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x08\\x00\\x00\\x00Lavender\\xe6\\x00\\x00\\x00\\x82\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x07\\x00\\x00\\x00Magenta\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\r\\x00\\x00\\x00Light Magenta\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xb4\\x00\\x00\\x00\\xd2\\x00\\x00\\x00FB\\x00\\x00\\x00\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [255, 255, 255] + }, + { + "name": "Light Green", + "value": [160, 255, 160] + }, + { + "name": "Green", + "value": [0, 255, 80] + }, + { + "name": "Cyan", + "value": [0, 255, 200] + }, + { + "name": "Blue", + "value": [100, 140, 255] + }, + { + "name": "Lavender", + "value": [230, 130, 255] + }, + { + "name": "Magenta", + "value": [255, 0, 128] + }, + { + "name": "Light Magenta", + "value": [255, 180, 210] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 98360, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM", + "HYBRID_HEATER" + ] + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0 + }, + "function": 5, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0 + }, + "function": 0, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0 + }, + "function": 14, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12 + } + }, + "pump": { + "0": { + "data": 70 + }, + "1": { + "data": 66 + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + } + } + }, + "status": { + "raw": { + "__type": "", + "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00<\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00E\\x00\\x00\\x00\\x00\\x00\\x00\\x00S\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00T\\x00\\x00\\x00\\x00\\x00\\x00\\x00_\\x00\\x00\\x00<\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x02\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01\\n\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfb\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfc\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfe\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x02\\x00\\x00\\x1f\\x03\\x00\\x00\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x06\\x00\\x00\\x00\\x07\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": ["Unknown", "Ready", "Sync", "Service"] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 60, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.67, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 799, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.01, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 6, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 7, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + }, + "configuration": { + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 95, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 60, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 3, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "501": { + "circuit_id": 501, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "502": { + "circuit_id": 502, + "value": 0, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "configuration": { + "delay": 0 + } + }, + "503": { + "circuit_id": 503, + "value": 0, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "configuration": { + "delay": 0 + } + }, + "504": { + "circuit_id": 504, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "505": { + "circuit_id": 505, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "506": { + "circuit_id": 506, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "507": { + "circuit_id": 507, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "508": { + "circuit_id": 508, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "510": { + "circuit_id": 510, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "511": { + "circuit_id": 511, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + } + } + } + }, + "pumps": [ + { + "raw": { + "__type": "", + "repr": "b'\\x03\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\t\\x05\\x00\\x00\\xbd\\n\\x00\\x00\\x00\\x00\\x00\\x00>\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x06\\x00\\x00\\x00?\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00z\\r\\x00\\x00\\x01\\x00\\x00\\x00\\x82\\x00\\x00\\x00K\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "0": { + "type": 3, + "state": { + "name": "Default Pump", + "value": 1 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 1289, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 2749, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 62, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + } + } + } + }, + { + "raw": { + "__type": "", + "repr": "b'\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x8c\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "1": { + "type": 3, + "state": { + "name": "Default Pump", + "value": 0 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + } + } + } + } + ], + "chemistry": { + "raw": { + "__type": "", + "repr": "b'*\\x00\\x00\\x00\\x00\\x02\\xff\\x03\\x1f\\x02\\xf8\\x02\\xd0\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x04\\x004\\x00\\x08\\x06\\x07\\xff\\x03 \\x00-\\x00-\\x14\\x00E\\x00\\x00\\x95 <\\x01\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 7.67, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 799, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 6, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 7, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.01, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 69, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH", + "max_setpoint": 7.6, + "min_setpoint": 7.2 + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV", + "max_setpoint": 800, + "min_setpoint": 400 + }, + "calcium_hardness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm", + "max_setpoint": 201, + "min_setpoint": 0 + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm", + "max_setpoint": 6500, + "min_setpoint": 500 + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 30, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 4, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 52, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 149, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 1, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + } + }, + "alarm": { + "flags": 0, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + } + } + }, + "scg": { + "raw": { + "__type": "", + "repr": "b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x003\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 51, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 1, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 1, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 0, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0 + } + } + }, + "color": null, + "date_time": { + "raw": { + "__type": "", + "repr": "b'\\xe7\\x07\\x0b\\x00\\x06\\x00\\x0c\\x00\\x10\\x00\\x18\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded":{ + "timestamp":1699835040.0, + "auto_dst":{ + "name": "Automatic Daylight Saving Time", + "value": 1 + } + } + } +} diff --git a/tests/test_cli.py b/tests/test_cli.py index c904b29..67ded51 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -295,3 +295,37 @@ async def test_set_chemistry( ): assert await cli(arguments.split()) == return_code assert capsys.readouterr().out.strip() == expected_output + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "arguments, return_code, expected_output", + [ + ("get date-time", 0, "2023-11-12 16:24:00"), + ( + "-v get dt -f %I:%M%p", + 0, + EXPECTED_VERBOSE_PREAMBLE + "04:24PM", + ), + ("get auto-dst", 0, "1"), + ("get dst", 0, "1"), + ( + "set date-time --date-time 2023-11-12T16:24:00", + 0, + "Controller time now: 2023-11-12 16:24:00", + ), + ( + "set date-time", + 0, + "Controller time now: 2023-11-12 16:24:00", + ), + ], + ) + async def test_date_time( + self, + capsys: pytest.CaptureFixture, + arguments: str, + return_code: int, + expected_output: str, + ): + assert await cli(arguments.split()) == return_code + assert capsys.readouterr().out.strip() == expected_output diff --git a/tests/test_data.py b/tests/test_data.py index 9967c75..f14ad1b 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -9,6 +9,7 @@ ) from screenlogicpy.requests.gateway import decode_version from screenlogicpy.requests.config import decode_pool_config +from screenlogicpy.requests.datetime import decode_date_time from screenlogicpy.requests.status import decode_pool_status from screenlogicpy.requests.pump import decode_pump_status from screenlogicpy.requests.chemistry import decode_chemistry @@ -24,6 +25,7 @@ def test_validate_complete(response_collection: ScreenLogicResponseCollection): decode_pump_status(pump.raw, data, idx) decode_chemistry(response_collection.chemistry.raw, data) decode_scg_config(response_collection.scg.raw, data) + decode_date_time(response_collection.date_time.raw, data) assert data == response_collection.decoded_complete From 2f9b7c874b3938ac8ee1f4a6cf8a89ef14b250a0 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Thu, 16 Nov 2023 00:49:57 -0800 Subject: [PATCH 17/53] Use Enums for SCG status/state. Make super_chlorinate full entry --- screenlogicpy/device_const/scg.py | 19 ++++++++++++++++++- screenlogicpy/requests/scg.py | 9 +++++++-- ...-52-build-7360-rel_easytouch2-8_98360.json | 12 ++++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/screenlogicpy/device_const/scg.py b/screenlogicpy/device_const/scg.py index 41d1ea2..247f6e0 100644 --- a/screenlogicpy/device_const/scg.py +++ b/screenlogicpy/device_const/scg.py @@ -1,5 +1,22 @@ from .system import BODY_TYPE -from ..const.common import SLValueRange +from ..const.common import SLIntEnum, SLValueRange + + +class STATUS_FLAG(SLIntEnum): + SCG_ACTIVE = 0x01 + + # Assumptions + SALT_VERY_LOW = 0x10 + SALT_LOW = 0x20 + SALT_OK = 0x40 + SALT_HIGH = 0x80 + + # Maybe + ICHEM_IN_CONTROL = 0x8000 + + +class STATE_FLAG(SLIntEnum): + SUPER_CHLORINATE = 0x01 class SCG_RANGE: diff --git a/screenlogicpy/requests/scg.py b/screenlogicpy/requests/scg.py index 7c2b8d4..87b8a62 100644 --- a/screenlogicpy/requests/scg.py +++ b/screenlogicpy/requests/scg.py @@ -3,7 +3,7 @@ from ..const.common import STATE_TYPE, UNIT, ON_OFF, ScreenLogicResponseError from ..const.msg import CODE, COM_MAX_RETRIES from ..const.data import ATTR, DEVICE, GROUP, VALUE -from ..device_const.scg import SCG_RANGE +from ..device_const.scg import SCG_RANGE, STATE_FLAG, STATUS_FLAG from ..device_const.system import BODY_TYPE from .protocol import ScreenLogicProtocol from .request import async_make_request @@ -30,7 +30,7 @@ def decode_scg_config(buff: bytes, data: dict) -> None: state, offset = getSome("I", buff, offset) # 4 scg_sensor[VALUE.STATE] = { ATTR.NAME: "Chlorinator", - ATTR.VALUE: ON_OFF.from_bool(state & 0x01).value, + ATTR.VALUE: ON_OFF.from_bool(state & STATUS_FLAG.SCG_ACTIVE).value, } scg_config: dict = scg.setdefault(GROUP.CONFIGURATION, {}) @@ -68,6 +68,11 @@ def decode_scg_config(buff: bytes, data: dict) -> None: flags, offset = getSome("I", buff, offset) # 20 scg[VALUE.FLAGS] = flags + scg[VALUE.SUPER_CHLORINATE] = { + ATTR.NAME: "Super Chlorinate", + ATTR.VALUE: ON_OFF.from_bool(flags & STATE_FLAG.SUPER_CHLORINATE).value, + } + superChlorTimer, offset = getSome("I", buff, offset) # 24 scg_config[VALUE.SUPER_CHLOR_TIMER] = { ATTR.NAME: "Super Chlorination Timer", diff --git a/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json index 3fda797..6765f3c 100644 --- a/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json +++ b/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json @@ -895,7 +895,11 @@ "step": 1 } }, - "flags": 0 + "flags": 0, + "super_chlorinate": { + "name": "Super Chlorinate", + "value": 0 + } } }, "version": { @@ -1962,7 +1966,11 @@ "step": 1 } }, - "flags": 0 + "flags": 0, + "super_chlorinate": { + "name": "Super Chlorinate", + "value": 0 + } } } }, From 5bc426b0479b961722b945ef586c71d152e73f33 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 17 Nov 2023 01:12:19 -0800 Subject: [PATCH 18/53] Exception docstrings --- screenlogicpy/const/common.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/screenlogicpy/const/common.py b/screenlogicpy/const/common.py index 459f902..4a92084 100644 --- a/screenlogicpy/const/common.py +++ b/screenlogicpy/const/common.py @@ -32,22 +32,32 @@ class ScreenLogicWarning(ScreenLogicException): class ScreenLogicError(ScreenLogicException): + """General error.""" + pass class ScreenLogicRequestError(ScreenLogicException): + """Protocol adapter indicated an unknown or malformed request.""" + pass class ScreenLogicConnectionError(ScreenLogicException): + """Connection to the protocol adapter was lost.""" + pass class ScreenLogicResponseError(ScreenLogicException): + """Protocol adapter returned an unexpected response.""" + pass class ScreenLogicLoginError(ScreenLogicException): + """The login was explicitly rejected.""" + pass From a4316fb8e22832bb317f222ab33110a8c35cd8d5 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 17 Nov 2023 01:12:35 -0800 Subject: [PATCH 19/53] Simplify request exception handling --- screenlogicpy/gateway.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/screenlogicpy/gateway.py b/screenlogicpy/gateway.py index eac686a..6013176 100644 --- a/screenlogicpy/gateway.py +++ b/screenlogicpy/gateway.py @@ -8,10 +8,9 @@ from .const.common import ( DATA_REQUEST, ON_OFF, + ScreenLogicException, ScreenLogicError, ScreenLogicConnectionError, - ScreenLogicRequestError, - ScreenLogicResponseError, ) from .const.msg import COM_MAX_RETRIES from .device_const.chemistry import CHEM_RANGE as cr @@ -546,18 +545,11 @@ async def attempt_request(): try: return await attempt_request() - except ( - ScreenLogicConnectionError, - ScreenLogicRequestError, - ScreenLogicResponseError, - ) as sle: + except ScreenLogicException as sle: _LOGGER.debug("%s. Attempting to reconnect", sle.msg) await self.async_disconnect(True) await asyncio.sleep(reconnect_delay) - try: - return await attempt_request() - except ScreenLogicRequestError as re2: - raise ScreenLogicError(re2.msg) from re2 + return await attempt_request() def _common_connection_closed_callback(self): """Perform any needed cleanup.""" From e938de1d0e9acba5f17347668a28c7315e3143ec Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sat, 18 Nov 2023 01:59:56 -0800 Subject: [PATCH 20/53] Raise on unexpected response --- screenlogicpy/requests/client.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/screenlogicpy/requests/client.py b/screenlogicpy/requests/client.py index 3a00672..8eaa14b 100644 --- a/screenlogicpy/requests/client.py +++ b/screenlogicpy/requests/client.py @@ -1,6 +1,6 @@ import struct -from ..const.common import CLIENT_ID +from ..const.common import CLIENT_ID, ScreenLogicResponseError from ..const.msg import CODE, COM_MAX_RETRIES from .protocol import ScreenLogicProtocol from .request import async_make_request @@ -11,15 +11,17 @@ async def async_request_add_client( clientID: int = CLIENT_ID, max_retries: int = COM_MAX_RETRIES, ) -> bool: - return ( - await async_make_request( + if ( + response := await async_make_request( protocol, CODE.ADD_CLIENT_QUERY, struct.pack(" bool: - return ( - await async_make_request( + if ( + response := await async_make_request( protocol, CODE.REMOVE_CLIENT_QUERY, struct.pack(" Date: Sat, 18 Nov 2023 02:00:49 -0800 Subject: [PATCH 21/53] Refactor to handle ScreenLogicExceptions --- screenlogicpy/client.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/screenlogicpy/client.py b/screenlogicpy/client.py index cb44fa6..b159c23 100644 --- a/screenlogicpy/client.py +++ b/screenlogicpy/client.py @@ -4,7 +4,7 @@ import random from typing import Any, Awaitable, Callable -from .const.common import COM_KEEPALIVE, ScreenLogicRequestError +from .const.common import COM_KEEPALIVE, ScreenLogicException from .const.msg import ( CODE, COM_MAX_RETRIES, @@ -131,8 +131,7 @@ async def async_subscribe( if self.client_needed: _LOGGER.debug("Client needed.") - if not await self.async_subscribe_gateway(): - return None + await self.async_subscribe_gateway() def remove_listener(): """Remove listener callback.""" @@ -163,25 +162,23 @@ async def _async_ping(self): try: if await async_request_ping(self._protocol, max_retries=0): _LOGGER.debug("Ping successful.") - except ScreenLogicRequestError as re: - _LOGGER.warning(f"Failed to receive response to ping: {re.msg}") + except ScreenLogicException as sle: + _LOGGER.warning(f"Failed to receive response to ping: {sle.msg}") async def _async_add_client(self): """Send a managed add client request.""" _LOGGER.debug("Requesting add client") - return await self._async_managed_request( - async_request_add_client, self._client_id - ) + await self._async_managed_request(async_request_add_client, self._client_id) async def _async_remove_client(self): """Send a unmanaged remove client request.""" _LOGGER.debug("Requesting remove client") try: - return await async_request_remove_client( + await async_request_remove_client( self._protocol, self._client_id, max_retries=0 ) - except ScreenLogicRequestError: - return False + except ScreenLogicException: + pass async def async_subscribe_gateway(self) -> bool: """ @@ -194,14 +191,12 @@ async def async_subscribe_gateway(self) -> bool: async with self._client_sub_unsub_lock: if not self.is_client: _LOGGER.debug("Subscribing gateway.") - if await self._async_add_client(): - self._is_client = True - self._protocol.enable_keepalive(self._async_ping, COM_KEEPALIVE) - _LOGGER.debug( - f"Gateway subscribed with client id: {self._client_id}" - ) - return True - return False + await self._async_add_client() + self._is_client = True + self._protocol.enable_keepalive(self._async_ping, COM_KEEPALIVE) + _LOGGER.debug( + f"Gateway subscribed with client id: {self._client_id}" + ) return True async def async_unsubscribe_gateway(self) -> bool: @@ -218,5 +213,5 @@ async def async_unsubscribe_gateway(self) -> bool: self._protocol.disable_keepalive() self._protocol.remove_all_async_message_callbacks() _LOGGER.debug(f"Gateway unsubscribing client id: {self._client_id}") - return await self._async_remove_client() + await self._async_remove_client() return True From a3ff92f1333bb996caadb4252283c7091ffce5ab Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 19 Nov 2023 00:21:29 -0800 Subject: [PATCH 22/53] Un-mock protocol --- tests/conftest.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4e81fd6..c03c6f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -86,6 +86,7 @@ async def MockDiscoveryAdapter( async def stub_async_connect( + loop: asyncio.BaseEventLoop, data: dict, self: ScreenLogicGateway, ip: str = None, @@ -106,18 +107,21 @@ async def stub_async_connect( self._name = name self._custom_connection_closed_callback = connection_closed_callback self._mac = FAKE_GATEWAY_MAC - self._protocol = MagicMock(spec=ScreenLogicProtocol, _connected=True) + self._protocol = ScreenLogicProtocol(loop) + self._protocol.connection_made(MagicMock(spec=asyncio.Transport)) self._data = data return True @pytest_asyncio.fixture() -async def MockConnectedGateway(response_collection: ScreenLogicResponseCollection): +async def MockConnectedGateway( + event_loop, response_collection: ScreenLogicResponseCollection +): with patch.multiple( ScreenLogicGateway, async_connect=lambda *args, **kwargs: stub_async_connect( - response_collection.decoded_complete, *args, **kwargs + event_loop, response_collection.decoded_complete, *args, **kwargs ), async_disconnect=DEFAULT, # get_debug=lambda self: {}, From e4ec1bd4fe0b76875895422582622dcb9e583857 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 19 Nov 2023 00:22:10 -0800 Subject: [PATCH 23/53] Pass event_loop to patched gateway --- tests/test_cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 67ded51..814e158 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -19,11 +19,13 @@ @pytest_asyncio.fixture() -async def PatchedGateway(response_collection: ScreenLogicResponseCollection): +async def PatchedGateway( + event_loop, response_collection: ScreenLogicResponseCollection +): with patch.multiple( ScreenLogicGateway, async_connect=lambda *args, **kwargs: stub_async_connect( - response_collection.decoded_complete, *args, **kwargs + event_loop, response_collection.decoded_complete, *args, **kwargs ), async_disconnect=DEFAULT, _async_connected_request=DEFAULT, From f7b84da2c3011f20afdd6106b7c0a075d9cc7aed Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 19 Nov 2023 00:22:55 -0800 Subject: [PATCH 24/53] Tests for register_async_message_handler --- tests/test_gateway.py | 34 ++++++++++++++++------------------ tests/test_protocol.py | 14 +++++++++++++- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 3ef2cf2..6bce7bc 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -20,7 +20,6 @@ async def test_mock_gateway( MockConnectedGateway, response_collection: ScreenLogicResponseCollection ): - gateway = MockConnectedGateway assert gateway.ip == FAKE_GATEWAY_ADDRESS @@ -67,7 +66,6 @@ async def test_gateway_async_connect_and_disconnect( "screenlogicpy.gateway.ClientManager", spec=ClientManager, ) as client_manager: - gateway = ScreenLogicGateway() await gateway.async_connect(**FAKE_CONNECT_INFO) @@ -93,14 +91,12 @@ async def test_gateway_get_status( MockConnectedGateway: ScreenLogicGateway, response_collection: ScreenLogicResponseCollection, ): - gateway = MockConnectedGateway with patch( "screenlogicpy.requests.status.async_make_request", return_value=response_collection.status.raw, ) as mock_request: - await gateway.async_get_status() mock_request.assert_awaited_once_with( @@ -114,7 +110,6 @@ async def test_gateway_get_pumps( MockConnectedGateway: ScreenLogicGateway, response_collection: ScreenLogicResponseCollection, ): - gateway = MockConnectedGateway with patch( @@ -124,7 +119,6 @@ async def test_gateway_get_pumps( response_collection.pumps[1].raw, ], ) as mock_request: - await gateway.async_get_pumps() mock_request.assert_has_awaits( @@ -142,14 +136,12 @@ async def test_gateway_get_chemistry( MockConnectedGateway: ScreenLogicGateway, response_collection: ScreenLogicResponseCollection, ): - gateway = MockConnectedGateway with patch( "screenlogicpy.requests.chemistry.async_make_request", return_value=response_collection.chemistry.raw, ) as mock_request: - await gateway.async_get_chemistry() mock_request.assert_awaited_once_with( @@ -163,14 +155,12 @@ async def test_gateway_get_scg( MockConnectedGateway: ScreenLogicGateway, response_collection: ScreenLogicResponseCollection, ): - gateway = MockConnectedGateway with patch( "screenlogicpy.requests.scg.async_make_request", return_value=response_collection.scg.raw, ) as mock_request: - await gateway.async_get_scg() mock_request.assert_awaited_once_with( @@ -313,7 +303,6 @@ async def test_gateway_async_set_circuit(MockConnectedGateway: ScreenLogicGatewa "screenlogicpy.requests.button.async_make_request", return_value=b"", ) as mockRequest: - await gateway.async_set_circuit(502, 1) mockRequest.assert_awaited_once_with( @@ -334,7 +323,6 @@ async def test_gateway_async_set_heat(MockConnectedGateway: ScreenLogicGateway): "screenlogicpy.requests.heat.async_make_request", return_value=b"", ) as mockRequest: - await gateway.async_set_heat_temp(0, 84) await gateway.async_set_heat_mode(1, 3) @@ -358,14 +346,12 @@ async def test_gateway_async_set_heat(MockConnectedGateway: ScreenLogicGateway): @pytest.mark.asyncio async def test_gateway_async_set_color_lights(MockConnectedGateway: ScreenLogicGateway): - gateway = MockConnectedGateway with patch( "screenlogicpy.requests.lights.async_make_request", return_value=b"", ) as mockRequest: - await gateway.async_set_color_lights(7) mockRequest.assert_awaited_once_with( @@ -378,14 +364,12 @@ async def test_gateway_async_set_color_lights(MockConnectedGateway: ScreenLogicG @pytest.mark.asyncio async def test_gateway_async_set_scg_config(MockConnectedGateway: ScreenLogicGateway): - gateway = MockConnectedGateway with patch( "screenlogicpy.requests.scg.async_make_request", return_value=b"", ) as mockRequest: - await gateway.async_set_scg_config(pool_setpoint=50, spa_setpoint=0) mockRequest.assert_awaited_once_with( @@ -398,14 +382,12 @@ async def test_gateway_async_set_scg_config(MockConnectedGateway: ScreenLogicGat @pytest.mark.asyncio async def test_gateway_async_set_chem_data(MockConnectedGateway: ScreenLogicGateway): - gateway = MockConnectedGateway with patch( "screenlogicpy.requests.chemistry.async_make_request", return_value=b"", ) as mockRequest: - await gateway.async_set_chem_data( ph_setpoint=7.5, orp_setpoint=700, @@ -421,3 +403,19 @@ async def test_gateway_async_set_chem_data(MockConnectedGateway: ScreenLogicGate b"\x00\x00\x00\x00\xEE\x02\x00\x00\xBC\x02\x00\x00\x2C\x01\x00\x00\x50\x00\x00\x00\x2D\x00\x00\x00\xE8\x03\x00\x00", 1, ) + + +@pytest.mark.asyncio +async def test_gateway_register_async_message_handler( + MockConnectedGateway: ScreenLogicGateway, +): + gateway = MockConnectedGateway + + async def callback(): + pass + + MSG_CODE = 16 + gateway.register_async_message_handler(MSG_CODE, callback, 1) + + assert MSG_CODE in gateway._protocol._callbacks + assert gateway._protocol._callbacks[MSG_CODE] == (callback, (1,)) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 3197347..dd0caa6 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -7,7 +7,7 @@ @pytest.mark.asyncio -async def test_async_data_received(event_loop): +async def test_async_data_received(event_loop: asyncio.AbstractEventLoop): CODE = 1196 MESSAGE = b"success" @@ -18,6 +18,18 @@ async def test_async_data_received(event_loop): protocol.data_received(payload) assert fut.result() == (0, CODE, MESSAGE) + async_callback_called = event_loop.create_future() + CODE2 = 1296 + + async def async_callback(data): + async_callback_called.set_result(data) + + protocol.register_async_message_callback(CODE2, async_callback) + + payload2 = makeMessage(1, CODE2, MESSAGE) + protocol.data_received(payload2) + await async_callback_called + assert async_callback_called.result() == MESSAGE @pytest.mark.asyncio From fddf9c70c93c8cc9b45f1eb7888f7cdd9754bb71 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 19 Nov 2023 17:38:59 -0800 Subject: [PATCH 25/53] Add deconstruct_response_collection --- screenlogicpy/data.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/screenlogicpy/data.py b/screenlogicpy/data.py index abde356..bee3a6f 100644 --- a/screenlogicpy/data.py +++ b/screenlogicpy/data.py @@ -63,6 +63,25 @@ def build_response_collection(raw: dict, data: dict) -> ScreenLogicResponseColle return ScreenLogicResponseCollection(data, **SLResponseColArgs) +def deconstruct_response_collection( + collection: ScreenLogicResponseCollection, +) -> tuple[dict, dict]: + data = collection.decoded_complete + raw = {} + for dr, data_request in asdict(collection).items(): + if dr == "decoded_complete": + continue + if isinstance(data_request, list): + data_dict = raw.setdefault(dr, {}) + for idx, val in enumerate(data_request): + data_dict[idx] = val["raw"] + + else: + raw[dr] = data_request["raw"] if data_request is not None else None + + return raw, data + + T_KEY = "__type" From 283bc9029b0d9fb97395476e069fef17782d8d16 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 19 Nov 2023 17:39:21 -0800 Subject: [PATCH 26/53] Cleanup unused func --- screenlogicpy/requests/utility.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/screenlogicpy/requests/utility.py b/screenlogicpy/requests/utility.py index 589fefb..5a8be87 100644 --- a/screenlogicpy/requests/utility.py +++ b/screenlogicpy/requests/utility.py @@ -103,26 +103,6 @@ def getSome(format, buff, offset) -> tuple[Any, int]: return struct.unpack_from(fmt, buff, offset)[0], newoffset -def getValueAt(buff, offset, want, **kwargs): - fmt = want if want.startswith(">") else "<" + want - val = kwargs.get("adjustment", lambda x: x)( - struct.unpack_from(fmt, buff, offset)[0] - ) - if name := kwargs.get("name"): - data = { - "name": name, - "value": val, - } - if unit := kwargs.get("unit"): - data["unit"] = unit - if device_type := kwargs.get("device_type"): - data["device_type"] = device_type - else: - data = val - newoffset = offset + struct.calcsize(fmt) - return data, newoffset - - def getString(buff, offset) -> tuple[str, int]: fmtLen = " Date: Sun, 19 Nov 2023 18:02:02 -0800 Subject: [PATCH 27/53] Load raw responses into _last for MockConnectedGateway --- tests/conftest.py | 13 +++++++++---- tests/test_cli.py | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c03c6f4..5eb4dbe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import asyncio from collections.abc import Callable +from dataclasses import asdict import os import pytest_asyncio import socket @@ -7,7 +8,11 @@ from unittest.mock import DEFAULT, MagicMock, patch from screenlogicpy import ScreenLogicGateway -from screenlogicpy.data import ScreenLogicResponseCollection, import_response_collection +from screenlogicpy.data import ( + ScreenLogicResponseCollection, + deconstruct_response_collection, + import_response_collection, +) from screenlogicpy.discovery import DISCOVERY_PORT from screenlogicpy.requests.protocol import ScreenLogicProtocol @@ -87,7 +92,7 @@ async def MockDiscoveryAdapter( async def stub_async_connect( loop: asyncio.BaseEventLoop, - data: dict, + resp_col: ScreenLogicResponseCollection, self: ScreenLogicGateway, ip: str = None, port: int = None, @@ -109,7 +114,7 @@ async def stub_async_connect( self._mac = FAKE_GATEWAY_MAC self._protocol = ScreenLogicProtocol(loop) self._protocol.connection_made(MagicMock(spec=asyncio.Transport)) - self._data = data + self._last, self._data = deconstruct_response_collection(resp_col) return True @@ -121,7 +126,7 @@ async def MockConnectedGateway( with patch.multiple( ScreenLogicGateway, async_connect=lambda *args, **kwargs: stub_async_connect( - event_loop, response_collection.decoded_complete, *args, **kwargs + event_loop, response_collection, *args, **kwargs ), async_disconnect=DEFAULT, # get_debug=lambda self: {}, diff --git a/tests/test_cli.py b/tests/test_cli.py index 814e158..9af967e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,7 @@ import json import pytest import pytest_asyncio -from unittest.mock import DEFAULT, patch +from unittest.mock import DEFAULT, MagicMock, mock_open, patch from screenlogicpy import ScreenLogicGateway from screenlogicpy.cli import cli @@ -25,7 +25,7 @@ async def PatchedGateway( with patch.multiple( ScreenLogicGateway, async_connect=lambda *args, **kwargs: stub_async_connect( - event_loop, response_collection.decoded_complete, *args, **kwargs + event_loop, response_collection, *args, **kwargs ), async_disconnect=DEFAULT, _async_connected_request=DEFAULT, @@ -331,3 +331,38 @@ async def test_date_time( ): assert await cli(arguments.split()) == return_code assert capsys.readouterr().out.strip() == expected_output + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "arguments, return_code, expected_output", + [ + ("export", 0, ""), + ], + ) + async def test_export_data_collection( + self, + capsys: pytest.CaptureFixture, + arguments: str, + return_code: int, + expected_output: str, + ): + written: str = "" + + def write(data): + nonlocal written + written += data + + mo: MagicMock = mock_open() + handle = mo() + handle.write.side_effect = write + with patch("screenlogicpy.data.open", mo), patch( + "screenlogicpy.cli.ScreenLogicGateway.async_update" + ): + assert await cli(arguments.split()) == return_code + mo.assert_called_with( + "slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json", + "w", + encoding="utf-8", + ) + + assert written From 40b7aeb4213c8613a9f4c3a24ba2506267958364 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sun, 19 Nov 2023 18:30:39 -0800 Subject: [PATCH 28/53] Test datetime encode and decode --- tests/test_utility.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/test_utility.py diff --git a/tests/test_utility.py b/tests/test_utility.py new file mode 100644 index 0000000..a17ae95 --- /dev/null +++ b/tests/test_utility.py @@ -0,0 +1,12 @@ +import asyncio +from datetime import datetime +import pytest + +from screenlogicpy.requests.utility import decodeMessageTime, encodeMessageTime + + +def test_encode_decode_time(): + time = datetime(2023, 11, 19, 18, 15, 36, 175759) + data = encodeMessageTime(time) + assert data == b"\xe7\x07\x0b\x00\x06\x00\x13\x00\x12\x00\x0f\x00\x00\x00\xaf\x00" + assert decodeMessageTime(data) == datetime(2023, 11, 19, 18, 15, 0, 175000) From 47a91ba916ec0998aeb401b08f2db6e693869d15 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:53:42 -0800 Subject: [PATCH 29/53] New data set for adapter v738 --- ...-52-build-7380-rel_easytouch2-8_32824.json | 2103 +++++++++++++++++ 1 file changed, 2103 insertions(+) create mode 100644 tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json diff --git a/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json b/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json new file mode 100644 index 0000000..b5e082f --- /dev/null +++ b/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json @@ -0,0 +1,2103 @@ +{ + "decoded_complete": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 738.0 Rel" + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0, + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 32824, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM" + ] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 64, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.54, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 725, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.28, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 1, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 2, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 1, + "device_type": "alarm" + } + }, + "date_time": { + "timestamp": 1700517969.0, + "auto_dst": { + "name": "Automatic Daylight Saving Time", + "value": 1 + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0, + "delay": 0 + }, + "function": 5, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 1 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0, + "delay": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7, + "value": 0 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8, + "value": 1 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9, + "value": 0 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0, + "delay": 0 + }, + "function": 14, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11, + "value": 0 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12, + "value": 0 + } + }, + "pump": { + "0": { + "data": 70, + "type": 3, + "state": { + "name": "Pool Low Pump", + "value": 1 + }, + "watts_now": { + "name": "Pool Low Pump Watts Now", + "value": 1246, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Low Pump RPM Now", + "value": 2710, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Pool Low Pump GPM Now", + "value": 63, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "1": { + "data": 66, + "type": 3, + "state": { + "name": "Waterfall Pump", + "value": 0 + }, + "watts_now": { + "name": "Waterfall Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Waterfall Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Waterfall Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 59, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 59, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 95, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 64, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 3, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 7.54, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 725, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 1, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 2, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.28, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 59, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH", + "min_setpoint": 7.2, + "max_setpoint": 7.6 + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV", + "min_setpoint": 400, + "max_setpoint": 800 + }, + "calcium_hardness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm", + "min_setpoint": 0, + "max_setpoint": 201 + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm", + "min_setpoint": 500, + "max_setpoint": 6500 + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 7, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 18, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 12, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 39, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 165, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 32, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 1, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 1, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 1, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 6, + "unit": "hr", + "min_setpoint": 0, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0, + "super_chlorinate": { + "name": "Super Chlorinate", + "value": 0 + } + } + }, + "version": { + "raw": { + "__type": "", + "repr": "b'\\x19\\x00\\x00\\x00POOL: 5.2 Build 738.0 Rel\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x0c\\x00\\x00\\x00'" + }, + "decoded": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 738.0 Rel" + } + } + } + }, + "config": { + "raw": { + "__type": "", + "repr": "b'd\\x00\\x00\\x00(h(h\\x00\\r\\x00\\x008\\x80\\x00\\x00\\x0e\\x00\\x00\\x00Water Features\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x03\\x00\\x00\\x00Spa\\x00G\\x01\\x01\\x01\\x00\\x00\\x00\\x01\\xd0\\x02\\x00\\x00\\xf5\\x01\\x00\\x00\\t\\x00\\x00\\x00Waterfall\\x00\\x00\\x00U\\x00\\x02\\x00\\x00\\x00\\x00\\x02\\xd0\\x02\\x00\\x00\\xf6\\x01\\x00\\x00\\n\\x00\\x00\\x00Pool Light\\x00\\x00>\\x10\\x03\\x00\\x02\\x00\\x02\\x03\\xd0\\x02\\x00\\x00\\xf7\\x01\\x00\\x00\\t\\x00\\x00\\x00Spa Light\\x00\\x00\\x00I\\x10\\x03\\x00\\x06\\x01\\n\\x04\\xd0\\x02\\x00\\x00\\xf8\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cleaner\\x00\\x15\\x05\\x02\\x00\\x00\\x00\\x00\\x05\\xf0\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x08\\x00\\x00\\x00Pool Low?\\x02\\x00\\x01\\x00\\x00\\x00\\x06\\xd0\\x02\\x00\\x00\\xfa\\x01\\x00\\x00\\n\\x00\\x00\\x00Yard Light\\x00\\x00[\\x07\\x04\\x00\\x00\\x00\\x00\\x07\\xd0\\x02\\x00\\x00\\xfb\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cameras\\x00e\\x00\\x02\\x00\\x00\\x00\\x00\\x08T\\x06\\x00\\x00\\xfc\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x02\\x00\\x00\\x00\\x00\\t\\xd0\\x02\\x00\\x00\\xfe\\x01\\x00\\x00\\x08\\x00\\x00\\x00SpillwayN\\x0e\\x02\\x00\\x00\\x00\\x00\\x0b\\xd0\\x02\\x00\\x00\\xff\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x05\\x00\\x00\\x00\\x00\\x0c\\xd0\\x02\\x00\\x00\\x08\\x00\\x00\\x00\\x05\\x00\\x00\\x00White\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x0b\\x00\\x00\\x00Light Green\\x00\\xa0\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xa0\\x00\\x00\\x00\\x05\\x00\\x00\\x00Green\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00P\\x00\\x00\\x00\\x04\\x00\\x00\\x00Cyan\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xc8\\x00\\x00\\x00\\x04\\x00\\x00\\x00Blued\\x00\\x00\\x00\\x8c\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x08\\x00\\x00\\x00Lavender\\xe6\\x00\\x00\\x00\\x82\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x07\\x00\\x00\\x00Magenta\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\r\\x00\\x00\\x00Light Magenta\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xb4\\x00\\x00\\x00\\xd2\\x00\\x00\\x00FB\\x00\\x00\\x00\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [ + 255, + 255, + 255 + ] + }, + { + "name": "Light Green", + "value": [ + 160, + 255, + 160 + ] + }, + { + "name": "Green", + "value": [ + 0, + 255, + 80 + ] + }, + { + "name": "Cyan", + "value": [ + 0, + 255, + 200 + ] + }, + { + "name": "Blue", + "value": [ + 100, + 140, + 255 + ] + }, + { + "name": "Lavender", + "value": [ + 230, + 130, + 255 + ] + }, + { + "name": "Magenta", + "value": [ + 255, + 0, + 128 + ] + }, + { + "name": "Light Magenta", + "value": [ + 255, + 180, + 210 + ] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 32824, + "list": [ + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM" + ] + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0 + }, + "function": 5, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0 + }, + "function": 14, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12 + } + }, + "pump": { + "0": { + "data": 70 + }, + "1": { + "data": 66 + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + } + } + }, + "status": { + "raw": { + "__type": "", + "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00@\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00;\\x00\\x00\\x00\\x00\\x00\\x00\\x00S\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00;\\x00\\x00\\x00\\x00\\x00\\x00\\x00_\\x00\\x00\\x00@\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x02\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01\\n\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfb\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfc\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfe\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf2\\x02\\x00\\x00\\xd5\\x02\\x00\\x00\\xe4\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": [ + "Unknown", + "Ready", + "Sync", + "Service" + ] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 64, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.54, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 725, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.28, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 1, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 2, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 1, + "device_type": "alarm" + } + }, + "configuration": { + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 59, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 59, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Heater", + "Both" + ] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 95, + "unit": "°F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 64, + "unit": "°F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 3, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "501": { + "circuit_id": 501, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "502": { + "circuit_id": 502, + "value": 0, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "configuration": { + "delay": 0 + } + }, + "503": { + "circuit_id": 503, + "value": 0, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "configuration": { + "delay": 0 + } + }, + "504": { + "circuit_id": 504, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "505": { + "circuit_id": 505, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "506": { + "circuit_id": 506, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "507": { + "circuit_id": 507, + "value": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "508": { + "circuit_id": 508, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "510": { + "circuit_id": 510, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "511": { + "circuit_id": 511, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + } + } + } + }, + "pumps": [ + { + "raw": { + "__type": "", + "repr": "b'\\x03\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\xde\\x04\\x00\\x00\\x96\\n\\x00\\x00\\x00\\x00\\x00\\x00?\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x06\\x00\\x00\\x00?\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00z\\r\\x00\\x00\\x01\\x00\\x00\\x00\\x82\\x00\\x00\\x00K\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "0": { + "type": 3, + "state": { + "name": "Default Pump", + "value": 1 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 1246, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 2710, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 63, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + } + } + } + }, + { + "raw": { + "__type": "", + "repr": "b'\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x8c\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + }, + "decoded": { + "pump": { + "1": { + "type": 3, + "state": { + "name": "Default Pump", + "value": 0 + }, + "watts_now": { + "name": "Default Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Default Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Default Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + } + } + } + } + ], + "chemistry": { + "raw": { + "__type": "", + "repr": "b\"*\\x00\\x00\\x00\\x00\\x02\\xf2\\x02\\xd5\\x02\\xf8\\x02\\xd0\\x00\\x00\\x00\\x07\\x00\\x00\\x00\\x12\\x00\\x0c\\x00'\\x01\\x02\\xe4\\x03 \\x00-\\x00-\\x14\\x00; \\x00\\xa5 <\\x01\\x00\\x00\\x00\\x00\\x00\"" + }, + "decoded": { + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 7.54, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 725, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 1, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 2, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": -0.28, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 59, + "unit": "°F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH", + "min_setpoint": 7.2, + "max_setpoint": 7.6 + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV", + "min_setpoint": 400, + "max_setpoint": 800 + }, + "calcium_hardness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm", + "min_setpoint": 0, + "max_setpoint": 201 + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm", + "min_setpoint": 500, + "max_setpoint": 6500 + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 7, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 18, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 12, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 39, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 165, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": [ + "Dosing", + "Mixing", + "Monitoring" + ] + } + }, + "alarm": { + "flags": 32, + "flow_alarm": { + "name": "Flow Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 1, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060" + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + } + } + }, + "scg": { + "raw": { + "__type": "", + "repr": "b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x00\\x00\\x00'" + }, + "decoded": { + "scg": { + "scg_present": 0, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 1, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 1, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 6, + "unit": "hr", + "min_setpoint": 0, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0, + "super_chlorinate": { + "name": "Super Chlorinate", + "value": 0 + } + } + } + }, + "color": null, + "date_time": { + "raw": { + "__type": "", + "repr": "b'\\xe7\\x07\\x0b\\x00\\x00\\x00\\x14\\x00\\x0e\\x00\\x06\\x00\\t\\x00\\x00\\x00\\x01\\x00\\x00\\x00'" + }, + "decoded": { + "controller": { + "date_time": { + "timestamp": 1700517969.0, + "auto_dst": { + "name": "Automatic Daylight Saving Time", + "value": 1 + } + } + } + } + } +} \ No newline at end of file From 78a331ef0a3c00794bb7e4692bba079fec3a612b Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:54:10 -0800 Subject: [PATCH 30/53] Use ATTR consts for keys --- screenlogicpy/requests/datetime.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/screenlogicpy/requests/datetime.py b/screenlogicpy/requests/datetime.py index a76510a..226cabd 100644 --- a/screenlogicpy/requests/datetime.py +++ b/screenlogicpy/requests/datetime.py @@ -4,7 +4,7 @@ import struct from ..const.common import ScreenLogicResponseError -from ..const.data import DEVICE, GROUP, VALUE +from ..const.data import ATTR, DEVICE, GROUP, VALUE from ..const.msg import CODE from .protocol import ScreenLogicProtocol from .request import async_make_request @@ -30,8 +30,8 @@ def decode_date_time(buffer: bytes, data: dict): auto_dst, offset = getSome("I", buffer, offset) date_time[VALUE.AUTO_DST] = { - "name": "Automatic Daylight Saving Time", - "value": auto_dst, + ATTR.NAME: "Automatic Daylight Saving Time", + ATTR.VALUE: auto_dst, } From a2d4ac229a7acfd429ab18b9397ce8167d68b6ce Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:54:51 -0800 Subject: [PATCH 31/53] Include date_time in async_update --- screenlogicpy/gateway.py | 1 + 1 file changed, 1 insertion(+) diff --git a/screenlogicpy/gateway.py b/screenlogicpy/gateway.py index 6013176..5e86fc7 100644 --- a/screenlogicpy/gateway.py +++ b/screenlogicpy/gateway.py @@ -181,6 +181,7 @@ async def async_update(self) -> None: await self.async_get_pumps() await self.async_get_chemistry() await self.async_get_scg() + await self.async_get_datetime() _LOGGER.debug("Update complete") async def async_get_config(self): From fa70acf5e17f8b3c0dc4f6a61a3a68f25583ac15 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:55:21 -0800 Subject: [PATCH 32/53] Reference upgraded adapter data_set in tests --- tests/conftest.py | 2 +- tests/const_data.py | 6 +++--- tests/test_cli.py | 24 ++++++++++++------------ tests/test_gateway.py | 15 +++++++-------- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5eb4dbe..b32e58b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,7 +29,7 @@ ) -DEFAULT_RESPONSE = "slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json" +DEFAULT_RESPONSE = "slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json" def load_response_collection(filenames: list[str] | None = None): diff --git a/tests/const_data.py b/tests/const_data.py index e56866e..56b7d87 100644 --- a/tests/const_data.py +++ b/tests/const_data.py @@ -51,12 +51,12 @@ EXPECTED_DASHBOARD = """Discovered 'Fake: 00-00-00' at 127.0.0.1:6448 EasyTouch2 8 ************************** -Pool temperature is last 69°F +Pool temperature is last 59°F Pool Heat Set Point: 83°F Pool Heat: Off Pool Heat Mode: Off -------------------------- -Spa temperature is last 84°F +Spa temperature is last 59°F Spa Heat Set Point: 95°F Spa Heat: Off Spa Heat Mode: Heater @@ -78,5 +78,5 @@ EXPECTED_VERBOSE_PREAMBLE = """Discovered 'Fake: 00-00-00' at 127.0.0.1:6448 EasyTouch2 8 -Version: POOL: 5.2 Build 736.0 Rel +Version: POOL: 5.2 Build 738.0 Rel """ diff --git a/tests/test_cli.py b/tests/test_cli.py index 9af967e..7bb0b3e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -115,8 +115,8 @@ async def test_circuit( @pytest.mark.parametrize( "arguments, return_code, expected_output", [ - ("get current-temp 0", 0, "69"), - ("get t spa", 0, "84"), + ("get current-temp 0", 0, "59"), + ("get t spa", 0, "59"), ], ) async def test_get_current_temp( @@ -243,19 +243,19 @@ async def test_set_color_lights( @pytest.mark.parametrize( "arguments, return_code, expected_output", [ - ("set salt-generator --pool 100 --spa 20", 0, "51 0"), + ("set salt-generator --pool 100 --spa 20", 0, "0 0"), ( "-v set scg -p 20 -s 0", 0, EXPECTED_VERBOSE_PREAMBLE - + "Pool Chlorinator Setpoint: 51 Spa Chlorinator Setpoint: 0", + + "Pool Chlorinator Setpoint: 0 Spa Chlorinator Setpoint: 0", ), - ("set scg -p 50", 0, "51"), + ("set scg -p 50", 0, "0"), ("set scg -s 20", 0, "0"), ("set scg", 65, "No new chlorinator values. Nothing to do."), - ("set super-chlorinate --state 1 --time 24", 0, "0"), + ("set super-chlorinate --state 1 --time 24", 0, "6"), ("set sc -s 0", 0, ""), - ("set sc -t 12", 0, "0"), + ("set sc -t 12", 0, "6"), ], ) async def test_set_scg( @@ -302,23 +302,23 @@ async def test_set_chemistry( @pytest.mark.parametrize( "arguments, return_code, expected_output", [ - ("get date-time", 0, "2023-11-12 16:24:00"), + ("get date-time", 0, "2023-11-20 14:06:09"), ( "-v get dt -f %I:%M%p", 0, - EXPECTED_VERBOSE_PREAMBLE + "04:24PM", + EXPECTED_VERBOSE_PREAMBLE + "02:06PM", ), ("get auto-dst", 0, "1"), ("get dst", 0, "1"), ( "set date-time --date-time 2023-11-12T16:24:00", 0, - "Controller time now: 2023-11-12 16:24:00", + "Controller time now: 2023-11-20 14:06:09", ), ( "set date-time", 0, - "Controller time now: 2023-11-12 16:24:00", + "Controller time now: 2023-11-20 14:06:09", ), ], ) @@ -360,7 +360,7 @@ def write(data): ): assert await cli(arguments.split()) == return_code mo.assert_called_with( - "slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json", + "slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json", "w", encoding="utf-8", ) diff --git a/tests/test_gateway.py b/tests/test_gateway.py index 6bce7bc..b85547d 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -75,9 +75,9 @@ async def test_gateway_async_connect_and_disconnect( assert gateway.port == FAKE_GATEWAY_PORT assert gateway.name == FAKE_GATEWAY_NAME assert gateway.mac == FAKE_GATEWAY_MAC - assert gateway.version == "POOL: 5.2 Build 736.0 Rel" + assert gateway.version == "POOL: 5.2 Build 738.0 Rel" assert gateway.controller_model == "EasyTouch2 8" - assert gateway.equipment_flags == 98360 + assert gateway.equipment_flags == 32824 assert gateway.temperature_unit == "°F" await gateway.async_disconnect() @@ -176,7 +176,7 @@ async def test_gateway_get_scg( ("controller", "sensor", "air_temperature"), { "name": "Air Temperature", - "value": 60, + "value": 64, "unit": "°F", "device_type": "temperature", "state_type": "measurement", @@ -185,13 +185,12 @@ async def test_gateway_get_scg( ( ("controller", "equipment"), { - "flags": 98360, + "flags": 32824, "list": [ "INTELLIBRITE", "INTELLIFLO_0", "INTELLIFLO_1", "INTELLICHEM", - "HYBRID_HEATER", ], }, ), @@ -200,7 +199,7 @@ async def test_gateway_get_scg( { "firmware": { "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 736.0 Rel", + "value": "POOL: 5.2 Build 738.0 Rel", } }, ), @@ -227,7 +226,7 @@ def test_gateway_get_data(MockConnectedGateway: ScreenLogicGateway, path, expect ), ( ("controller", "sensor", "air_temperature"), - 60, + 64, ), ( ("circuit", 505), @@ -375,7 +374,7 @@ async def test_gateway_async_set_scg_config(MockConnectedGateway: ScreenLogicGat mockRequest.assert_awaited_once_with( gateway._protocol, 12576, - b"\x00\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + b"\x00\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00", 1, ) From 0ddce1d2a953cee7b00e04420d551392d910b41e Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:49:29 -0800 Subject: [PATCH 33/53] Add ScreenLogicCommunicationError --- screenlogicpy/__init__.py | 2 +- screenlogicpy/client.py | 6 +++--- screenlogicpy/const/common.py | 14 ++++++++++---- screenlogicpy/gateway.py | 4 ++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/screenlogicpy/__init__.py b/screenlogicpy/__init__.py index bce60b5..6da6f51 100644 --- a/screenlogicpy/__init__.py +++ b/screenlogicpy/__init__.py @@ -1,5 +1,5 @@ __version__ = "0.10.0" # flake8: noqa F401 from screenlogicpy.gateway import ScreenLogicGateway -from screenlogicpy.const.common import ScreenLogicError, ScreenLogicRequestError +from screenlogicpy.const.common import ScreenLogicError, ScreenLogicCommunicationError from screenlogicpy.discovery import async_discover diff --git a/screenlogicpy/client.py b/screenlogicpy/client.py index b159c23..ccc0ed3 100644 --- a/screenlogicpy/client.py +++ b/screenlogicpy/client.py @@ -4,7 +4,7 @@ import random from typing import Any, Awaitable, Callable -from .const.common import COM_KEEPALIVE, ScreenLogicException +from .const.common import COM_KEEPALIVE, ScreenLogicCommunicationError from .const.msg import ( CODE, COM_MAX_RETRIES, @@ -162,7 +162,7 @@ async def _async_ping(self): try: if await async_request_ping(self._protocol, max_retries=0): _LOGGER.debug("Ping successful.") - except ScreenLogicException as sle: + except ScreenLogicCommunicationError as sle: _LOGGER.warning(f"Failed to receive response to ping: {sle.msg}") async def _async_add_client(self): @@ -177,7 +177,7 @@ async def _async_remove_client(self): await async_request_remove_client( self._protocol, self._client_id, max_retries=0 ) - except ScreenLogicException: + except ScreenLogicCommunicationError: pass async def async_subscribe_gateway(self) -> bool: diff --git a/screenlogicpy/const/common.py b/screenlogicpy/const/common.py index 4a92084..12d732c 100644 --- a/screenlogicpy/const/common.py +++ b/screenlogicpy/const/common.py @@ -37,25 +37,31 @@ class ScreenLogicError(ScreenLogicException): pass -class ScreenLogicRequestError(ScreenLogicException): +class ScreenLogicCommunicationError(ScreenLogicException): + """Base class for all communication errors.""" + + pass + + +class ScreenLogicRequestError(ScreenLogicCommunicationError): """Protocol adapter indicated an unknown or malformed request.""" pass -class ScreenLogicConnectionError(ScreenLogicException): +class ScreenLogicConnectionError(ScreenLogicCommunicationError): """Connection to the protocol adapter was lost.""" pass -class ScreenLogicResponseError(ScreenLogicException): +class ScreenLogicResponseError(ScreenLogicCommunicationError): """Protocol adapter returned an unexpected response.""" pass -class ScreenLogicLoginError(ScreenLogicException): +class ScreenLogicLoginError(ScreenLogicCommunicationError): """The login was explicitly rejected.""" pass diff --git a/screenlogicpy/gateway.py b/screenlogicpy/gateway.py index 5e86fc7..68e1c9b 100644 --- a/screenlogicpy/gateway.py +++ b/screenlogicpy/gateway.py @@ -8,7 +8,7 @@ from .const.common import ( DATA_REQUEST, ON_OFF, - ScreenLogicException, + ScreenLogicCommunicationError, ScreenLogicError, ScreenLogicConnectionError, ) @@ -546,7 +546,7 @@ async def attempt_request(): try: return await attempt_request() - except ScreenLogicException as sle: + except ScreenLogicCommunicationError as sle: _LOGGER.debug("%s. Attempting to reconnect", sle.msg) await self.async_disconnect(True) await asyncio.sleep(reconnect_delay) From 9f06ee510accceda67ffc833c5c1bb03491d3fca Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:56:26 -0800 Subject: [PATCH 34/53] Add help for export cli arg --- screenlogicpy/cli.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/screenlogicpy/cli.py b/screenlogicpy/cli.py index 5ce4ea4..39a56ef 100644 --- a/screenlogicpy/cli.py +++ b/screenlogicpy/cli.py @@ -343,7 +343,10 @@ async def async_get_json(): ) # pylint: disable=unused-variable - export_parser = subparsers.add_parser("export") # noqa F841 + export_parser = subparsers.add_parser( + "export", + help="Exports complete response collection to slpy[libversion]\_[adapter-firmware]\_[controller-model]\_[equipment-flags].json", + ) # noqa F841 # Get options get_parser = subparsers.add_parser("get", help="Gets the specified value or state") @@ -562,7 +565,7 @@ async def async_get_json(): set_chem_data_parser = set_subparsers.add_parser( "chemistry-value", aliases=["cv"], - help="Set various chemistry values for LSI calculation in the IntelliChem system", + help="Set various user-supplied chemistry values for LSI calculation in the IntelliChem system", ) set_chem_data_parser.add_argument( "-ch", From 6031c0af323e7189b4fbf3d0368bbda8143065a2 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:56:49 -0800 Subject: [PATCH 35/53] New features for v0.10.0 --- README.md | 184 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 135 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index d7aff71..8125b54 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ $ pip install screenlogicpy * _Changed in v0.5.0: The screenlogicpy library has moved over to using asyncio for all network I/O. Relevant methods now require the `async`/`await` syntax._ * _New in v0.8.0: Support for Python 3.8 and 3.9 is being phased out across future releases. Version 0.8.x will be the last versions to support Python 3.8._ * _New in v0.9.0: Support for Python 3.8 has been removed. Support for Python 3.9 is being phased out across future releases. Version 0.9.x will be the last versions to support Python 3.9._ -* _**New in v0.10.0**: Support for Python 3.9 has been removed._ +* _**New in v0.10.0**: Support for Python 3.9 has been deprecated. Additionally, support for Python 3.10 is being phased out across future releases. Versions 0.10.x will be the last versions to support Python 3.10._ The `ScreenLogicGateway` class is the primary interface. @@ -38,11 +38,12 @@ Once instantiated, use `async_connect()` to connect and login to the ScreenLogic If disconnected, this method may be called without any parameters to reconnect with the previous connection info, or with new parameters to connect to a different host. ```python -success = await gateway.async_connect("192.168.x.x") +await gateway.async_connect("192.168.x.x") ``` * _New in v0.5.0._ * _Changed in v0.7.0: `async_connect()` now accepts adapter connection info. This supports handling ip changes to the protocol adapter._ +* _**Changed in v0.10.0**: `async_connect()` no longer returns a `bool` indicating success. If the action is unsuccessful, an exception indicating the failure mode is raised._ ## Polling the pool state @@ -58,10 +59,12 @@ This update consists of sending requests for: 2. Detailed information for _each_ configured pump 3. Detailed pool chemistry information 4. Status and settings for any configured salt chlorine generators +5. The controller's current date and time, and auto DST settings **Warning:** This method is not rate-limited. The calling application is responsible for maintaining reasonable intervals between updates. The ScreenLogic protocol adapter may respond with an error message if too many requests are made too quickly. * _Changed in v0.5.0: This method is now an async coroutine and no longer disconnects from the protocol adapter after polling the data._ +* _**Changed in v0.10.0**: Now includes polling for information regarding the controller's date and time settings._ ## Subscribing to pool state updates @@ -117,6 +120,9 @@ await gateway.async_get_chemistry() # Updates the state of any configured salt chlorine generators await gateway.async_get_scg() + +# Updates the current pool controller time and Daylight Saving Time adjustments setting +await gateway.async_get_datetime() ``` Push subscriptions and polling of all or specific data can be used on their own or at the same time. @@ -228,76 +234,96 @@ The following actions can be performed with methods on the `ScreenLogicGateway` * Set a heating mode for a specific body of water (spa/pool) * Set a target heating temperature for a specific body of water (spa/pool) * Select various color-enabled lighting options -* Set the chlorinator output levels +* Set chlorinator output levels and super-chlorination values * Setting IntelliChem chemistry values +* Setting the pool controller's system date and time -Each method will `return True` if the operation reported no exceptions. +If the action is unsuccessful, an exception will be raised indicating the failure mode. **Note:** The methods do not confirm the requested action is now in effect on the pool controller. +* _**Changed in v0.10.0**: Actions no longer return a `bool` indicating success. If the action is unsuccessful, an exception is raised indicating the failure mode._ + ## Turning a circuit ON or OFF -A circuit can be requested to be turned on or off with the `async_set_circuit()` method. `async_set_circuit` takes two required arguments, `circuitID` which is the id number of the circuit as an `int`, and `circuitState` which represents the desired new state of the circuit, as an `int`. See [Circuit State](#circuit-state) below. +A circuit can be requested to be turned on or off with the `async_set_circuit()` method. `async_set_circuit` takes two required arguments, `circuitID` which is the id number of the circuit as an `int`, and `circuitState` which represents the desired new state of the circuit, as an `int`. See [State](#state) below. ```python -success = await gateway.async_set_circuit(circuitID, circuitState) +await gateway.async_set_circuit(circuitID, circuitState) ``` * _Changed in v0.5.0: This method is now an async coroutine._ +* _**Changed in v0.10.0**: No longer returns a `bool` indicating success. If the action is unsuccessful, an exception is raised indicating the failure mode._ ## Setting a heating mode The desired heating mode can be set per body of water (pool or spa) with `async_set_heat_mode()`. `async_set_heat_mode` takes two required arguments, `body` as an `int` representing the [body of water](#body), and `mode` as an `int` of the desired [heating mode](#heat-modes). ```python -success = await gateway.async_set_heat_mode(body, mode) +await gateway.async_set_heat_mode(body, mode) ``` * _Changed in v0.5.0: This method is now an async coroutine._ +* _**Changed in v0.10.0**: No longer returns a `bool` indicating success. If the action is unsuccessful, an exception is raised indicating the failure mode._ ## Setting a target temperature The target heating temperature can be set per body of water (pool or spa) with `async_set_heat_temp()`. `async_set_heat_temp` takes two required arguments, `body` as an `int` representing the [body of water](#body), and `temp` as an `int` of the desired target temperature. ```python -success = await gateway.async_set_heat_temp(body, temp) +await gateway.async_set_heat_temp(body, temp) ``` -_Changed in v0.5.0: This method is now an async coroutine._ +* _Changed in v0.5.0: This method is now an async coroutine._ +* _**Changed in v0.10.0**: No longer returns a `bool` indicating success. If the action is unsuccessful, an exception is raised indicating the failure mode._ ## Setting light colors or shows Colors or color-shows can be set for compatible color-enable lighting with `async_set_color_lights()`. `async_set_color_lights` takes one required argument, `light_command` as an `int` representing the desired [command/show/color](#color-modes) ```python -success = await gateway.async_set_color_lights(light_command) +await gateway.async_set_color_lights(light_command) ``` * _Changed in v0.5.0: This method is now an async coroutine._ +* _**Changed in v0.10.0**: No longer returns a `bool` indicating success. If the action is unsuccessful, an exception is raised indicating the failure mode._ -## Setting chlorinator output levels - -### ScreenLogicGateway.**async_set_scg_config**(_pool_setpoint=None, spa_setpoint=None_ ) +## Setting chlorinator output levels, super chlorination -Chlorinator output levels can be set with `async_set_scg_config()`. `async_set_scg_config` takes up to two keyword arguments, `pool_setpoint` and `spa_setpoint`, both `int`. +Chlorinator output levels can be set with `async_set_scg_config()`. `async_set_scg_config` takes up to four keyword arguments, `pool_setpoint`, `spa_setpoint`, `super_chlorinate`, and `super_chlor_timer`. All are `int` with `super_chlorinate` representing an ON or OFF state. See [State](#state) below. ```python -success = await gateway.async_set_scg_config(pool_setpoint=pool_output, spa_setpoint=spa_output) +await gateway.async_set_scg_config(pool_setpoint=pool_output, spa_setpoint=spa_output) ``` * _New in v0.5.0._ -* _**Changed in v0.10.0.** `async_set_scg_config` now accepts keyword arguments only. This allows the caller to specify only the value(s) they want to update, retaining all other existing values._ +* _**Changed in v0.10.0**:_ + * _Added `super_chlorinate`, and `super_chlor_timer` parameters to control super chlorination functions._ + * _Now accepts keyword arguments only. This allows the caller to specify only the value(s) they want to update, retaining all other existing values._ + * _No longer returns a `bool` indicating success. If the action is unsuccessful, an exception is raised indicating the failure mode._ -## Setting IntelliChem Chemistry values +## Setting IntelliChem chemistry values Chemistry values used in the IntelliChem system can be set with `async_set_chem_data()`. `async_set_chem_data` takes up to six keyword arguments, `ph_setpoint`, `orp_setpoint`, `calcium_hardness`, `total_alkalinity`, `cyanuric_acid`, and `salt_tds_ppm`. `ph_setpoint` is a `float` and the rest are `int`. ```python -success = await gateway.async_set_chem_data(ph_setpoint=ph, orp_setpoint=orp, calcium_hardness=cal, total_alkalinity=alk, cyanuric_acid=cya, salt_tds_ppm=salt) +await gateway.async_set_chem_data(ph_setpoint=ph, orp_setpoint=orp, calcium_hardness=cal, total_alkalinity=alk, cyanuric_acid=cya, salt_tds_ppm=salt) ``` * _New in v0.6.0._ -* _**Changed in v0.10.0:** `async_set_chem_data` now accepts keyword arguments only. This allows the caller to specify only the value(s) they want to update, retaining all other existing values._ +* _**Changed in v0.10.0**:_ + * _Added `calcium_hardness`, `total_alkalinity`, `cyanuric_acid`, and `salt_tds_ppm` parameters._ + * _Now accepts keyword arguments only. This allows the caller to specify only the value(s) they want to update, retaining all other existing values._ + * _No longer returns a `bool` indicating success. If the action is unsuccessful, an exception is raised indicating the failure mode._ +## Setting controller date and time + +The system date and time in the pool controller can be set with `async_set_date_time()`. Method takes up to two keyword arguments, `date_time` as a python `datetime` object and `auto_dst` as an `int` representing an ON or OFF state. See [State](#state) below. + +```python +await gateway.async_set_date_time(date_time=datetime.now(), auto_dst=set_dst) +``` + +* _**New in v0.10.0.**_ ## Handling unsolicited messages @@ -398,7 +424,7 @@ Spa Heat Mode: Heater ## Argument usage ```text -screenlogicpy [-h] [-v] [-i IP] [-p PORT] {discover,get,set} ... +screenlogicpy [-h] [-v] [-i IP] [-p PORT] {discover,export,get,set} ... ``` ## Optional arguments @@ -439,6 +465,14 @@ screenlogicpy discover Attempts to discover ScreenLogic protocol adapters on the local network via UDP broadcast. Returns `[ip address]:[port]` of each discovered ScreenLogic protocol adapter, one per line. **Note:** Discovery is limited to finding protocol adapters on the same subnet as the host running `screenlogicpy`. +### `export` + +```shell +screenlogicpy export +``` + +Exports a response collection saved as a JSON file that can be used for debugging/testing purposes. A response collection includes each controller response with both the original `bytes` data along with a decoded `dict` of that data, and all data merged into a complete `dict` as it would be in the `ScreenLogicGateway`. JSON file is named for "slpy[libversion]\_[adapter-firmware]\_[controller-model]\_[equipment-flags].json". + ### `get` ```shell @@ -450,7 +484,7 @@ The get option is use with additional options to return the current state of the #### get `circuit, c` ```shell -screenlogicpy get circuit [circuit number] +screenlogicpy get circuit CIRCUIT_NUM ``` Returns 1 for on and 0 for off @@ -458,38 +492,58 @@ Returns 1 for on and 0 for off #### get `heat-mode, hm` ```shell -screenlogicpy get heat-mode [body] +screenlogicpy get heat-mode BODY ``` Returns the current heating mode for the specified body of water. -**Note:** `[body]` can be an `int` or `string` representing the [body of water](#body). +**Note:** `BODY` can be an `int` or `string` representing the [body of water](#body). #### get `heat-temp, ht` ```shell -screenlogicpy get heat-temp [body] +screenlogicpy get heat-temp BODY ``` Returns the current target heating temperature for the specified body of water. -**Note:** `[body]` can be an `int` or `string` representing the [body of water](#body). +**Note:** `BODY` can be an `int` or `string` representing the [body of water](#body). #### get `heat-state, hs` ```shell -screenlogicpy get heat-state [body] +screenlogicpy get heat-state BODY ``` Returns the current state of the heater for the specified body of water. The current state will match the heat mode when heating is active, otherwise will be 0 (off). -**Note:** `[body]` can be an `int` or `string` representing the [body of water](#body). +**Note:** `BODY` can be an `int` or `string` representing the [body of water](#body). #### get `current-temp, t` ```shell -screenlogicpy get current-temp [body] +screenlogicpy get current-temp BODY ``` Returns the current temperature for the specified body of water. This is actually the last-known temperature from when that body of water was active (Pool or Spa) -**Note:** `[body]` can be an `int` or `string` representing the [body of water](#body). +**Note:** `BODY` can be an `int` or `string` representing the [body of water](#body). + +#### get `date-time, dt` + +```shell +screenlogicpy get date-time [--format FORMAT] +``` + +Returns the pool controller's internal date and time in ISO 8601 format. Use the optional argument to specify a format string. + +##### get date-time `--format, -f` + +Specify a format string for the returned `datetime` to be formatted with. + +#### get `auto-dst, dst` + +```shell +screenlogicpy get auto-dst +``` + +Returns the value of the pool controller's "Adjust for Daylight Savings Time" setting. #### get `json, j` @@ -502,7 +556,7 @@ Returns a json dump of all data cached in the data `dict`. ### `set` ```shell -screenlogicpy set {circuit,c,color-lights,cl,heat-mode,hm,heat-temp,ht} ... +screenlogicpy set {circuit,c,color-lights,cl,heat-mode,hm,heat-temp,ht,salt-generator,scg,super-chlorinate,sc,chemistry-setpoint,cs,chemistry-value,cv,date-time,dt} ... ``` All `set` commands work like their corresponding `get` commands, but take an additional argument or arguments for the desired setting. @@ -510,29 +564,29 @@ All `set` commands work like their corresponding `get` commands, but take an add #### set `circuit, c` ```shell -screenlogicpy set circuit [circuit number] [circuit state] +screenlogicpy set circuit CIRCUIT_NUM STATE ``` Sets the specified circuit to the specified circuit state. -**Note:** `[circuit state]` can be an `int` or `string` representing the desired [state](#state). +**Note:** `STATE` can be an `int` or `string` representing the desired [state](#state). #### set `heat-mode, hm` ```shell -screenlogicpy set heat-mode [body] [heat mode] +screenlogicpy set heat-mode BODY MODE ``` Sets the desired heating mode for the specified body of water. -**Note:** `[body]` can be an `int` or `string` representing the [body of water](#body). `[heat mode]` can be an `int` or `string` representing the desired [heat mode](#heat-modes) +**Note:** `BODY` can be an `int` or `string` representing the [body of water](#body). `MODE` can be an `int` or `string` representing the desired [heat mode](#heat-modes) #### set `heat-temp, ht` ```shell -screenlogicpy set heat-temp [body] [heat temp] +screenlogicpy set heat-temp BODY TEMP ``` Sets the desired target heating temperature for the specified body of water. -**Note:** `[body]` can be an `int` or `string` representing the [body of water](#body). `[heat temp]` is an `int` representing the desired target temperature. +**Note:** `BODY` can be an `int` or `string` representing the [body of water](#body). `TEMP` is an `int` representing the desired target temperature. #### set `color-lights, cl` @@ -548,7 +602,6 @@ Sets a color mode for all color-capable lights configured on the pool controller #### set `salt-generator, scg` ```shell - screenlogicpy set salt-generator [--pool POOL_PCT] [--spa SPA_PCT] ``` @@ -567,6 +620,24 @@ Specify the output level for the spa as an `int` between `0`-`100`. * _New in v0.5.0._ * _**Changed in v0.10.0:** Pool and spa arguments are now optional. Users may specify one or the other, or both._ +#### set `super-chlorinate, sc` + +```shell +screenlogicpy set super-chlorinate [--state ON_OFF] [--time HOURS] +``` + +Enables or disables super chlorination for the specified amount of time via two option arguments: + +##### set super-chlorinate `-s, --state` + +Specify a new state for super chlorination as an `int` or `string` representing the desired [state](#state). + +#### set super-chlorinate `-t, --time` + +Specify the number of hours to run super chlorination for as an `int` between `1`-`24`. + +* _**New in v0.10.0.**_ + #### set `chemistry-setpoint, cs` ```shell @@ -583,7 +654,6 @@ Specify the target pH value for Intellichem to maintain as a `float` between `7. Specify the target ORP value for Intellichem to maintain as an `int` between `400`-`800`. - **Note:** `chemistry-setpoint` replaces the previous chem-data. * _**New in v0.10.0.**_ @@ -596,24 +666,41 @@ screenlogicpy set chemistry-value [--calcium-hardness CALCIUM] [--total-alkalini Sets values used in the calculation of Saturation Index in the IntelliChem system via four optional arguments: -##### set chemistry-setpoint `-ch, --calcium-hardness` +##### set chemistry-value `-ch, --calcium-hardness` -Specify the calcium hardness value for Saturation Index calculation as an `int` between `X`-`X`. +Specify the calcium hardness value for Saturation Index calculation as an `int` between `25`-`800`. -##### set chemistry-setpoint `-ta, --total-alkalinity` +##### set chemistry-value `-ta, --total-alkalinity` -Specify the total alkalinity value for Saturation Index calculation as an `int` between `X`-`X`. +Specify the total alkalinity value for Saturation Index calculation as an `int` between `25`-`800`. -##### set chemistry-setpoint `-cya, --cyanuric-acid` +##### set chemistry-value `-cya, --cyanuric-acid` -Specify the cyanuric acid value for Saturation Index calculation as an `int` between `X`-`X`. +Specify the cyanuric acid value for Saturation Index calculation as an `int` between `0`-`201`. -##### set chemistry-setpoint `-tds, --total-dissolved-solids` +##### set chemistry-value `-tds, --total-dissolved-solids` -Specify the total dissolved solids value for Saturation Index calculation as an `int` between `X`-`X`. +Specify the total dissolved solids value for Saturation Index calculation as an `int` between `500`-`6500`. * _**New in v0.10.0.**_ +#### set `date-time, dt` + +```shell +screenlogicpy set date-time [--date-time ISO_8601] [--auto-dst ON_OFF] +``` + +Sets the date and time on the pool controller (used in schedules), and the "Adjust for DST" setting via optional arguments: + +##### set date-time `-dt, --date-time` + +Specify a date and time with an ISO 8601 formatted `string`. **Note:** `string` must not contain spaces. Use the 'T' character between the date and the time (`2023-11-12T16:24:00`). + +##### set date-time `-dst, --auto-dst` + +Enable or disable automatic adjustment of system time for Daylight Saving Time as an `int` or `string` representing the desired [state](#state). + +* _**New in v0.10.0.**_ # Reference @@ -641,7 +728,6 @@ Specify the total dissolved solids value for Saturation Index calculation as an | `3` | `heater` | Heater | Heating will use the heater to achieve the desired temperature set point. | | `4` | `dont_change` | Don't Change | Don't change the heating mode based on circuit or function changes. | - ## Color Modes | `int` | `string` | Name | Description | @@ -687,6 +773,6 @@ from screenlogicpy.const import CODE ## Acknowledgements -Inspired by https://github.com/keithpjolley/soipip +Inspired by [https://github.com/keithpjolley/soipip](https://github.com/keithpjolley/soipip) -The protocol and codes are documented fairly well here: https://github.com/ceisenach/screenlogic_over_ip +The protocol and codes are documented fairly well here: [https://github.com/ceisenach/screenlogic_over_ip](https://github.com/ceisenach/screenlogic_over_ip) From 637724b471254a35345b413f21cb942ab7c6a408 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:10:08 -0800 Subject: [PATCH 36/53] Correct maximum super chlorinate time --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8125b54..6deaf4a 100644 --- a/README.md +++ b/README.md @@ -634,7 +634,7 @@ Specify a new state for super chlorination as an `int` or `string` representing #### set super-chlorinate `-t, --time` -Specify the number of hours to run super chlorination for as an `int` between `1`-`24`. +Specify the number of hours to run super chlorination for as an `int` between `1`-`72`. * _**New in v0.10.0.**_ From f0d0181af1c46470ea5e28feaafea135efb49131 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 24 Nov 2023 03:23:08 -0800 Subject: [PATCH 37/53] Include host time in date_time data --- screenlogicpy/const/data.py | 1 + screenlogicpy/requests/datetime.py | 6 +++--- .../slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json | 2 ++ tests/test_data.py | 6 +++++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/screenlogicpy/const/data.py b/screenlogicpy/const/data.py index b601913..531ee40 100644 --- a/screenlogicpy/const/data.py +++ b/screenlogicpy/const/data.py @@ -126,6 +126,7 @@ class VALUE: SUPER_CHLOR_TIMER = "super_chlor_timer" SUPER_CHLORINATE = "super_chlorinate" TIMESTAMP = "timestamp" + TIMESTAMP_HOST = "timestamp_host" TOTAL_ALKALINITY = "total_alkalinity" TYPE = "type" WATTS_NOW = "watts_now" diff --git a/screenlogicpy/requests/datetime.py b/screenlogicpy/requests/datetime.py index 226cabd..0caa04f 100644 --- a/screenlogicpy/requests/datetime.py +++ b/screenlogicpy/requests/datetime.py @@ -1,6 +1,4 @@ -import datetime - - +from datetime import datetime import struct from ..const.common import ScreenLogicResponseError @@ -22,11 +20,13 @@ async def async_request_date_time( def decode_date_time(buffer: bytes, data: dict): + host_time = datetime.now() controller: dict = data.setdefault(DEVICE.CONTROLLER, {}) date_time: dict = controller.setdefault(GROUP.DATE_TIME, {}) dt, offset = getTime(buffer, 0) date_time[VALUE.TIMESTAMP] = dt.timestamp() + date_time[VALUE.TIMESTAMP_HOST] = host_time.timestamp() auto_dst, offset = getSome("I", buffer, offset) date_time[VALUE.AUTO_DST] = { diff --git a/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json b/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json index b5e082f..2b9a6eb 100644 --- a/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json +++ b/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json @@ -192,6 +192,7 @@ }, "date_time": { "timestamp": 1700517969.0, + "timestamp_host": 1700517812.0, "auto_dst": { "name": "Automatic Daylight Saving Time", "value": 1 @@ -2092,6 +2093,7 @@ "controller": { "date_time": { "timestamp": 1700517969.0, + "timestamp_host": 1700517812.0, "auto_dst": { "name": "Automatic Daylight Saving Time", "value": 1 diff --git a/tests/test_data.py b/tests/test_data.py index f14ad1b..f9268c4 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,3 +1,4 @@ +from datetime import datetime from unittest.mock import MagicMock, mock_open, patch from screenlogicpy.data import ( @@ -25,7 +26,10 @@ def test_validate_complete(response_collection: ScreenLogicResponseCollection): decode_pump_status(pump.raw, data, idx) decode_chemistry(response_collection.chemistry.raw, data) decode_scg_config(response_collection.scg.raw, data) - decode_date_time(response_collection.date_time.raw, data) + with patch("screenlogicpy.requests.datetime.datetime") as mock_datetime: + mock_datetime.now.return_value = datetime.fromtimestamp(1700517812.0) + mock_datetime.side_effect = lambda *a, **kwa: datetime(*a, **kwa) + decode_date_time(response_collection.date_time.raw, data) assert data == response_collection.decoded_complete From 1ea5d06970fc0881d434586b30cb2a26784faafb Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:35:00 -0800 Subject: [PATCH 38/53] Add "major" and "minor" attribute const --- screenlogicpy/const/data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/screenlogicpy/const/data.py b/screenlogicpy/const/data.py index 531ee40..e58fdf9 100644 --- a/screenlogicpy/const/data.py +++ b/screenlogicpy/const/data.py @@ -14,8 +14,10 @@ class ATTR: INTERFACE = "interface" IS_RPM = "is_rpm" LIMIT = "limit" + MAJOR = "major" MAX_SETPOINT = "max_setpoint" MIN_SETPOINT = "min_setpoint" + MINOR = "minor" NAME = "name" NAME_INDEX = "name_index" PROGRESS = "progress" From 15aa3346b81a19f8fdd8ef6a13835e6fd930956e Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:36:53 -0800 Subject: [PATCH 39/53] Remove old testing data --- tests/const_data.py | 23 - tests/data_sets.py | 4424 ------------------------------------------ tests/test_client.py | 9 +- 3 files changed, 4 insertions(+), 4452 deletions(-) delete mode 100644 tests/data_sets.py diff --git a/tests/const_data.py b/tests/const_data.py index 56b7d87..953c094 100644 --- a/tests/const_data.py +++ b/tests/const_data.py @@ -7,9 +7,6 @@ ) from screenlogicpy.const.msg import CODE -from .data_sets import TESTING_DATA_COLLECTION as TDC - - FAKE_GATEWAY_ADDRESS = "127.0.0.1" FAKE_GATEWAY_CHK = 2 FAKE_GATEWAY_DISCOVERY_PORT = 1444 @@ -27,26 +24,6 @@ SL_GATEWAY_NAME: FAKE_GATEWAY_NAME, } -ASYNC_SL_RESPONSES = { - CODE.VERSION_QUERY: TDC.version.raw, - CODE.CTRLCONFIG_QUERY: TDC.config.raw, - CODE.POOLSTATUS_QUERY: TDC.status.raw, - CODE.STATUS_CHANGED: TDC.status.raw, - CODE.PUMPSTATUS_QUERY: TDC.pumps[0].raw, - CODE.CHEMISTRY_QUERY: TDC.chemistry.raw, - CODE.CHEMISTRY_CHANGED: TDC.chemistry.raw, - CODE.SCGCONFIG_QUERY: TDC.scg.raw, - CODE.COLOR_UPDATE: TDC.color[7].raw, - CODE.BUTTONPRESS_QUERY: b"", - CODE.LIGHTCOMMAND_QUERY: b"", - CODE.SETHEATMODE_QUERY: b"", - CODE.SETHEATTEMP_QUERY: b"", - CODE.SETSCG_QUERY: b"", - CODE.SETCHEMDATA_QUERY: b"", - CODE.ADD_CLIENT_QUERY: b"", - CODE.REMOVE_CLIENT_QUERY: b"", - CODE.PING_QUERY: b"", -} EXPECTED_DASHBOARD = """Discovered 'Fake: 00-00-00' at 127.0.0.1:6448 EasyTouch2 8 diff --git a/tests/data_sets.py b/tests/data_sets.py deleted file mode 100644 index 2093180..0000000 --- a/tests/data_sets.py +++ /dev/null @@ -1,4424 +0,0 @@ -from dataclasses import dataclass -import struct - - -@dataclass(frozen=True) -class ScreenLogicResponseSet: - raw: bytes - decoded: dict - - -@dataclass(frozen=True) -class ScreenLogicResponseCollection: - decoded_complete: dict - version: ScreenLogicResponseSet = None - config: ScreenLogicResponseSet = None - status: ScreenLogicResponseSet = None - pumps: list[ScreenLogicResponseSet] = None - chemistry: ScreenLogicResponseSet = None - scg: ScreenLogicResponseSet = None - color: list[ScreenLogicResponseSet] = None - - -TEST_DATA_COLLECTIONS: list[ScreenLogicResponseCollection] = [ - ScreenLogicResponseCollection( - decoded_complete={ - "adapter": { - "firmware": { - "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 736.0 Rel", - } - }, - "controller": { - "controller_id": 100, - "configuration": { - "body_type": { - 0: {"min_setpoint": 40, "max_setpoint": 104}, - 1: {"min_setpoint": 40, "max_setpoint": 104}, - }, - "is_celsius": {"name": "Is Celsius", "value": 0}, - "controller_type": 13, - "hardware_type": 0, - "controller_data": 0, - "generic_circuit_name": "Water Features", - "circuit_count": 18, - "color_count": 8, - "color": [ - {"name": "White", "value": (255, 255, 255)}, - {"name": "Light Green", "value": (160, 255, 160)}, - {"name": "Green", "value": (0, 255, 80)}, - {"name": "Cyan", "value": (0, 255, 200)}, - {"name": "Blue", "value": (100, 140, 255)}, - {"name": "Lavender", "value": (230, 130, 255)}, - {"name": "Magenta", "value": (255, 0, 128)}, - {"name": "Light Magenta", "value": (255, 180, 210)}, - ], - "interface_tab_flags": 127, - "show_alarms": 0, - "remotes": 0, - "unknown_at_offset_09": 0, - "unknown_at_offset_10": 0, - "unknown_at_offset_11": 0, - }, - "model": {"name": "Model", "value": "EasyTouch2 8"}, - "equipment": { - "flags": 98360, - "list": [ - "INTELLIBRITE", - "INTELLIFLO_0", - "INTELLIFLO_1", - "INTELLICHEM", - "HYBRID_HEATER", - ], - }, - "sensor": { - "state": { - "name": "Controller State", - "value": 1, - "device_type": "enum", - "enum_options": ["Unknown", "Ready", "Sync", "Service"], - }, - "freeze_mode": {"name": "Freeze Mode", "value": 0}, - "pool_delay": {"name": "Pool Delay", "value": 0}, - "spa_delay": {"name": "Spa Delay", "value": 0}, - "cleaner_delay": {"name": "Cleaner Delay", "value": 0}, - "air_temperature": { - "name": "Air Temperature", - "value": 57, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "ph": { - "name": "pH", - "value": 7.48, - "unit": "pH", - "state_type": "measurement", - }, - "orp": { - "name": "ORP", - "value": 701, - "unit": "mV", - "state_type": "measurement", - }, - "saturation": { - "name": "Saturation Index", - "value": -0.13, - "unit": "lsi", - "state_type": "measurement", - }, - "salt_ppm": { - "name": "Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement", - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 3, - "state_type": "measurement", - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 4, - "state_type": "measurement", - }, - "active_alert": { - "name": "Active Alert", - "value": 1, - "device_type": "alarm", - }, - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "name": "Spa", - "configuration": { - "name_index": 71, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_62": 0, - "unknown_at_offset_63": 0, - "delay": 0, - }, - "function": 1, - "interface": 1, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 1, - "value": 0, - }, - 501: { - "circuit_id": 501, - "name": "Waterfall", - "configuration": { - "name_index": 85, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_94": 0, - "unknown_at_offset_95": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 2, - "value": 0, - }, - 502: { - "circuit_id": 502, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_126": 0, - "unknown_at_offset_127": 0, - "delay": 0, - }, - "function": 16, - "interface": 3, - "color": {"color_set": 2, "color_position": 0, "color_stagger": 10}, - "device_id": 3, - "value": 1, - }, - 503: { - "circuit_id": 503, - "name": "Spa Light", - "configuration": { - "name_index": 73, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_158": 0, - "unknown_at_offset_159": 0, - "delay": 0, - }, - "function": 16, - "interface": 3, - "color": {"color_set": 7, "color_position": 0, "color_stagger": 10}, - "device_id": 4, - "value": 1, - }, - 504: { - "circuit_id": 504, - "name": "Cleaner", - "configuration": { - "name_index": 21, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_186": 0, - "unknown_at_offset_187": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 5, - "value": 1, - }, - 505: { - "circuit_id": 505, - "name": "Pool Low", - "configuration": { - "name_index": 63, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_214": 0, - "unknown_at_offset_215": 0, - "delay": 0, - }, - "function": 2, - "interface": 0, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 6, - "value": 0, - }, - 506: { - "circuit_id": 506, - "name": "Yard Light", - "configuration": { - "name_index": 91, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_246": 0, - "unknown_at_offset_247": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 7, - "value": 1, - }, - 507: { - "circuit_id": 507, - "name": "Aux 6", - "configuration": { - "name_index": 7, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_274": 0, - "unknown_at_offset_275": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 8, - "value": 0, - }, - 508: { - "circuit_id": 508, - "name": "Pool High", - "configuration": { - "name_index": 61, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_306": 0, - "unknown_at_offset_307": 0, - "delay": 0, - }, - "function": 5, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 9, - "value": 0, - }, - 510: { - "circuit_id": 510, - "name": "Feature 1", - "configuration": { - "name_index": 93, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_338": 0, - "unknown_at_offset_339": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 11, - "value": 0, - }, - 511: { - "circuit_id": 511, - "name": "Feature 2", - "configuration": { - "name_index": 94, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_370": 0, - "unknown_at_offset_371": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 12, - "value": 0, - }, - 512: { - "circuit_id": 512, - "name": "Feature 3", - "configuration": { - "name_index": 95, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_402": 0, - "unknown_at_offset_403": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 13, - "value": 0, - }, - 513: { - "circuit_id": 513, - "name": "Feature 4", - "configuration": { - "name_index": 96, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_434": 0, - "unknown_at_offset_435": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 14, - "value": 0, - }, - 514: { - "circuit_id": 514, - "name": "Feature 5", - "configuration": { - "name_index": 97, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_466": 0, - "unknown_at_offset_467": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 15, - "value": 0, - }, - 515: { - "circuit_id": 515, - "name": "Feature 6", - "configuration": { - "name_index": 98, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_498": 0, - "unknown_at_offset_499": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 16, - "value": 0, - }, - 516: { - "circuit_id": 516, - "name": "Feature 7", - "configuration": { - "name_index": 99, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_530": 0, - "unknown_at_offset_531": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 17, - "value": 0, - }, - 517: { - "circuit_id": 517, - "name": "Feature 8", - "configuration": { - "name_index": 100, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_562": 0, - "unknown_at_offset_563": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 18, - "value": 0, - }, - 519: { - "circuit_id": 519, - "name": "AuxEx", - "configuration": { - "name_index": 92, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_590": 0, - "unknown_at_offset_591": 0, - "delay": 0, - }, - "function": 0, - "interface": 2, - "color": {"color_set": 0, "color_position": 0, "color_stagger": 0}, - "device_id": 20, - "value": 0, - }, - }, - "pump": { - 0: { - "data": 70, - "type": 3, - "state": {"name": "Pool Low Pump", "value": 0}, - "watts_now": { - "name": "Pool Low Pump Watts Now", - "value": 0, - "unit": "W", - "device_type": "power", - "state_type": "measurement", - }, - "rpm_now": { - "name": "Pool Low Pump RPM Now", - "value": 0, - "unit": "rpm", - "state_type": "measurement", - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Pool Low Pump GPM Now", - "value": 0, - "unit": "gpm", - "state_type": "measurement", - }, - "unknown_at_offset_24": 255, - "preset": { - 0: {"device_id": 6, "setpoint": 2500, "is_rpm": 1}, - 1: {"device_id": 9, "setpoint": 2800, "is_rpm": 1}, - 2: {"device_id": 1, "setpoint": 3450, "is_rpm": 1}, - 3: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - 4: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - 5: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - 6: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - 7: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - }, - }, - 1: {"data": 0}, - 2: {"data": 0}, - 3: {"data": 0}, - 4: {"data": 0}, - 5: {"data": 0}, - 6: {"data": 0}, - 7: {"data": 0}, - }, - "body": { - 0: { - "body_type": 0, - "min_setpoint": 40, - "max_setpoint": 104, - "name": "Pool", - "last_temperature": { - "name": "Last Pool Temperature", - "value": 56, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Pool Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Pool Heat Set Point", - "value": 86, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Pool Cool Set Point", - "value": 100, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Pool Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - 1: { - "body_type": 1, - "min_setpoint": 40, - "max_setpoint": 104, - "name": "Spa", - "last_temperature": { - "name": "Last Spa Temperature", - "value": 97, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Spa Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Spa Heat Set Point", - "value": 97, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Spa Cool Set Point", - "value": 57, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Spa Heat Mode", - "value": 3, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - }, - "intellichem": { - "unknown_at_offset_00": 42, - "unknown_at_offset_04": 0, - "sensor": { - "ph_now": { - "name": "pH Now", - "value": 0.0, - "unit": "pH", - "state_type": "measurement", - }, - "orp_now": { - "name": "ORP Now", - "value": 0, - "unit": "mV", - "state_type": "measurement", - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 3, - "state_type": "measurement", - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 4, - "state_type": "measurement", - }, - "saturation": { - "name": "Saturation Index", - "value": -0.13, - "unit": "lsi", - "state_type": "measurement", - }, - "ph_probe_water_temp": { - "name": "pH Probe Water Temperature", - "value": 56, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - }, - "configuration": { - "ph_setpoint": { - "name": "pH Setpoint", - "value": 7.5, - "unit": "pH", - "min_setpoint": 7.2, - "max_setpoint": 7.6, - }, - "orp_setpoint": { - "name": "ORP Setpoint", - "value": 700, - "unit": "mV", - "min_setpoint": 400, - "max_setpoint": 800, - }, - "calcium_hardness": { - "name": "Calcium Hardness", - "value": 740, - "unit": "ppm", - "min_setpoint": 25, - "max_setpoint": 800, - }, - "cya": { - "name": "Cyanuric Acid", - "value": 36, - "unit": "ppm", - "min_setpoint": 0, - "max_setpoint": 201, - }, - "total_alkalinity": { - "name": "Total Alkalinity", - "value": 70, - "unit": "ppm", - "min_setpoint": 25, - "max_setpoint": 800, - }, - "salt_tds_ppm": { - "name": "Salt/TDS", - "value": 1000, - "unit": "ppm", - "min_setpoint": 500, - "max_setpoint": 6500, - }, - "probe_is_celsius": 0, - "flags": 32, - }, - "dose_status": { - "ph_last_dose_time": { - "name": "Last pH Dose Time", - "value": 5, - "unit": "sec", - "device_type": "duration", - "state_type": "total_increasing", - }, - "orp_last_dose_time": { - "name": "Last ORP Dose Time", - "value": 2, - "unit": "sec", - "device_type": "duration", - "state_type": "total_increasing", - }, - "ph_last_dose_volume": { - "name": "Last pH Dose Volume", - "value": 10, - "unit": "mL", - "device_type": "volume", - "state_type": "total_increasing", - }, - "orp_last_dose_volume": { - "name": "Last ORP Dose Volume", - "value": 4, - "unit": "mL", - "device_type": "volume", - "state_type": "total_increasing", - }, - "flags": 165, - "ph_dosing_state": { - "name": "pH Dosing State", - "value": 2, - "device_type": "enum", - "enum_options": ["Dosing", "Mixing", "Monitoring"], - }, - "orp_dosing_state": { - "name": "ORP Dosing State", - "value": 2, - "device_type": "enum", - "enum_options": ["Dosing", "Mixing", "Monitoring"], - }, - }, - "alarm": { - "flags": 129, - "flow_alarm": { - "name": "Flow Alarm", - "value": 1, - "device_type": "alarm", - }, - "ph_high_alarm": { - "name": "pH HIGH Alarm", - "value": 0, - "device_type": "alarm", - }, - "ph_low_alarm": { - "name": "pH LOW Alarm", - "value": 0, - "device_type": "alarm", - }, - "orp_high_alarm": { - "name": "ORP HIGH Alarm", - "value": 0, - "device_type": "alarm", - }, - "orp_low_alarm": { - "name": "ORP LOW Alarm", - "value": 0, - "device_type": "alarm", - }, - "ph_supply_alarm": { - "name": "pH Supply Alarm", - "value": 0, - "device_type": "alarm", - }, - "orp_supply_alarm": { - "name": "ORP Supply Alarm", - "value": 0, - "device_type": "alarm", - }, - "probe_fault_alarm": { - "name": "Probe Fault", - "value": 1, - "device_type": "alarm", - }, - }, - "alert": { - "flags": 0, - "ph_lockout": {"name": "pH Lockout", "value": 0}, - "ph_limit": {"name": "pH Dose Limit Reached", "value": 0}, - "orp_limit": {"name": "ORP Dose Limit Reached", "value": 0}, - }, - "firmware": {"name": "IntelliChem Firmware", "value": "1.060"}, - "water_balance": { - "flags": 0, - "corrosive": { - "name": "SI Corrosive", - "value": 0, - "device_type": "alarm", - }, - "scaling": { - "name": "SI Scaling", - "value": 0, - "device_type": "alarm", - }, - }, - "unknown_at_offset_44": 0, - "unknown_at_offset_45": 0, - "unknown_at_offset_46": 0, - }, - "scg": { - "scg_present": 0, - "sensor": { - "state": {"name": "Chlorinator", "value": 1}, - "salt_ppm": { - "name": "Chlorinator Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement", - }, - }, - "configuration": { - "pool_setpoint": { - "name": "Pool Chlorinator Setpoint", - "value": 50, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 1, - "body_type": 0, - }, - "spa_setpoint": { - "name": "Spa Chlorinator Setpoint", - "value": 0, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 1, - "body_type": 1, - }, - "super_chlor_timer": { - "name": "Super Chlorination Timer", - "value": 0, - "unit": "hr", - "min_setpoint": 0, - "max_setpoint": 72, - "step": 1, - }, - }, - "flags": 0, - }, - }, - version=ScreenLogicResponseSet( - raw=b"\x19\x00\x00\x00POOL: 5.2 Build 736.0 Rel\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x0c\x00\x00\x00", - decoded={ - "adapter": { - "firmware": { - "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 736.0 Rel", - } - } - }, - ), - config=ScreenLogicResponseSet( - raw=b"d\x00\x00\x00(h(h\x00\r\x00\x008\x80\xff\xff\x0e\x00\x00\x00Water Features\x00\x00\x12\x00\x00\x00\xf4\x01\x00\x00\x03\x00\x00\x00Spa\x00G\x01\x01\x01\x00\x00\x00\x01\xd0\x02\x00\x00\xf5\x01\x00\x00\t\x00\x00\x00Waterfall\x00\x00\x00U\x00\x02\x00\x00\x00\x00\x02\xd0\x02\x00\x00\xf6\x01\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\x10\x03\x00\x02\x00\n\x03\xd0\x02\x00\x00\xf7\x01\x00\x00\t\x00\x00\x00Spa Light\x00\x00\x00I\x10\x03\x00\x07\x00\n\x04\xd0\x02\x00\x00\xf8\x01\x00\x00\x07\x00\x00\x00Cleaner\x00\x15\x00\x02\x00\x00\x00\x00\x05\xd0\x02\x00\x00\xf9\x01\x00\x00\x08\x00\x00\x00Pool Low?\x02\x00\x01\x00\x00\x00\x06\xd0\x02\x00\x00\xfa\x01\x00\x00\n\x00\x00\x00Yard Light\x00\x00[\x00\x02\x00\x00\x00\x00\x07\xd0\x02\x00\x00\xfb\x01\x00\x00\x05\x00\x00\x00Aux 6\x00\x00\x00\x07\x00\x02\x00\x00\x00\x00\x08\xd0\x02\x00\x00\xfc\x01\x00\x00\t\x00\x00\x00Pool High\x00\x00\x00=\x05\x02\x00\x00\x00\x00\t\xd0\x02\x00\x00\xfe\x01\x00\x00\t\x00\x00\x00Feature 1\x00\x00\x00]\x00\x02\x00\x00\x00\x00\x0b\xd0\x02\x00\x00\xff\x01\x00\x00\t\x00\x00\x00Feature 2\x00\x00\x00^\x00\x02\x00\x00\x00\x00\x0c\xd0\x02\x00\x00\x00\x02\x00\x00\t\x00\x00\x00Feature 3\x00\x00\x00_\x00\x02\x00\x00\x00\x00\r\xd0\x02\x00\x00\x01\x02\x00\x00\t\x00\x00\x00Feature 4\x00\x00\x00`\x00\x02\x00\x00\x00\x00\x0e\xd0\x02\x00\x00\x02\x02\x00\x00\t\x00\x00\x00Feature 5\x00\x00\x00a\x00\x02\x00\x00\x00\x00\x0f\xd0\x02\x00\x00\x03\x02\x00\x00\t\x00\x00\x00Feature 6\x00\x00\x00b\x00\x02\x00\x00\x00\x00\x10\xd0\x02\x00\x00\x04\x02\x00\x00\t\x00\x00\x00Feature 7\x00\x00\x00c\x00\x02\x00\x00\x00\x00\x11\xd0\x02\x00\x00\x05\x02\x00\x00\t\x00\x00\x00Feature 8\x00\x00\x00d\x00\x02\x00\x00\x00\x00\x12\xd0\x02\x00\x00\x07\x02\x00\x00\x05\x00\x00\x00AuxEx\x00\x00\x00\\\x00\x02\x00\x00\x00\x00\x14\xd0\x02\x00\x00\x08\x00\x00\x00\x05\x00\x00\x00White\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\x0b\x00\x00\x00Light Green\x00\xa0\x00\x00\x00\xff\x00\x00\x00\xa0\x00\x00\x00\x05\x00\x00\x00Green\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00P\x00\x00\x00\x04\x00\x00\x00Cyan\x00\x00\x00\x00\xff\x00\x00\x00\xc8\x00\x00\x00\x04\x00\x00\x00Blued\x00\x00\x00\x8c\x00\x00\x00\xff\x00\x00\x00\x08\x00\x00\x00Lavender\xe6\x00\x00\x00\x82\x00\x00\x00\xff\x00\x00\x00\x07\x00\x00\x00Magenta\x00\xff\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\r\x00\x00\x00Light Magenta\x00\x00\x00\xff\x00\x00\x00\xb4\x00\x00\x00\xd2\x00\x00\x00F\x00\x00\x00\x00\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "controller": { - "controller_id": 100, - "configuration": { - "body_type": { - 0: {"min_setpoint": 40, "max_setpoint": 104}, - 1: {"min_setpoint": 40, "max_setpoint": 104}, - }, - "is_celsius": {"name": "Is Celsius", "value": 0}, - "controller_type": 13, - "hardware_type": 0, - "controller_data": 0, - "generic_circuit_name": "Water Features", - "circuit_count": 18, - "color_count": 8, - "color": [ - {"name": "White", "value": (255, 255, 255)}, - {"name": "Light Green", "value": (160, 255, 160)}, - {"name": "Green", "value": (0, 255, 80)}, - {"name": "Cyan", "value": (0, 255, 200)}, - {"name": "Blue", "value": (100, 140, 255)}, - {"name": "Lavender", "value": (230, 130, 255)}, - {"name": "Magenta", "value": (255, 0, 128)}, - {"name": "Light Magenta", "value": (255, 180, 210)}, - ], - "interface_tab_flags": 127, - "show_alarms": 0, - }, - "model": {"name": "Model", "value": "EasyTouch2 8"}, - "equipment": { - "flags": 98360, - "list": [ - "INTELLIBRITE", - "INTELLIFLO_0", - "INTELLIFLO_1", - "INTELLICHEM", - "HYBRID_HEATER", - ], - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "name": "Spa", - "configuration": { - "name_index": 71, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_62": 0, - "unknown_at_offset_63": 0, - }, - "function": 1, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 1, - }, - 501: { - "circuit_id": 501, - "name": "Waterfall", - "configuration": { - "name_index": 85, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_94": 0, - "unknown_at_offset_95": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 2, - }, - 502: { - "circuit_id": 502, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_126": 0, - "unknown_at_offset_127": 0, - }, - "function": 16, - "interface": 3, - "color": { - "color_set": 2, - "color_position": 0, - "color_stagger": 10, - }, - "device_id": 3, - }, - 503: { - "circuit_id": 503, - "name": "Spa Light", - "configuration": { - "name_index": 73, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_158": 0, - "unknown_at_offset_159": 0, - }, - "function": 16, - "interface": 3, - "color": { - "color_set": 7, - "color_position": 0, - "color_stagger": 10, - }, - "device_id": 4, - }, - 504: { - "circuit_id": 504, - "name": "Cleaner", - "configuration": { - "name_index": 21, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_186": 0, - "unknown_at_offset_187": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 5, - }, - 505: { - "circuit_id": 505, - "name": "Pool Low", - "configuration": { - "name_index": 63, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_214": 0, - "unknown_at_offset_215": 0, - }, - "function": 2, - "interface": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 6, - }, - 506: { - "circuit_id": 506, - "name": "Yard Light", - "configuration": { - "name_index": 91, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_246": 0, - "unknown_at_offset_247": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 7, - }, - 507: { - "circuit_id": 507, - "name": "Aux 6", - "configuration": { - "name_index": 7, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_274": 0, - "unknown_at_offset_275": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 8, - }, - 508: { - "circuit_id": 508, - "name": "Pool High", - "configuration": { - "name_index": 61, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_306": 0, - "unknown_at_offset_307": 0, - }, - "function": 5, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 9, - }, - 510: { - "circuit_id": 510, - "name": "Feature 1", - "configuration": { - "name_index": 93, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_338": 0, - "unknown_at_offset_339": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 11, - }, - 511: { - "circuit_id": 511, - "name": "Feature 2", - "configuration": { - "name_index": 94, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_370": 0, - "unknown_at_offset_371": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 12, - }, - 512: { - "circuit_id": 512, - "name": "Feature 3", - "configuration": { - "name_index": 95, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_402": 0, - "unknown_at_offset_403": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 13, - }, - 513: { - "circuit_id": 513, - "name": "Feature 4", - "configuration": { - "name_index": 96, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_434": 0, - "unknown_at_offset_435": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 14, - }, - 514: { - "circuit_id": 514, - "name": "Feature 5", - "configuration": { - "name_index": 97, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_466": 0, - "unknown_at_offset_467": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 15, - }, - 515: { - "circuit_id": 515, - "name": "Feature 6", - "configuration": { - "name_index": 98, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_498": 0, - "unknown_at_offset_499": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 16, - }, - 516: { - "circuit_id": 516, - "name": "Feature 7", - "configuration": { - "name_index": 99, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_530": 0, - "unknown_at_offset_531": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 17, - }, - 517: { - "circuit_id": 517, - "name": "Feature 8", - "configuration": { - "name_index": 100, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_562": 0, - "unknown_at_offset_563": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 18, - }, - 519: { - "circuit_id": 519, - "name": "AuxEx", - "configuration": { - "name_index": 92, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_590": 0, - "unknown_at_offset_591": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 20, - }, - }, - "pump": { - 0: {"data": 70}, - 1: {"data": 0}, - 2: {"data": 0}, - 3: {"data": 0}, - 4: {"data": 0}, - 5: {"data": 0}, - 6: {"data": 0}, - 7: {"data": 0}, - }, - }, - ), - status=ScreenLogicResponseSet( - raw=b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x009\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00V\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00a\x00\x00\x00\x00\x00\x00\x00a\x00\x00\x009\x00\x00\x00\x03\x00\x00\x00\x12\x00\x00\x00\xf4\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf5\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\x01\x00\x00\x01\x00\x00\x00\x02\x00\n\x00\xf7\x01\x00\x00\x01\x00\x00\x00\x07\x00\n\x00\xf8\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xf9\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfa\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xfb\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfc\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xec\x02\x00\x00\xbd\x02\x00\x00\xf3\xff\xff\xff\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00", - decoded={ - "controller": { - "sensor": { - "state": { - "name": "Controller State", - "value": 1, - "device_type": "enum", - "enum_options": ["Unknown", "Ready", "Sync", "Service"], - }, - "freeze_mode": {"name": "Freeze Mode", "value": 0}, - "pool_delay": {"name": "Pool Delay", "value": 0}, - "spa_delay": {"name": "Spa Delay", "value": 0}, - "cleaner_delay": {"name": "Cleaner Delay", "value": 0}, - "air_temperature": { - "name": "Air Temperature", - "value": 57, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "ph": { - "name": "pH", - "value": 7.48, - "unit": "pH", - "state_type": "measurement", - }, - "orp": { - "name": "ORP", - "value": 701, - "unit": "mV", - "state_type": "measurement", - }, - "saturation": { - "name": "Saturation Index", - "value": -0.13, - "unit": "lsi", - "state_type": "measurement", - }, - "salt_ppm": { - "name": "Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement", - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 3, - "state_type": "measurement", - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 4, - "state_type": "measurement", - }, - "active_alert": { - "name": "Active Alert", - "value": 1, - "device_type": "alarm", - }, - }, - "configuration": { - "remotes": 0, - "unknown_at_offset_09": 0, - "unknown_at_offset_10": 0, - "unknown_at_offset_11": 0, - }, - }, - "body": { - 0: { - "body_type": 0, - "name": "Pool", - "last_temperature": { - "name": "Last Pool Temperature", - "value": 56, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Pool Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Pool Heat Set Point", - "value": 86, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Pool Cool Set Point", - "value": 100, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Pool Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - 1: { - "body_type": 1, - "name": "Spa", - "last_temperature": { - "name": "Last Spa Temperature", - "value": 97, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Spa Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Spa Heat Set Point", - "value": 97, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Spa Cool Set Point", - "value": 57, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Spa Heat Mode", - "value": 3, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 501: { - "circuit_id": 501, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 502: { - "circuit_id": 502, - "value": 1, - "color": { - "color_set": 2, - "color_position": 0, - "color_stagger": 10, - }, - "configuration": {"delay": 0}, - }, - 503: { - "circuit_id": 503, - "value": 1, - "color": { - "color_set": 7, - "color_position": 0, - "color_stagger": 10, - }, - "configuration": {"delay": 0}, - }, - 504: { - "circuit_id": 504, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 505: { - "circuit_id": 505, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 506: { - "circuit_id": 506, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 507: { - "circuit_id": 507, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 508: { - "circuit_id": 508, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 510: { - "circuit_id": 510, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 511: { - "circuit_id": 511, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 512: { - "circuit_id": 512, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 513: { - "circuit_id": 513, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 514: { - "circuit_id": 514, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 515: { - "circuit_id": 515, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 516: { - "circuit_id": 516, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 517: { - "circuit_id": 517, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 519: { - "circuit_id": 519, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - }, - }, - ), - pumps=[ - ScreenLogicResponseSet( - raw=b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x06\x00\x00\x00\xc4\t\x00\x00\x01\x00\x00\x00\t\x00\x00\x00\xf0\n\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00z\r\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "pump": { - 0: { - "type": 3, - "state": {"name": "Default Pump", "value": 0}, - "watts_now": { - "name": "Default Pump Watts Now", - "value": 0, - "unit": "W", - "device_type": "power", - "state_type": "measurement", - }, - "rpm_now": { - "name": "Default Pump RPM Now", - "value": 0, - "unit": "rpm", - "state_type": "measurement", - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Default Pump GPM Now", - "value": 0, - "unit": "gpm", - "state_type": "measurement", - }, - "unknown_at_offset_24": 255, - "preset": { - 0: {"device_id": 6, "setpoint": 2500, "is_rpm": 1}, - 1: {"device_id": 9, "setpoint": 2800, "is_rpm": 1}, - 2: {"device_id": 1, "setpoint": 3450, "is_rpm": 1}, - 3: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - 4: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - 5: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - 6: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - 7: {"device_id": 0, "setpoint": 30, "is_rpm": 0}, - }, - } - } - }, - ), - ], - chemistry=ScreenLogicResponseSet( - raw=b"*\x00\x00\x00\x00\x00\x00\x00\x00\x02\xee\x02\xbc\x00\x00\x00\x05\x00\x00\x00\x02\x00\n\x00\x04\x03\x04\xf3\x02\xe4\x00$\x00F\x14\x008\x81\x00\xa5 <\x01\x00\x00\x00\x00\x00", - decoded={ - "intellichem": { - "unknown_at_offset_00": 42, - "unknown_at_offset_04": 0, - "sensor": { - "ph_now": { - "name": "pH Now", - "value": 0.0, - "unit": "pH", - "state_type": "measurement", - }, - "orp_now": { - "name": "ORP Now", - "value": 0, - "unit": "mV", - "state_type": "measurement", - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 3, - "state_type": "measurement", - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 4, - "state_type": "measurement", - }, - "saturation": { - "name": "Saturation Index", - "value": -0.13, - "unit": "lsi", - "state_type": "measurement", - }, - "ph_probe_water_temp": { - "name": "pH Probe Water Temperature", - "value": 56, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - }, - "configuration": { - "ph_setpoint": { - "name": "pH Setpoint", - "value": 7.5, - "unit": "pH", - "min_setpoint": 7.2, - "max_setpoint": 7.6, - }, - "orp_setpoint": { - "name": "ORP Setpoint", - "value": 700, - "unit": "mV", - "min_setpoint": 400, - "max_setpoint": 800, - }, - "calcium_hardness": { - "name": "Calcium Hardness", - "value": 740, - "unit": "ppm", - "min_setpoint": 25, - "max_setpoint": 800, - }, - "cya": { - "name": "Cyanuric Acid", - "value": 36, - "unit": "ppm", - "min_setpoint": 0, - "max_setpoint": 201, - }, - "total_alkalinity": { - "name": "Total Alkalinity", - "value": 70, - "unit": "ppm", - "min_setpoint": 25, - "max_setpoint": 800, - }, - "salt_tds_ppm": { - "name": "Salt/TDS", - "value": 1000, - "unit": "ppm", - "min_setpoint": 500, - "max_setpoint": 6500, - }, - "probe_is_celsius": 0, - "flags": 32, - }, - "dose_status": { - "ph_last_dose_time": { - "name": "Last pH Dose Time", - "value": 5, - "unit": "sec", - "device_type": "duration", - "state_type": "total_increasing", - }, - "orp_last_dose_time": { - "name": "Last ORP Dose Time", - "value": 2, - "unit": "sec", - "device_type": "duration", - "state_type": "total_increasing", - }, - "ph_last_dose_volume": { - "name": "Last pH Dose Volume", - "value": 10, - "unit": "mL", - "device_type": "volume", - "state_type": "total_increasing", - }, - "orp_last_dose_volume": { - "name": "Last ORP Dose Volume", - "value": 4, - "unit": "mL", - "device_type": "volume", - "state_type": "total_increasing", - }, - "flags": 165, - "ph_dosing_state": { - "name": "pH Dosing State", - "value": 2, - "device_type": "enum", - "enum_options": ["Dosing", "Mixing", "Monitoring"], - }, - "orp_dosing_state": { - "name": "ORP Dosing State", - "value": 2, - "device_type": "enum", - "enum_options": ["Dosing", "Mixing", "Monitoring"], - }, - }, - "alarm": { - "flags": 129, - "flow_alarm": { - "name": "Flow Alarm", - "value": 1, - "device_type": "alarm", - }, - "ph_high_alarm": { - "name": "pH HIGH Alarm", - "value": 0, - "device_type": "alarm", - }, - "ph_low_alarm": { - "name": "pH LOW Alarm", - "value": 0, - "device_type": "alarm", - }, - "orp_high_alarm": { - "name": "ORP HIGH Alarm", - "value": 0, - "device_type": "alarm", - }, - "orp_low_alarm": { - "name": "ORP LOW Alarm", - "value": 0, - "device_type": "alarm", - }, - "ph_supply_alarm": { - "name": "pH Supply Alarm", - "value": 0, - "device_type": "alarm", - }, - "orp_supply_alarm": { - "name": "ORP Supply Alarm", - "value": 0, - "device_type": "alarm", - }, - "probe_fault_alarm": { - "name": "Probe Fault", - "value": 1, - "device_type": "alarm", - }, - }, - "alert": { - "flags": 0, - "ph_lockout": {"name": "pH Lockout", "value": 0}, - "ph_limit": {"name": "pH Dose Limit Reached", "value": 0}, - "orp_limit": {"name": "ORP Dose Limit Reached", "value": 0}, - }, - "firmware": {"name": "IntelliChem Firmware", "value": "1.060"}, - "water_balance": { - "flags": 0, - "corrosive": { - "name": "SI Corrosive", - "value": 0, - "device_type": "alarm", - }, - "scaling": { - "name": "SI Scaling", - "value": 0, - "device_type": "alarm", - }, - }, - "unknown_at_offset_44": 0, - "unknown_at_offset_45": 0, - "unknown_at_offset_46": 0, - } - }, - ), - scg=ScreenLogicResponseSet( - raw=b"\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "scg": { - "scg_present": 0, - "sensor": { - "state": {"name": "Chlorinator", "value": 1}, - "salt_ppm": { - "name": "Chlorinator Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement", - }, - }, - "configuration": { - "pool_setpoint": { - "name": "Pool Chlorinator Setpoint", - "value": 50, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 1, - "body_type": 0, - }, - "spa_setpoint": { - "name": "Spa Chlorinator Setpoint", - "value": 0, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 1, - "body_type": 1, - }, - "super_chlor_timer": { - "name": "Super Chlorination Timer", - "value": 0, - "unit": "hr", - "min_setpoint": 0, - "max_setpoint": 72, - "step": 1, - }, - }, - "flags": 0, - } - }, - ), - color=[ - ScreenLogicResponseSet( - raw=struct.pack("<5I", 12, progress, 15, 0, 0), - decoded={ - "controller": { - "color_lights": { - "color_mode": 12, - "progress": progress, - "limit": 15, - "text": "", - } - } - }, - ) - for progress in list(range(15)) + [0] - ], - ), - ScreenLogicResponseCollection( - {}, - config=ScreenLogicResponseSet( - raw=b"d\x00\x00\x00(h(h\x00\r\x06 \x18\x00\x00\x00\x0e\x00\x00\x00Water Features\x00\x00\x07\x00\x00\x00\xf4\x01\x00\x00\x03\x00\x00\x00Spa\x00G\x01\x01\x01\x00\x00\x00\x01\xd0\x02\x00\x00\xf5\x01\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\x10\x04\x00\x00\x00\x00\x02\xd0\x02\x00\x00\xf6\x01\x00\x00\x05\x00\x00\x00Aux 2\x00\x00\x00\x03\x00\x02\x00\x00\x00\x00\x03\xd0\x02\x00\x00\xf7\x01\x00\x00\x05\x00\x00\x00Aux 3\x00\x00\x00\x04\x00\x02\x00\x00\x00\x00\x04\xd0\x02\x00\x00\xf9\x01\x00\x00\x04\x00\x00\x00Pool<\x02\x00\x01\x00\x00\x00\x06\xd0\x02\x00\x00\xfe\x01\x00\x00\t\x00\x00\x00Feature 1\x00\x00\x00]\x00\x02\x00\x00\x00\x00\x0b\xd0\x02\x00\x00\xff\x01\x00\x00\t\x00\x00\x00Feature 2\x00\x00\x00^\x00\x02\x00\x00\x00\x00\x0c\xd0\x02\x00\x00\x08\x00\x00\x00\x05\x00\x00\x00White\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\x0b\x00\x00\x00Light Green\x00\xa0\x00\x00\x00\xff\x00\x00\x00\xa0\x00\x00\x00\x05\x00\x00\x00Green\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00P\x00\x00\x00\x04\x00\x00\x00Cyan\x00\x00\x00\x00\xff\x00\x00\x00\xc8\x00\x00\x00\x04\x00\x00\x00Blued\x00\x00\x00\x8c\x00\x00\x00\xff\x00\x00\x00\x08\x00\x00\x00Lavender\xe6\x00\x00\x00\x82\x00\x00\x00\xff\x00\x00\x00\x07\x00\x00\x00Magenta\x00\xff\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\r\x00\x00\x00Light Magenta\x00\x00\x00\xff\x00\x00\x00\xb4\x00\x00\x00\xd2\x00\x00\x00\x81\x00\x00\x00\x00\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "controller": { - "controller_id": 100, - "configuration": { - "body_type": { - 0: {"min_setpoint": 40, "max_setpoint": 104}, - 1: {"min_setpoint": 40, "max_setpoint": 104}, - }, - "is_celsius": {"name": "Is Celsius", "value": 0}, - "controller_type": 13, - "hardware_type": 6, - "controller_data": 32, - "generic_circuit_name": "Water Features", - "circuit_count": 7, - "color_count": 8, - "color": [ - {"name": "White", "value": (255, 255, 255)}, - {"name": "Light Green", "value": (160, 255, 160)}, - {"name": "Green", "value": (0, 255, 80)}, - {"name": "Cyan", "value": (0, 255, 200)}, - {"name": "Blue", "value": (100, 140, 255)}, - {"name": "Lavender", "value": (230, 130, 255)}, - {"name": "Magenta", "value": (255, 0, 128)}, - {"name": "Light Magenta", "value": (255, 180, 210)}, - ], - "interface_tab_flags": 127, - "show_alarms": 0, - }, - "model": {"name": "Model", "value": "EasyTouch2 PSL4"}, - "equipment": { - "flags": 24, - "list": ["INTELLIBRITE", "INTELLIFLO_0"], - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "name": "Spa", - "configuration": { - "name_index": 71, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_62": 0, - "unknown_at_offset_63": 0, - }, - "function": 1, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 1, - }, - 501: { - "circuit_id": 501, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_94": 0, - "unknown_at_offset_95": 0, - }, - "function": 16, - "interface": 4, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 2, - }, - 502: { - "circuit_id": 502, - "name": "Aux 2", - "configuration": { - "name_index": 3, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_122": 0, - "unknown_at_offset_123": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 3, - }, - 503: { - "circuit_id": 503, - "name": "Aux 3", - "configuration": { - "name_index": 4, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_150": 0, - "unknown_at_offset_151": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 4, - }, - 505: { - "circuit_id": 505, - "name": "Pool", - "configuration": { - "name_index": 60, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_174": 0, - "unknown_at_offset_175": 0, - }, - "function": 2, - "interface": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 6, - }, - 510: { - "circuit_id": 510, - "name": "Feature 1", - "configuration": { - "name_index": 93, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_206": 0, - "unknown_at_offset_207": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 11, - }, - 511: { - "circuit_id": 511, - "name": "Feature 2", - "configuration": { - "name_index": 94, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_238": 0, - "unknown_at_offset_239": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 12, - }, - }, - "pump": { - 0: {"data": 129}, - 1: {"data": 0}, - 2: {"data": 0}, - 3: {"data": 0}, - 4: {"data": 0}, - 5: {"data": 0}, - 6: {"data": 0}, - 7: {"data": 0}, - }, - }, - ), - status=ScreenLogicResponseSet( - raw=b"\x01\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00-\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00:\x00\x00\x00\x00\x00\x00\x002\x00\x00\x00d\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00]\x00\x00\x00\x00\x00\x00\x00I\x00\x00\x00-\x00\x00\x00\x03\x00\x00\x00\x07\x00\x00\x00\xf4\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf5\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf7\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf9\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xfe\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "controller": { - "sensor": { - "state": { - "name": "Controller State", - "value": 1, - "device_type": "enum", - "enum_options": ["Unknown", "Ready", "Sync", "Service"], - }, - "freeze_mode": {"name": "Freeze Mode", "value": 0}, - "pool_delay": {"name": "Pool Delay", "value": 0}, - "spa_delay": {"name": "Spa Delay", "value": 0}, - "cleaner_delay": {"name": "Cleaner Delay", "value": 0}, - "air_temperature": { - "name": "Air Temperature", - "value": 45, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "ph": { - "name": "pH", - "value": 0.0, - "unit": "pH", - "state_type": "measurement", - }, - "orp": { - "name": "ORP", - "value": 0, - "unit": "mV", - "state_type": "measurement", - }, - "saturation": { - "name": "Saturation Index", - "value": 0.0, - "unit": "lsi", - "state_type": "measurement", - }, - "salt_ppm": { - "name": "Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement", - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 0, - "state_type": "measurement", - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 0, - "state_type": "measurement", - }, - "active_alert": { - "name": "Active Alert", - "value": 0, - "device_type": "alarm", - }, - }, - "configuration": { - "remotes": 32, - "unknown_at_offset_09": 0, - "unknown_at_offset_10": 0, - "unknown_at_offset_11": 0, - }, - }, - "body": { - 0: { - "body_type": 0, - "name": "Pool", - "last_temperature": { - "name": "Last Pool Temperature", - "value": 58, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Pool Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Pool Heat Set Point", - "value": 50, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Pool Cool Set Point", - "value": 100, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Pool Heat Mode", - "value": 3, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - 1: { - "body_type": 1, - "name": "Spa", - "last_temperature": { - "name": "Last Spa Temperature", - "value": 93, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Spa Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Spa Heat Set Point", - "value": 73, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Spa Cool Set Point", - "value": 45, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Spa Heat Mode", - "value": 3, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 501: { - "circuit_id": 501, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 502: { - "circuit_id": 502, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 503: { - "circuit_id": 503, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 505: { - "circuit_id": 505, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 510: { - "circuit_id": 510, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 511: { - "circuit_id": 511, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - }, - }, - ), - pumps=[ - ScreenLogicResponseSet( - raw=b"\x02\x00\x00\x00\x01\x00\x00\x00u\x08\x00\x00z\r\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\x01\x00\x00\x00z\r\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00:\x07\x00\x00\x01\x00\x00\x00\x82\x00\x00\x00\xc4\t\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00", - decoded={ - "pump": { - 0: { - "type": 2, - "state": {"name": "Default Pump", "value": 1}, - "watts_now": { - "name": "Default Pump Watts Now", - "value": 2165, - "unit": "W", - "device_type": "power", - "state_type": "measurement", - }, - "rpm_now": { - "name": "Default Pump RPM Now", - "value": 3450, - "unit": "rpm", - "state_type": "measurement", - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Default Pump GPM Now", - "value": 255, - "unit": "gpm", - "state_type": "measurement", - }, - "unknown_at_offset_24": 255, - "preset": { - 0: {"device_id": 1, "setpoint": 3450, "is_rpm": 1}, - 1: {"device_id": 6, "setpoint": 1850, "is_rpm": 1}, - 2: {"device_id": 130, "setpoint": 2500, "is_rpm": 1}, - 3: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 4: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 5: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 6: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 7: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - }, - } - } - }, - ), - ], - ), - ScreenLogicResponseCollection( - {}, - config=ScreenLogicResponseSet( - raw=b"d\x00\x00\x00(h(h\x00\x02\x00\xa00\x00\x00\x00\x0e\x00\x00\x00Water Features\x00\x00\x1b\x00\x00\x00\xf4\x01\x00\x00\x03\x00\x00\x00Spa\x00G\x01\x01\x01\x00\x00\x00\x01\xd0\x02\x00\x00\xf5\x01\x00\x00\x07\x00\x00\x00Cleaner\x00\x15\x05\x00\x00\x00\x00\x00\x02\xd0\x02\x00\x00\xf6\x01\x00\x00\x04\x00\x00\x00Jets-\x00\x01\x00\x00\x00\x00\x03\xd0\x02\x00\x00\xf7\x01\x00\x00\r\x00\x00\x00Water Feature\x00\x00\x00S\x00\x02\x00\x00\x00\x00\x04\xd0\x02\x00\x00\xf8\x01\x00\x00\x0b\x00\x00\x00Fiber Optic\x00\x1d\x0b\x04\x00\x00\x00\x00\x05\xd0\x02\x00\x00\xf9\x01\x00\x00\x04\x00\x00\x00Pool<\x02\x00\x01\x00\x00\x00\x06\xd0\x02\x00\x00\xfa\x01\x00\x00\x0b\x00\x00\x00Color Wheel\x00\x16\x0c\x03\x00\x00\x00\n\x07\xd0\x02\x00\x00\xfb\x01\x00\x00\x0b\x00\x00\x00Color Wheel\x00\x16\x0c\x03\x00\x00\x00\n\x08\xd0\x02\x00\x00\xfc\x01\x00\x00\x05\x00\x00\x00Aux 7\x00\x00\x00\x08\x00\x05\x00\x00\x00\x00\t\xd0\x02\x00\x00\xfd\x01\x00\x00\x05\x00\x00\x00Aux 8\x00\x00\x00\t\x00\x05\x00\x00\x00\x00\n\xd0\x02\x00\x00\xfe\x01\x00\x00\t\x00\x00\x00Spa Light\x00\x00\x00I\n\x03\x00\x00\x00\x00\x0b\xd0\x02\x00\x00\xff\x01\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\x07\x04\x00\x00\x00\x00\x0c\xd0\x02\x00\x00\x00\x02\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\t\x03\x00\x00\x00\x00\r\xd0\x02\x00\x00\x01\x02\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\t\x03\x00\x00\x00\x00\x0e\xd0\x02\x00\x00\x02\x02\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\t\x03\x00\x00\x00\x00\x0f\xd0\x02\x00\x00\x03\x02\x00\x00\n\x00\x00\x00Pool Light\x00\x00>\t\x03\x00\x00\x00\x00\x10\xd0\x02\x00\x00\x04\x02\x00\x00\t\x00\x00\x00Waterfall\x00\x00\x00U\x00\x02\x00\x00\x00\x00\x11\xd0\x02\x00\x00\x05\x02\x00\x00\x05\x00\x00\x00Aux 8\x00\x00\x00\t\x00\x05\x00\x00\x00\x00\x12\xd0\x02\x00\x00\x06\x02\x00\x00\x05\x00\x00\x00Aux 9\x00\x00\x00\n\x00\x05\x00\x00\x00\x00\x13\xd0\x02\x00\x00\x07\x02\x00\x00\x06\x00\x00\x00Aux 10\x00\x00\x0b\x00\x05\x00\x00\x00\x00\x14\xd0\x02\x00\x00\x08\x02\x00\x00\x08\x00\x00\x00Upr Pool^\x00\x02\x00\x00\x00\x00\x15\xd0\x02\x00\x00\t\x02\x00\x00\x0b\x00\x00\x00Upr Cleaner\x00_\x00\x00\x00\x00\x00\x00\x16\xd0\x02\x00\x00\n\x02\x00\x00\x0b\x00\x00\x00Upr Wtrfall\x00`\x00\x02\x00\x00\x00\x00\x17\xd0\x02\x00\x00\x0b\x02\x00\x00\t\x00\x00\x00Upr Light\x00\x00\x00a\x07\x04\x00\x00\x00\x00\x18\xd0\x02\x00\x00\x0c\x02\x00\x00\x05\x00\x00\x00Aux 5\x00\x00\x00\x06\x00\x05\x00\x00\x00\x00\x19\xd0\x02\x00\x00\x1c\x02\x00\x00\r\x00\x00\x00Spa Waterfall\x00\x00\x00M\x0e\x02\x00\x00\x00\x00)\xd0\x02\x00\x00\x1d\x02\x00\x00\x05\x00\x00\x00Slide\x00\x00\x00E\x00\x02\x00\x00\x00\x00*\xd0\x02\x00\x00\x08\x00\x00\x00\x05\x00\x00\x00White\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\x0b\x00\x00\x00Light Green\x00\xa0\x00\x00\x00\xff\x00\x00\x00\xa0\x00\x00\x00\x05\x00\x00\x00Green\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00P\x00\x00\x00\x04\x00\x00\x00Cyan\x00\x00\x00\x00\xff\x00\x00\x00\xc8\x00\x00\x00\x04\x00\x00\x00Blued\x00\x00\x00\x8c\x00\x00\x00\xff\x00\x00\x00\x08\x00\x00\x00Lavender\xe6\x00\x00\x00\x82\x00\x00\x00\xff\x00\x00\x00\x07\x00\x00\x00Magenta\x00\xff\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\r\x00\x00\x00Light Magenta\x00\x00\x00\xff\x00\x00\x00\xb4\x00\x00\x00\xd2\x00\x00\x00\x86\xaa\x00\x00\x00\x00\x00\x00\x7f\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "controller": { - "controller_id": 100, - "configuration": { - "body_type": { - 0: {"min_setpoint": 40, "max_setpoint": 104}, - 1: {"min_setpoint": 40, "max_setpoint": 104}, - }, - "is_celsius": {"name": "Is Celsius", "value": 0}, - "controller_type": 2, - "hardware_type": 0, - "controller_data": 160, - "generic_circuit_name": "Water Features", - "circuit_count": 27, - "color_count": 8, - "color": [ - {"name": "White", "value": (255, 255, 255)}, - {"name": "Light Green", "value": (160, 255, 160)}, - {"name": "Green", "value": (0, 255, 80)}, - {"name": "Cyan", "value": (0, 255, 200)}, - {"name": "Blue", "value": (100, 140, 255)}, - {"name": "Lavender", "value": (230, 130, 255)}, - {"name": "Magenta", "value": (255, 0, 128)}, - {"name": "Light Magenta", "value": (255, 180, 210)}, - ], - "interface_tab_flags": 127, - "show_alarms": 0, - }, - "model": {"name": "Model", "value": "IntelliTouch i9+3"}, - "equipment": { - "flags": 48, - "list": ["INTELLIFLO_0", "INTELLIFLO_1"], - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "name": "Spa", - "configuration": { - "name_index": 71, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_62": 0, - "unknown_at_offset_63": 0, - }, - "function": 1, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 1, - }, - 501: { - "circuit_id": 501, - "name": "Cleaner", - "configuration": { - "name_index": 21, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_90": 0, - "unknown_at_offset_91": 0, - }, - "function": 5, - "interface": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 2, - }, - 502: { - "circuit_id": 502, - "name": "Jets", - "configuration": { - "name_index": 45, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_114": 0, - "unknown_at_offset_115": 0, - }, - "function": 0, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 3, - }, - 503: { - "circuit_id": 503, - "name": "Water Feature", - "configuration": { - "name_index": 83, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_150": 0, - "unknown_at_offset_151": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 4, - }, - 504: { - "circuit_id": 504, - "name": "Fiber Optic", - "configuration": { - "name_index": 29, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_182": 0, - "unknown_at_offset_183": 0, - }, - "function": 11, - "interface": 4, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 5, - }, - 505: { - "circuit_id": 505, - "name": "Pool", - "configuration": { - "name_index": 60, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_206": 0, - "unknown_at_offset_207": 0, - }, - "function": 2, - "interface": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 6, - }, - 506: { - "circuit_id": 506, - "name": "Color Wheel", - "configuration": { - "name_index": 22, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_238": 0, - "unknown_at_offset_239": 0, - }, - "function": 12, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 10, - }, - "device_id": 7, - }, - 507: { - "circuit_id": 507, - "name": "Color Wheel", - "configuration": { - "name_index": 22, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_270": 0, - "unknown_at_offset_271": 0, - }, - "function": 12, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 10, - }, - "device_id": 8, - }, - 508: { - "circuit_id": 508, - "name": "Aux 7", - "configuration": { - "name_index": 8, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_298": 0, - "unknown_at_offset_299": 0, - }, - "function": 0, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 9, - }, - 509: { - "circuit_id": 509, - "name": "Aux 8", - "configuration": { - "name_index": 9, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_326": 0, - "unknown_at_offset_327": 0, - }, - "function": 0, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 10, - }, - 510: { - "circuit_id": 510, - "name": "Spa Light", - "configuration": { - "name_index": 73, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_358": 0, - "unknown_at_offset_359": 0, - }, - "function": 10, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 11, - }, - 511: { - "circuit_id": 511, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_390": 0, - "unknown_at_offset_391": 0, - }, - "function": 7, - "interface": 4, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 12, - }, - 512: { - "circuit_id": 512, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_422": 0, - "unknown_at_offset_423": 0, - }, - "function": 9, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 13, - }, - 513: { - "circuit_id": 513, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_454": 0, - "unknown_at_offset_455": 0, - }, - "function": 9, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 14, - }, - 514: { - "circuit_id": 514, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_486": 0, - "unknown_at_offset_487": 0, - }, - "function": 9, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 15, - }, - 515: { - "circuit_id": 515, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_518": 0, - "unknown_at_offset_519": 0, - }, - "function": 9, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 16, - }, - 516: { - "circuit_id": 516, - "name": "Waterfall", - "configuration": { - "name_index": 85, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_550": 0, - "unknown_at_offset_551": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 17, - }, - 517: { - "circuit_id": 517, - "name": "Aux 8", - "configuration": { - "name_index": 9, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_578": 0, - "unknown_at_offset_579": 0, - }, - "function": 0, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 18, - }, - 518: { - "circuit_id": 518, - "name": "Aux 9", - "configuration": { - "name_index": 10, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_606": 0, - "unknown_at_offset_607": 0, - }, - "function": 0, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 19, - }, - 519: { - "circuit_id": 519, - "name": "Aux 10", - "configuration": { - "name_index": 11, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_634": 0, - "unknown_at_offset_635": 0, - }, - "function": 0, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 20, - }, - 520: { - "circuit_id": 520, - "name": "Upr Pool", - "configuration": { - "name_index": 94, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_662": 0, - "unknown_at_offset_663": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 21, - }, - 521: { - "circuit_id": 521, - "name": "Upr Cleaner", - "configuration": { - "name_index": 95, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_694": 0, - "unknown_at_offset_695": 0, - }, - "function": 0, - "interface": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 22, - }, - 522: { - "circuit_id": 522, - "name": "Upr Wtrfall", - "configuration": { - "name_index": 96, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_726": 0, - "unknown_at_offset_727": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 23, - }, - 523: { - "circuit_id": 523, - "name": "Upr Light", - "configuration": { - "name_index": 97, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_758": 0, - "unknown_at_offset_759": 0, - }, - "function": 7, - "interface": 4, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 24, - }, - 524: { - "circuit_id": 524, - "name": "Aux 5", - "configuration": { - "name_index": 6, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_786": 0, - "unknown_at_offset_787": 0, - }, - "function": 0, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 25, - }, - 540: { - "circuit_id": 540, - "name": "Spa Waterfall", - "configuration": { - "name_index": 77, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_822": 0, - "unknown_at_offset_823": 0, - }, - "function": 14, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 41, - }, - 541: { - "circuit_id": 541, - "name": "Slide", - "configuration": { - "name_index": 69, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_850": 0, - "unknown_at_offset_851": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 42, - }, - }, - "pump": { - 0: {"data": 134}, - 1: {"data": 170}, - 2: {"data": 0}, - 3: {"data": 0}, - 4: {"data": 0}, - 5: {"data": 0}, - 6: {"data": 0}, - 7: {"data": 0}, - }, - }, - ), - status=ScreenLogicResponseSet( - raw=b"\x01\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00>\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x000\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00H\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x1b\x00\x00\x00\xf4\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf5\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xf7\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xf8\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xf9\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xfa\x01\x00\x00\x01\x00\x00\x00\x00\x00\n\x00\xfb\x01\x00\x00\x01\x00\x00\x00\x00\x00\n\x00\xfc\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfd\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x05\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\t\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x0b\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x0c\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x1d\x02\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "controller": { - "sensor": { - "state": { - "name": "Controller State", - "value": 1, - "device_type": "enum", - "enum_options": ["Unknown", "Ready", "Sync", "Service"], - }, - "freeze_mode": {"name": "Freeze Mode", "value": 0}, - "pool_delay": {"name": "Pool Delay", "value": 0}, - "spa_delay": {"name": "Spa Delay", "value": 0}, - "cleaner_delay": {"name": "Cleaner Delay", "value": 0}, - "air_temperature": { - "name": "Air Temperature", - "value": 62, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "ph": { - "name": "pH", - "value": 0.0, - "unit": "pH", - "state_type": "measurement", - }, - "orp": { - "name": "ORP", - "value": 0, - "unit": "mV", - "state_type": "measurement", - }, - "saturation": { - "name": "Saturation Index", - "value": 0.0, - "unit": "lsi", - "state_type": "measurement", - }, - "salt_ppm": { - "name": "Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement", - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 0, - "state_type": "measurement", - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 0, - "state_type": "measurement", - }, - "active_alert": { - "name": "Active Alert", - "value": 0, - "device_type": "alarm", - }, - }, - "configuration": { - "remotes": 32, - "unknown_at_offset_09": 0, - "unknown_at_offset_10": 0, - "unknown_at_offset_11": 0, - }, - }, - "body": { - 0: { - "body_type": 0, - "name": "Pool", - "last_temperature": { - "name": "Last Pool Temperature", - "value": 48, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Pool Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Pool Heat Set Point", - "value": 84, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Pool Cool Set Point", - "value": 100, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Pool Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - 1: { - "body_type": 1, - "name": "Spa", - "last_temperature": { - "name": "Last Spa Temperature", - "value": 72, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Spa Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Spa Heat Set Point", - "value": 100, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Spa Cool Set Point", - "value": 62, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Spa Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 501: { - "circuit_id": 501, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 502: { - "circuit_id": 502, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 503: { - "circuit_id": 503, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 504: { - "circuit_id": 504, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 505: { - "circuit_id": 505, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 506: { - "circuit_id": 506, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 10, - }, - "configuration": {"delay": 0}, - }, - 507: { - "circuit_id": 507, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 10, - }, - "configuration": {"delay": 0}, - }, - 508: { - "circuit_id": 508, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 509: { - "circuit_id": 509, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 510: { - "circuit_id": 510, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 511: { - "circuit_id": 511, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 512: { - "circuit_id": 512, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 513: { - "circuit_id": 513, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 514: { - "circuit_id": 514, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 515: { - "circuit_id": 515, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 516: { - "circuit_id": 516, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 517: { - "circuit_id": 517, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 518: { - "circuit_id": 518, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 519: { - "circuit_id": 519, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 520: { - "circuit_id": 520, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 521: { - "circuit_id": 521, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 522: { - "circuit_id": 522, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 523: { - "circuit_id": 523, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 524: { - "circuit_id": 524, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 540: { - "circuit_id": 540, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 541: { - "circuit_id": 541, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - }, - }, - ), - pumps=[ - ScreenLogicResponseSet( - raw=b"\x02\x00\x00\x00\x01\x00\x00\x00\xdf\x02\x00\x00\xfc\x08\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\x06\x00\x00\x00\xfc\x08\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00`\t\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x8c\n\x00\x00\x01\x00\x00\x00\x9d\x00\x00\x00\xc2\x0b\x00\x00\x01\x00\x00\x00\x9f\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00)\x00\x00\x00\xb8\x0b\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00", - decoded={ - "pump": { - 0: { - "type": 2, - "state": {"name": "Default Pump", "value": 1}, - "watts_now": { - "name": "Default Pump Watts Now", - "value": 735, - "unit": "W", - "device_type": "power", - "state_type": "measurement", - }, - "rpm_now": { - "name": "Default Pump RPM Now", - "value": 2300, - "unit": "rpm", - "state_type": "measurement", - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Default Pump GPM Now", - "value": 255, - "unit": "gpm", - "state_type": "measurement", - }, - "unknown_at_offset_24": 255, - "preset": { - 0: {"device_id": 6, "setpoint": 2300, "is_rpm": 1}, - 1: {"device_id": 1, "setpoint": 2400, "is_rpm": 1}, - 2: {"device_id": 2, "setpoint": 2700, "is_rpm": 1}, - 3: {"device_id": 157, "setpoint": 3010, "is_rpm": 1}, - 4: {"device_id": 159, "setpoint": 1000, "is_rpm": 1}, - 5: {"device_id": 41, "setpoint": 3000, "is_rpm": 1}, - 6: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 7: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - }, - } - } - }, - ), - ScreenLogicResponseSet( - raw=b"\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00*\x00\x00\x00\xb8\x0b\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x01\x00\x00\x00", - decoded={ - "pump": { - 0: { - "type": 2, - "state": {"name": "Default Pump", "value": 0}, - "watts_now": { - "name": "Default Pump Watts Now", - "value": 0, - "unit": "W", - "device_type": "power", - "state_type": "measurement", - }, - "rpm_now": { - "name": "Default Pump RPM Now", - "value": 0, - "unit": "rpm", - "state_type": "measurement", - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Default Pump GPM Now", - "value": 0, - "unit": "gpm", - "state_type": "measurement", - }, - "unknown_at_offset_24": 255, - "preset": { - 0: {"device_id": 42, "setpoint": 3000, "is_rpm": 1}, - 1: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 2: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 3: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 4: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 5: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 6: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - 7: {"device_id": 0, "setpoint": 1000, "is_rpm": 1}, - }, - } - } - }, - ), - ], - ), - ScreenLogicResponseCollection( - {}, - config=ScreenLogicResponseSet( - raw=b"d\x00\x00\x00(h(h\x00\r\x06\x00\x1f \x00\x00\x0e\x00\x00\x00Water Features\x00\x00\x07\x00\x00\x00\xf4\x01\x00\x00\x08\x00\x00\x00Bubblersg\x01\x01\x00\x00\x00\x00\x01\xd0\x02\x00\x00\xf5\x01\x00\x00\x06\x00\x00\x00Lights\x00\x00.\x10\x04\x00\x00\x00\x00\x02\xd0\x02\x00\x00\xf6\x01\x00\x00\x07\x00\x00\x00Regular\x00e\x00\x02\x00\x00\x00\x00\x03\xd0\x02\x00\x00\xf7\x01\x00\x00\x04\x00\x00\x00Highh\x00\x02\x00\x00\x00\x00\x04\xd0\x02\x00\x00\xf9\x01\x00\x00\x04\x00\x00\x00Pool<\x02\x00\x00\x00\x00\x00\x06\xdf\x02\x00\x00\xfe\x01\x00\x00\x03\x00\x00\x00Low\x00i\x00\x02\x00\x00\x00\x00\x0b\xd0\x02\x00\x00\xff\x01\x00\x00\x08\x00\x00\x00Variablef\x00\x01\x00\x00\x00\x00\x0c\xd0\x02\x00\x00\x08\x00\x00\x00\x05\x00\x00\x00White\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\x0b\x00\x00\x00Light Green\x00\xa0\x00\x00\x00\xff\x00\x00\x00\xa0\x00\x00\x00\x05\x00\x00\x00Green\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00P\x00\x00\x00\x04\x00\x00\x00Cyan\x00\x00\x00\x00\xff\x00\x00\x00\xc8\x00\x00\x00\x04\x00\x00\x00Blued\x00\x00\x00\x8c\x00\x00\x00\xff\x00\x00\x00\x08\x00\x00\x00Lavender\xe6\x00\x00\x00\x82\x00\x00\x00\xff\x00\x00\x00\x07\x00\x00\x00Magenta\x00\xff\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\r\x00\x00\x00Light Magenta\x00\x00\x00\xff\x00\x00\x00\xb4\x00\x00\x00\xd2\x00\x00\x00F\x00\x00\x00\x00\x00\x00\x00\x7f\x00\x00\x00\x01\x00\x00\x00", - decoded={ - "controller": { - "controller_id": 100, - "configuration": { - "body_type": { - 0: {"min_setpoint": 40, "max_setpoint": 104}, - 1: {"min_setpoint": 40, "max_setpoint": 104}, - }, - "is_celsius": {"name": "Is Celsius", "value": 0}, - "controller_type": 13, - "hardware_type": 6, - "controller_data": 0, - "generic_circuit_name": "Water Features", - "circuit_count": 7, - "color_count": 8, - "color": [ - {"name": "White", "value": (255, 255, 255)}, - {"name": "Light Green", "value": (160, 255, 160)}, - {"name": "Green", "value": (0, 255, 80)}, - {"name": "Cyan", "value": (0, 255, 200)}, - {"name": "Blue", "value": (100, 140, 255)}, - {"name": "Lavender", "value": (230, 130, 255)}, - {"name": "Magenta", "value": (255, 0, 128)}, - {"name": "Light Magenta", "value": (255, 180, 210)}, - ], - "interface_tab_flags": 127, - "show_alarms": 1, - }, - "model": {"name": "Model", "value": "EasyTouch2 PSL4"}, - "equipment": { - "flags": 8223, - "list": [ - "SOLAR", - "SOLAR_AS_HEAT_PUMP", - "CHLORINATOR", - "INTELLIBRITE", - "INTELLIFLO_0", - "HAS_COOLING", - ], - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "name": "Bubblers", - "configuration": { - "name_index": 103, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_66": 0, - "unknown_at_offset_67": 0, - }, - "function": 1, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 1, - }, - 501: { - "circuit_id": 501, - "name": "Lights", - "configuration": { - "name_index": 46, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_94": 0, - "unknown_at_offset_95": 0, - }, - "function": 16, - "interface": 4, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 2, - }, - 502: { - "circuit_id": 502, - "name": "Regular", - "configuration": { - "name_index": 101, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_122": 0, - "unknown_at_offset_123": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 3, - }, - 503: { - "circuit_id": 503, - "name": "High", - "configuration": { - "name_index": 104, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_146": 0, - "unknown_at_offset_147": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 4, - }, - 505: { - "circuit_id": 505, - "name": "Pool", - "configuration": { - "name_index": 60, - "flags": 0, - "default_runtime": 735, - "unknown_at_offset_170": 0, - "unknown_at_offset_171": 0, - }, - "function": 2, - "interface": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 6, - }, - 510: { - "circuit_id": 510, - "name": "Low", - "configuration": { - "name_index": 105, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_194": 0, - "unknown_at_offset_195": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 11, - }, - 511: { - "circuit_id": 511, - "name": "Variable", - "configuration": { - "name_index": 102, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_222": 0, - "unknown_at_offset_223": 0, - }, - "function": 0, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 12, - }, - }, - "pump": { - 0: {"data": 70}, - 1: {"data": 0}, - 2: {"data": 0}, - 3: {"data": 0}, - 4: {"data": 0}, - 5: {"data": 0}, - 6: {"data": 0}, - 7: {"data": 0}, - }, - }, - ), - status=ScreenLogicResponseSet( - raw=b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Q\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00U\x00\x00\x00\x00\x00\x00\x00X\x00\x00\x00h\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00V\x00\x00\x00\x00\x00\x00\x00<\x00\x00\x00Q\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\xf4\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf5\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf7\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf9\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xfe\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00L\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "controller": { - "sensor": { - "state": { - "name": "Controller State", - "value": 1, - "device_type": "enum", - "enum_options": ["Unknown", "Ready", "Sync", "Service"], - }, - "freeze_mode": {"name": "Freeze Mode", "value": 0}, - "pool_delay": {"name": "Pool Delay", "value": 0}, - "spa_delay": {"name": "Spa Delay", "value": 0}, - "cleaner_delay": {"name": "Cleaner Delay", "value": 0}, - "air_temperature": { - "name": "Air Temperature", - "value": 81, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "ph": { - "name": "pH", - "value": 0.0, - "unit": "pH", - "state_type": "measurement", - }, - "orp": { - "name": "ORP", - "value": 0, - "unit": "mV", - "state_type": "measurement", - }, - "saturation": { - "name": "Saturation Index", - "value": 0.0, - "unit": "lsi", - "state_type": "measurement", - }, - "salt_ppm": { - "name": "Salt", - "value": 3800, - "unit": "ppm", - "state_type": "measurement", - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 0, - "state_type": "measurement", - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 0, - "state_type": "measurement", - }, - "active_alert": { - "name": "Active Alert", - "value": 0, - "device_type": "alarm", - }, - }, - "configuration": { - "remotes": 0, - "unknown_at_offset_09": 0, - "unknown_at_offset_10": 0, - "unknown_at_offset_11": 0, - }, - }, - "body": { - 0: { - "body_type": 0, - "name": "Pool", - "last_temperature": { - "name": "Last Pool Temperature", - "value": 85, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Pool Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Pool Heat Set Point", - "value": 88, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Pool Cool Set Point", - "value": 104, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Pool Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - 1: { - "body_type": 1, - "name": "Spa", - "last_temperature": { - "name": "Last Spa Temperature", - "value": 86, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Spa Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Spa Heat Set Point", - "value": 60, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Spa Cool Set Point", - "value": 81, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Spa Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 501: { - "circuit_id": 501, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 502: { - "circuit_id": 502, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 503: { - "circuit_id": 503, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 505: { - "circuit_id": 505, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 510: { - "circuit_id": 510, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 511: { - "circuit_id": 511, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - }, - }, - ), - scg=ScreenLogicResponseSet( - raw=b"e\x00\x00\x00\x81\x00\x00\x00(\x00\x00\x002\x00\x00\x00N\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "scg": { - "scg_present": 101, - "sensor": { - "state": {"name": "Chlorinator", "value": 1}, - "salt_ppm": { - "name": "Chlorinator Salt", - "value": 3900, - "unit": "ppm", - "state_type": "measurement", - }, - }, - "configuration": { - "pool_setpoint": { - "name": "Pool Chlorinator Setpoint", - "value": 40, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 1, - "body_type": 0, - }, - "spa_setpoint": { - "name": "Spa Chlorinator Setpoint", - "value": 50, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 1, - "body_type": 1, - }, - "super_chlor_timer": { - "name": "Super Chlorination Timer", - "value": 0, - "unit": "hr", - "min_setpoint": 1, - "max_setpoint": 72, - "step": 1, - }, - }, - "flags": 128, - } - }, - ), - pumps=[ - ScreenLogicResponseSet( - raw=b"\x03\x00\x00\x00\x01\x00\x00\x001\x00\x00\x00\xb6\x03\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\xff\x00\x00\x00\x06\x00\x00\x00\xe2\x04\x00\x00\x01\x00\x00\x00\x0c\x00\x00\x00\xaa\x05\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb6\x03\x00\x00\x01\x00\x00\x00\x0b\x00\x00\x00\xb6\x03\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\xe2\x04\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00`\t\x00\x00\x01\x00\x00\x00\x80\x00\x00\x00x\x05\x00\x00\x01\x00\x00\x00\x84\x00\x00\x00X\x02\x00\x00\x01\x00\x00\x00", - decoded={ - "pump": { - 0: { - "type": 3, - "state": {"name": "Default Pump", "value": 1}, - "watts_now": { - "name": "Default Pump Watts Now", - "value": 49, - "unit": "W", - "device_type": "power", - "state_type": "measurement", - }, - "rpm_now": { - "name": "Default Pump RPM Now", - "value": 950, - "unit": "rpm", - "state_type": "measurement", - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Default Pump GPM Now", - "value": 10, - "unit": "gpm", - "state_type": "measurement", - }, - "unknown_at_offset_24": 255, - "preset": { - 0: {"device_id": 6, "setpoint": 1250, "is_rpm": 1}, - 1: {"device_id": 12, "setpoint": 1450, "is_rpm": 1}, - 2: {"device_id": 1, "setpoint": 950, "is_rpm": 1}, - 3: {"device_id": 11, "setpoint": 950, "is_rpm": 1}, - 4: {"device_id": 3, "setpoint": 1250, "is_rpm": 1}, - 5: {"device_id": 4, "setpoint": 2400, "is_rpm": 1}, - 6: {"device_id": 128, "setpoint": 1400, "is_rpm": 1}, - 7: {"device_id": 132, "setpoint": 600, "is_rpm": 1}, - }, - } - } - }, - ) - ], - ), - ScreenLogicResponseCollection( - {}, - config=ScreenLogicResponseSet( - raw=b"d\x00\x00\x00(h(h\x00\r\x06\x00\x1f \x00\x00\x0e\x00\x00\x00Water Features\x00\x00\x07\x00\x00\x00\xf4\x01\x00\x00\x08\x00\x00\x00Bubblersg\x01\x01\x00\x00\x00\x00\x01\xd0\x02\x00\x00\xf5\x01\x00\x00\x06\x00\x00\x00Lights\x00\x00.\x10\x04\x00\x00\x00\x00\x02\xd0\x02\x00\x00\xf6\x01\x00\x00\x07\x00\x00\x00Regular\x00e\x00\x02\x00\x00\x00\x00\x03\xd0\x02\x00\x00\xf7\x01\x00\x00\x04\x00\x00\x00Highh\x00\x02\x00\x00\x00\x00\x04\xd0\x02\x00\x00\xf9\x01\x00\x00\x04\x00\x00\x00Pool<\x02\x00\x00\x00\x00\x00\x06\xdf\x02\x00\x00\xfe\x01\x00\x00\x03\x00\x00\x00Low\x00i\x00\x02\x00\x00\x00\x00\x0b\xd0\x02\x00\x00\xff\x01\x00\x00\x08\x00\x00\x00Variablef\x00\x01\x00\x00\x00\x00\x0c\xd0\x02\x00\x00\x08\x00\x00\x00\x05\x00\x00\x00White\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\x0b\x00\x00\x00Light Green\x00\xa0\x00\x00\x00\xff\x00\x00\x00\xa0\x00\x00\x00\x05\x00\x00\x00Green\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00P\x00\x00\x00\x04\x00\x00\x00Cyan\x00\x00\x00\x00\xff\x00\x00\x00\xc8\x00\x00\x00\x04\x00\x00\x00Blued\x00\x00\x00\x8c\x00\x00\x00\xff\x00\x00\x00\x08\x00\x00\x00Lavender\xe6\x00\x00\x00\x82\x00\x00\x00\xff\x00\x00\x00\x07\x00\x00\x00Magenta\x00\xff\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\r\x00\x00\x00Light Magenta\x00\x00\x00\xff\x00\x00\x00\xb4\x00\x00\x00\xd2\x00\x00\x00F\x00\x00\x00\x00\x00\x00\x00\x7f\x00\x00\x00\x01\x00\x00\x00", - decoded={ - "controller": { - "controller_id": 100, - "configuration": { - "body_type": { - 0: {"min_setpoint": 40, "max_setpoint": 104}, - 1: {"min_setpoint": 40, "max_setpoint": 104}, - }, - "is_celsius": {"name": "Is Celsius", "value": 0}, - "controller_type": 13, - "hardware_type": 6, - "controller_data": 0, - "generic_circuit_name": "Water Features", - "circuit_count": 7, - "color_count": 8, - "color": [ - {"name": "White", "value": (255, 255, 255)}, - {"name": "Light Green", "value": (160, 255, 160)}, - {"name": "Green", "value": (0, 255, 80)}, - {"name": "Cyan", "value": (0, 255, 200)}, - {"name": "Blue", "value": (100, 140, 255)}, - {"name": "Lavender", "value": (230, 130, 255)}, - {"name": "Magenta", "value": (255, 0, 128)}, - {"name": "Light Magenta", "value": (255, 180, 210)}, - ], - "interface_tab_flags": 127, - "show_alarms": 1, - }, - "model": {"name": "Model", "value": "EasyTouch2 PSL4"}, - "equipment": { - "flags": 8223, - "list": [ - "SOLAR", - "SOLAR_AS_HEAT_PUMP", - "CHLORINATOR", - "INTELLIBRITE", - "INTELLIFLO_0", - "HAS_COOLING", - ], - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "name": "Bubblers", - "configuration": { - "name_index": 103, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_66": 0, - "unknown_at_offset_67": 0, - }, - "function": 1, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 1, - }, - 501: { - "circuit_id": 501, - "name": "Lights", - "configuration": { - "name_index": 46, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_94": 0, - "unknown_at_offset_95": 0, - }, - "function": 16, - "interface": 4, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 2, - }, - 502: { - "circuit_id": 502, - "name": "Regular", - "configuration": { - "name_index": 101, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_122": 0, - "unknown_at_offset_123": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 3, - }, - 503: { - "circuit_id": 503, - "name": "High", - "configuration": { - "name_index": 104, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_146": 0, - "unknown_at_offset_147": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 4, - }, - 505: { - "circuit_id": 505, - "name": "Pool", - "configuration": { - "name_index": 60, - "flags": 0, - "default_runtime": 735, - "unknown_at_offset_170": 0, - "unknown_at_offset_171": 0, - }, - "function": 2, - "interface": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 6, - }, - 510: { - "circuit_id": 510, - "name": "Low", - "configuration": { - "name_index": 105, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_194": 0, - "unknown_at_offset_195": 0, - }, - "function": 0, - "interface": 2, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 11, - }, - 511: { - "circuit_id": 511, - "name": "Variable", - "configuration": { - "name_index": 102, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_222": 0, - "unknown_at_offset_223": 0, - }, - "function": 0, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "device_id": 12, - }, - }, - "pump": { - 0: {"data": 70}, - 1: {"data": 0}, - 2: {"data": 0}, - 3: {"data": 0}, - 4: {"data": 0}, - 5: {"data": 0}, - 6: {"data": 0}, - 7: {"data": 0}, - }, - }, - ), - status=ScreenLogicResponseSet( - raw=b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Q\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00U\x00\x00\x00\x00\x00\x00\x00X\x00\x00\x00h\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00V\x00\x00\x00\x00\x00\x00\x00<\x00\x00\x00Q\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\xf4\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf5\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf7\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf9\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xfe\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00L\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "controller": { - "sensor": { - "state": { - "name": "Controller State", - "value": 1, - "device_type": "enum", - "enum_options": ["Unknown", "Ready", "Sync", "Service"], - }, - "freeze_mode": {"name": "Freeze Mode", "value": 0}, - "pool_delay": {"name": "Pool Delay", "value": 0}, - "spa_delay": {"name": "Spa Delay", "value": 0}, - "cleaner_delay": {"name": "Cleaner Delay", "value": 0}, - "air_temperature": { - "name": "Air Temperature", - "value": 81, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "ph": { - "name": "pH", - "value": 0.0, - "unit": "pH", - "state_type": "measurement", - }, - "orp": { - "name": "ORP", - "value": 0, - "unit": "mV", - "state_type": "measurement", - }, - "saturation": { - "name": "Saturation Index", - "value": 0.0, - "unit": "lsi", - "state_type": "measurement", - }, - "salt_ppm": { - "name": "Salt", - "value": 3800, - "unit": "ppm", - "state_type": "measurement", - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 0, - "state_type": "measurement", - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 0, - "state_type": "measurement", - }, - "active_alert": { - "name": "Active Alert", - "value": 0, - "device_type": "alarm", - }, - }, - "configuration": { - "remotes": 0, - "unknown_at_offset_09": 0, - "unknown_at_offset_10": 0, - "unknown_at_offset_11": 0, - }, - }, - "body": { - 0: { - "body_type": 0, - "name": "Pool", - "last_temperature": { - "name": "Last Pool Temperature", - "value": 85, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Pool Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Pool Heat Set Point", - "value": 88, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Pool Cool Set Point", - "value": 104, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Pool Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - 1: { - "body_type": 1, - "name": "Spa", - "last_temperature": { - "name": "Last Spa Temperature", - "value": 86, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement", - }, - "heat_state": { - "name": "Spa Heat", - "value": 0, - "device_type": "enum", - "enum_options": ["Off", "Solar", "Heater", "Both"], - }, - "heat_setpoint": { - "name": "Spa Heat Set Point", - "value": 60, - "unit": "°F", - "device_type": "temperature", - }, - "cool_setpoint": { - "name": "Spa Cool Set Point", - "value": 81, - "unit": "°F", - "device_type": "temperature", - }, - "heat_mode": { - "name": "Spa Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change", - ], - }, - }, - }, - "circuit": { - 500: { - "circuit_id": 500, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 501: { - "circuit_id": 501, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 502: { - "circuit_id": 502, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 503: { - "circuit_id": 503, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 505: { - "circuit_id": 505, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 510: { - "circuit_id": 510, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - 511: { - "circuit_id": 511, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0, - }, - "configuration": {"delay": 0}, - }, - }, - }, - ), - scg=ScreenLogicResponseSet( - raw=b"e\x00\x00\x00\x80\x00\x00\x00(\x00\x00\x002\x00\x00\x00L\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00", - decoded={ - "scg": { - "scg_present": 101, - "sensor": { - "state": {"name": "Chlorinator", "value": 0}, - "salt_ppm": { - "name": "Chlorinator Salt", - "value": 3800, - "unit": "ppm", - "state_type": "measurement", - }, - }, - "configuration": { - "pool_setpoint": { - "name": "Pool Chlorinator Setpoint", - "value": 40, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 1, - "body_type": 0, - }, - "spa_setpoint": { - "name": "Spa Chlorinator Setpoint", - "value": 50, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 1, - "body_type": 1, - }, - "super_chlor_timer": { - "name": "Super Chlorination Timer", - "value": 0, - "unit": "hr", - "min_setpoint": 1, - "max_setpoint": 72, - "step": 1, - }, - }, - "flags": 128, - } - }, - ), - pumps=[ - ScreenLogicResponseSet( - raw=b"\x03\x00\x00\x00\x01\x00\x00\x001\x00\x00\x00\xb6\x03\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\xff\x00\x00\x00\x06\x00\x00\x00\xe2\x04\x00\x00\x01\x00\x00\x00\x0c\x00\x00\x00\xaa\x05\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb6\x03\x00\x00\x01\x00\x00\x00\x0b\x00\x00\x00\xb6\x03\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\xe2\x04\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00`\t\x00\x00\x01\x00\x00\x00\x80\x00\x00\x00x\x05\x00\x00\x01\x00\x00\x00\x84\x00\x00\x00X\x02\x00\x00\x01\x00\x00\x00", - decoded={ - "pump": { - 0: { - "type": 3, - "state": {"name": "Default Pump", "value": 1}, - "watts_now": { - "name": "Default Pump Watts Now", - "value": 49, - "unit": "W", - "device_type": "power", - "state_type": "measurement", - }, - "rpm_now": { - "name": "Default Pump RPM Now", - "value": 950, - "unit": "rpm", - "state_type": "measurement", - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Default Pump GPM Now", - "value": 10, - "unit": "gpm", - "state_type": "measurement", - }, - "unknown_at_offset_24": 255, - "preset": { - 0: {"device_id": 6, "setpoint": 1250, "is_rpm": 1}, - 1: {"device_id": 12, "setpoint": 1450, "is_rpm": 1}, - 2: {"device_id": 1, "setpoint": 950, "is_rpm": 1}, - 3: {"device_id": 11, "setpoint": 950, "is_rpm": 1}, - 4: {"device_id": 3, "setpoint": 1250, "is_rpm": 1}, - 5: {"device_id": 4, "setpoint": 2400, "is_rpm": 1}, - 6: {"device_id": 128, "setpoint": 1400, "is_rpm": 1}, - 7: {"device_id": 132, "setpoint": 600, "is_rpm": 1}, - }, - } - } - }, - ) - ], - ), -] - -TESTING_DATA_COLLECTION = TEST_DATA_COLLECTIONS[0] diff --git a/tests/test_client.py b/tests/test_client.py index 7cb438b..73e9571 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,10 +7,10 @@ from screenlogicpy import ScreenLogicGateway from screenlogicpy.client import ClientManager from screenlogicpy.const.msg import CODE +from screenlogicpy.data import ScreenLogicResponseCollection from .const_data import ( FAKE_CONNECT_INFO, ) -from .data_sets import TESTING_DATA_COLLECTION as TDC @pytest.mark.asyncio() @@ -74,11 +74,11 @@ def callback(): @pytest.mark.asyncio() -async def test_notify(): +async def test_notify(response_collection: ScreenLogicResponseCollection): code1 = CODE.STATUS_CHANGED code2 = CODE.CHEMISTRY_CHANGED - status_response = TDC.status - chem_response = TDC.chemistry + status_response = response_collection.status + chem_response = response_collection.chemistry cb1_hit = False cb2_hit = False @@ -159,7 +159,6 @@ def callback3(): }, } async with MockProtocolAdapter: - await gateway.async_connect(**FAKE_CONNECT_INFO) assert gateway._protocol._callbacks == { From 9f4800e4cd2689eea92ec6714a6f2285999853a8 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:38:44 -0800 Subject: [PATCH 40/53] Add major/minor breakdowns of version data --- screenlogicpy/requests/chemistry.py | 2 ++ screenlogicpy/requests/gateway.py | 7 ++++-- ...-52-build-7360-rel_easytouch2-8_98360.json | 22 ++++++++++++++----- ...-52-build-7380-rel_easytouch2-8_32824.json | 20 ++++++++++++----- tests/test_data.py | 4 ++++ tests/test_gateway.py | 2 ++ 6 files changed, 43 insertions(+), 14 deletions(-) diff --git a/screenlogicpy/requests/chemistry.py b/screenlogicpy/requests/chemistry.py index 0425796..ccd7a4c 100644 --- a/screenlogicpy/requests/chemistry.py +++ b/screenlogicpy/requests/chemistry.py @@ -281,6 +281,8 @@ def decode_chemistry(buff: bytes, data: dict) -> None: intellichem[VALUE.FIRMWARE] = { ATTR.NAME: "IntelliChem Firmware", ATTR.VALUE: f"{vMajor}.{vMinor:03}", + ATTR.MAJOR: vMajor, + ATTR.MINOR: vMinor, } intellichem_balance: dict = intellichem.setdefault(GROUP.WATER_BALANCE, {}) diff --git a/screenlogicpy/requests/gateway.py b/screenlogicpy/requests/gateway.py index ff07c77..59a6fb7 100644 --- a/screenlogicpy/requests/gateway.py +++ b/screenlogicpy/requests/gateway.py @@ -17,8 +17,11 @@ async def async_request_gateway_version( def decode_version(buff: bytes, data: dict): adapter: dict = data.setdefault(DEVICE.ADAPTER, {}) - + firmware = decodeMessageString(buff) + fw_parts = firmware.split() adapter[VALUE.FIRMWARE] = { ATTR.NAME: "Protocol Adapter Firmware", - ATTR.VALUE: decodeMessageString(buff), + ATTR.VALUE: firmware, + ATTR.MAJOR: float(fw_parts[1]), + ATTR.MINOR: float(fw_parts[3]), } diff --git a/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json index 6765f3c..681c6e3 100644 --- a/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json +++ b/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json @@ -3,7 +3,9 @@ "adapter": { "firmware": { "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 736.0 Rel" + "value": "POOL: 5.2 Build 736.0 Rel", + "major": 5.2, + "minor": 736.0 } }, "controller": { @@ -156,6 +158,7 @@ }, "date_time":{ "timestamp": 1699835040.0, + "timestamp_host": 1700517812.0, "auto_dst":{ "name": "Automatic Daylight Saving Time", "value": 1 @@ -834,8 +837,10 @@ }, "firmware": { "name": "IntelliChem Firmware", - "value": "1.060" - }, + "value": "1.060", + "major": 1, + "minor": 60 + }, "water_balance": { "flags": 0, "corrosive": { @@ -911,8 +916,10 @@ "adapter": { "firmware": { "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 736.0 Rel" - } + "value": "POOL: 5.2 Build 736.0 Rel", + "major": 5.2, + "minor": 736.0 + } } } }, @@ -1897,7 +1904,9 @@ }, "firmware": { "name": "IntelliChem Firmware", - "value": "1.060" + "value": "1.060", + "major": 1, + "minor": 60 }, "water_balance": { "flags": 0, @@ -1982,6 +1991,7 @@ }, "decoded":{ "timestamp":1699835040.0, + "timestamp_host": 1700517812.0, "auto_dst":{ "name": "Automatic Daylight Saving Time", "value": 1 diff --git a/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json b/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json index 2b9a6eb..caa124f 100644 --- a/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json +++ b/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json @@ -3,7 +3,9 @@ "adapter": { "firmware": { "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 738.0 Rel" + "value": "POOL: 5.2 Build 738.0 Rel", + "major": 5.2, + "minor": 738.0 } }, "controller": { @@ -889,8 +891,10 @@ }, "firmware": { "name": "IntelliChem Firmware", - "value": "1.060" - }, + "value": "1.060", + "major": 1, + "minor": 60 + }, "water_balance": { "flags": 0, "corrosive": { @@ -966,8 +970,10 @@ "adapter": { "firmware": { "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 738.0 Rel" - } + "value": "POOL: 5.2 Build 738.0 Rel", + "major": 5.2, + "minor": 738.0 + } } } }, @@ -2006,7 +2012,9 @@ }, "firmware": { "name": "IntelliChem Firmware", - "value": "1.060" + "value": "1.060", + "major": 1, + "minor": 60 }, "water_balance": { "flags": 0, diff --git a/tests/test_data.py b/tests/test_data.py index f9268c4..244bff3 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -44,6 +44,8 @@ def test_validate_complete(response_collection: ScreenLogicResponseCollection): "firmware": { "name": "Protocol Adapter Firmware", "value": "POOL: 5.2 Build 736.0 Rel", + "major": 5.2, + "minor": 736.0, } } } @@ -133,6 +135,8 @@ def test_validate_complete(response_collection: ScreenLogicResponseCollection): "firmware": { "name": "Protocol Adapter Firmware", "value": "POOL: 5.2 Build 736.0 Rel", + "major": 5.2, + "minor": 736.0, } }, "pump": { diff --git a/tests/test_gateway.py b/tests/test_gateway.py index b85547d..26c8f57 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -200,6 +200,8 @@ async def test_gateway_get_scg( "firmware": { "name": "Protocol Adapter Firmware", "value": "POOL: 5.2 Build 738.0 Rel", + "major": 5.2, + "minor": 738.0, } }, ), From 97315c189b8ab2e4dac02c89a0c1a341cec78d99 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:39:25 -0800 Subject: [PATCH 41/53] Add getAdapterVersion utility function --- screenlogicpy/requests/utility.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/screenlogicpy/requests/utility.py b/screenlogicpy/requests/utility.py index 5a8be87..dcdeef6 100644 --- a/screenlogicpy/requests/utility.py +++ b/screenlogicpy/requests/utility.py @@ -150,3 +150,7 @@ def getTemperatureUnit(data: dict): .get(ATTR.VALUE) else UNIT.FAHRENHEIT ) + + +def getAdapterVersion(data: dict): + return data.get(DEVICE.ADAPTER, {}).get(VALUE.FIRMWARE, {}).get(ATTR.MINOR) From 023cdb19cc348de740250c85345cdf2cfa33b3c2 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:42:22 -0800 Subject: [PATCH 42/53] Mask equipment flags for adapter firmware <738 --- screenlogicpy/device_const/system.py | 3 ++- screenlogicpy/requests/config.py | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/screenlogicpy/device_const/system.py b/screenlogicpy/device_const/system.py index 0eb74a6..f8ce04e 100644 --- a/screenlogicpy/device_const/system.py +++ b/screenlogicpy/device_const/system.py @@ -49,7 +49,8 @@ class CONTROLLER_STATE(SLIntEnum): SERVICE = 3 -EQUIPMENT_MASK = 0x1FFFF +# Only lower half of equipment int is valid on build 736.0 +EQUIPMENT_MASK_736 = 0xFFFF class EQUIPMENT_FLAG(IntFlag): diff --git a/screenlogicpy/requests/config.py b/screenlogicpy/requests/config.py index a0877a0..ac5eb64 100644 --- a/screenlogicpy/requests/config.py +++ b/screenlogicpy/requests/config.py @@ -3,10 +3,10 @@ from ..const.msg import CODE from ..const.data import ATTR, DEVICE, GROUP, VALUE, UNKNOWN -from ..device_const.system import CONTROLLER, EQUIPMENT_FLAG, EQUIPMENT_MASK +from ..device_const.system import CONTROLLER, EQUIPMENT_FLAG, EQUIPMENT_MASK_736 from .protocol import ScreenLogicProtocol from .request import async_make_request -from .utility import getSome, getString +from .utility import getAdapterVersion, getSome, getString async def async_request_pool_config( @@ -62,7 +62,10 @@ def decode_pool_config(buff: bytes, data: dict) -> dict: equipFlags, offset = getSome("I", buff, offset) # Include only known flags. - controller_equipment[VALUE.FLAGS] = equipFlags & EQUIPMENT_MASK + minor = getAdapterVersion(data) + equipFlags = equipFlags & EQUIPMENT_MASK_736 if minor <= 736 else equipFlags + + controller_equipment[VALUE.FLAGS] = equipFlags controller_equipment[VALUE.LIST] = [ # What's needed? Friendly name or FLAG name? @@ -79,7 +82,6 @@ def decode_pool_config(buff: bytes, data: dict) -> dict: circuit: dict = data.setdefault(DEVICE.CIRCUIT, {}) for i in range(circuitCount): - circuit_id, offset = getSome("i", buff, offset) circuit_indexed: dict = circuit.setdefault(circuit_id, {}) From 3a3b9ad0318a448816f51a959276083127042c19 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:42:49 -0800 Subject: [PATCH 43/53] Patch adapter version in decoding tests --- tests/test_cli.py | 4 ++++ tests/test_decode.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 7bb0b3e..b78ac47 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,6 +6,7 @@ from screenlogicpy import ScreenLogicGateway from screenlogicpy.cli import cli from screenlogicpy.data import ScreenLogicResponseCollection +from screenlogicpy.requests.utility import getAdapterVersion from .conftest import stub_async_connect from .const_data import ( @@ -31,6 +32,9 @@ async def PatchedGateway( _async_connected_request=DEFAULT, ) as gateway, patch( "screenlogicpy.cli.async_discover", return_value=[FAKE_CONNECT_INFO] + ), patch( + "screenlogicpy.requests.config.getAdapterVersion", + return_value=getAdapterVersion(response_collection.decoded_complete), ): yield gateway diff --git a/tests/test_decode.py b/tests/test_decode.py index 59d8c9f..31134a5 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1,10 +1,12 @@ +from unittest.mock import patch + from screenlogicpy.data import ScreenLogicResponseCollection from screenlogicpy.requests.config import decode_pool_config from screenlogicpy.requests.status import decode_pool_status from screenlogicpy.requests.pump import decode_pump_status from screenlogicpy.requests.chemistry import decode_chemistry from screenlogicpy.requests.scg import decode_scg_config -from screenlogicpy.requests.utility import makeMessage, takeMessages +from screenlogicpy.requests.utility import getAdapterVersion, makeMessage, takeMessages from screenlogicpy.requests.gateway import decode_version @@ -17,7 +19,11 @@ def test_decode_version(response_collection: ScreenLogicResponseCollection): def test_decode_config(response_collection: ScreenLogicResponseCollection): data = {} - decode_pool_config(response_collection.config.raw, data) + with patch( + "screenlogicpy.requests.config.getAdapterVersion", + return_value=getAdapterVersion(response_collection.decoded_complete), + ): + decode_pool_config(response_collection.config.raw, data) assert data == response_collection.config.decoded From 64ebdc4b71fae86e2de62aa3f67d85cf27024ddc Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sat, 25 Nov 2023 15:08:57 -0800 Subject: [PATCH 44/53] Rename test data files --- screenlogicpy/cli.py | 2 +- tests/conftest.py | 2 +- ...=> slpy-0100_pool-52-build-7360-rel_easytouch2-8_98360.json} | 0 ...=> slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json} | 0 tests/test_cli.py | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) rename tests/data/{slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json => slpy-0100_pool-52-build-7360-rel_easytouch2-8_98360.json} (100%) rename tests/data/{slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json => slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json} (100%) diff --git a/screenlogicpy/cli.py b/screenlogicpy/cli.py index 39a56ef..597f085 100644 --- a/screenlogicpy/cli.py +++ b/screenlogicpy/cli.py @@ -302,7 +302,7 @@ async def async_export_data_collection(): pa_ver = file_format(gateway.version) model = file_format(gateway.controller_model) equip = gateway.equipment_flags.value - filename = f"slpy{sl_ver}_{pa_ver}_{model}_{equip}.json" + filename = f"slpy-{sl_ver}_{pa_ver}_{model}_{equip}.json" response_collection = build_response_collection( gateway.get_debug(), gateway.get_data() ) diff --git a/tests/conftest.py b/tests/conftest.py index b32e58b..df58457 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,7 +29,7 @@ ) -DEFAULT_RESPONSE = "slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json" +DEFAULT_RESPONSE = "slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json" def load_response_collection(filenames: list[str] | None = None): diff --git a/tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/slpy-0100_pool-52-build-7360-rel_easytouch2-8_98360.json similarity index 100% rename from tests/data/slpy0100_pool-52-build-7360-rel_easytouch2-8_98360.json rename to tests/data/slpy-0100_pool-52-build-7360-rel_easytouch2-8_98360.json diff --git a/tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json b/tests/data/slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json similarity index 100% rename from tests/data/slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json rename to tests/data/slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json diff --git a/tests/test_cli.py b/tests/test_cli.py index b78ac47..c583925 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -364,7 +364,7 @@ def write(data): ): assert await cli(arguments.split()) == return_code mo.assert_called_with( - "slpy0100_pool-52-build-7380-rel_easytouch2-8_32824.json", + "slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json", "w", encoding="utf-8", ) From 05abe9709ef213ad3a6a502844fd3fc4a7995c2c Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sat, 25 Nov 2023 17:34:28 -0800 Subject: [PATCH 45/53] Remove dependency on getAdapterVersion --- screenlogicpy/requests/config.py | 4 ++-- tests/test_cli.py | 4 ---- tests/test_decode.py | 8 ++------ 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/screenlogicpy/requests/config.py b/screenlogicpy/requests/config.py index ac5eb64..1cd5c5d 100644 --- a/screenlogicpy/requests/config.py +++ b/screenlogicpy/requests/config.py @@ -62,8 +62,8 @@ def decode_pool_config(buff: bytes, data: dict) -> dict: equipFlags, offset = getSome("I", buff, offset) # Include only known flags. - minor = getAdapterVersion(data) - equipFlags = equipFlags & EQUIPMENT_MASK_736 if minor <= 736 else equipFlags + if (minor := getAdapterVersion(data)) is not None: + equipFlags = equipFlags & EQUIPMENT_MASK_736 if minor <= 736 else equipFlags controller_equipment[VALUE.FLAGS] = equipFlags diff --git a/tests/test_cli.py b/tests/test_cli.py index c583925..d45de5a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,7 +6,6 @@ from screenlogicpy import ScreenLogicGateway from screenlogicpy.cli import cli from screenlogicpy.data import ScreenLogicResponseCollection -from screenlogicpy.requests.utility import getAdapterVersion from .conftest import stub_async_connect from .const_data import ( @@ -32,9 +31,6 @@ async def PatchedGateway( _async_connected_request=DEFAULT, ) as gateway, patch( "screenlogicpy.cli.async_discover", return_value=[FAKE_CONNECT_INFO] - ), patch( - "screenlogicpy.requests.config.getAdapterVersion", - return_value=getAdapterVersion(response_collection.decoded_complete), ): yield gateway diff --git a/tests/test_decode.py b/tests/test_decode.py index 31134a5..3c589f0 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -6,7 +6,7 @@ from screenlogicpy.requests.pump import decode_pump_status from screenlogicpy.requests.chemistry import decode_chemistry from screenlogicpy.requests.scg import decode_scg_config -from screenlogicpy.requests.utility import getAdapterVersion, makeMessage, takeMessages +from screenlogicpy.requests.utility import makeMessage, takeMessages from screenlogicpy.requests.gateway import decode_version @@ -19,11 +19,7 @@ def test_decode_version(response_collection: ScreenLogicResponseCollection): def test_decode_config(response_collection: ScreenLogicResponseCollection): data = {} - with patch( - "screenlogicpy.requests.config.getAdapterVersion", - return_value=getAdapterVersion(response_collection.decoded_complete), - ): - decode_pool_config(response_collection.config.raw, data) + decode_pool_config(response_collection.config.raw, data) assert data == response_collection.config.decoded From 7372ae6f916125f489ee224f98569f8ca8e629d3 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Sat, 25 Nov 2023 17:46:38 -0800 Subject: [PATCH 46/53] Update equipment flags --- ...py-0100_pool-52-build-7360-rel_easytouch2-8_32824.json} | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) rename tests/data/{slpy-0100_pool-52-build-7360-rel_easytouch2-8_98360.json => slpy-0100_pool-52-build-7360-rel_easytouch2-8_32824.json} (99%) diff --git a/tests/data/slpy-0100_pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/slpy-0100_pool-52-build-7360-rel_easytouch2-8_32824.json similarity index 99% rename from tests/data/slpy-0100_pool-52-build-7360-rel_easytouch2-8_98360.json rename to tests/data/slpy-0100_pool-52-build-7360-rel_easytouch2-8_32824.json index 681c6e3..682a2cc 100644 --- a/tests/data/slpy-0100_pool-52-build-7360-rel_easytouch2-8_98360.json +++ b/tests/data/slpy-0100_pool-52-build-7360-rel_easytouch2-8_32824.json @@ -77,13 +77,12 @@ "value": "EasyTouch2 8" }, "equipment": { - "flags": 98360, + "flags": 32824, "list": [ "INTELLIBRITE", "INTELLIFLO_0", "INTELLIFLO_1", - "INTELLICHEM", - "HYBRID_HEATER" + "INTELLICHEM" ] }, "sensor": { @@ -994,7 +993,7 @@ "value": "EasyTouch2 8" }, "equipment": { - "flags": 98360, + "flags": 4294934584, "list": [ "INTELLIBRITE", "INTELLIFLO_0", From ea60d534b62601970ddadd39fd673159a33683d5 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 27 Nov 2023 08:32:39 -0800 Subject: [PATCH 47/53] Exclude source diagnostics exports --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f95b056..47324fc 100644 --- a/.gitignore +++ b/.gitignore @@ -123,7 +123,12 @@ dmypy.json # Pyre type checker .pyre/ + +# personal notes/notes.txt notes/circuit_names*.txt scratchpad/local_* -notes/8060_set_sys_data.txt \ No newline at end of file +notes/8060_set_sys_data.txt + +# source data +diagnostics/ \ No newline at end of file From f5717871727a66526cfc79a1e755fce627931c77 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 27 Nov 2023 08:33:28 -0800 Subject: [PATCH 48/53] Add additional suspected scg_state flags --- notes/scg_state_flags.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/notes/scg_state_flags.txt b/notes/scg_state_flags.txt index b2ba9cb..3365dea 100644 --- a/notes/scg_state_flags.txt +++ b/notes/scg_state_flags.txt @@ -1,3 +1,7 @@ Active 0001 +Very Low Salt ? +Low Salt ? +Good Salt ? +High Salt ? UNKNOWN 1000 0000 -IntelliChem in Control 1000 0000 0000 0000 \ No newline at end of file +IntelliChem in Control? 1000 0000 0000 0000 From 8cdcd789e9f0debeddda0301b5a1f44971c30a94 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 27 Nov 2023 08:37:03 -0800 Subject: [PATCH 49/53] Support multiple testing datasets --- tests/conftest.py | 20 +- ...2-build-7380-rel_intellitouch-i53s_60.json | 1768 ----------------- ...ol-52-build-7380-rel_easytouch2-8_28.json} | 1073 ++++++---- ...2-build-7380-rel_intellitouch-i73_56.json} | 140 +- tests/test_decode.py | 88 +- 5 files changed, 820 insertions(+), 2269 deletions(-) delete mode 100644 tests/data/pool-52-build-7380-rel_intellitouch-i53s_60.json rename tests/data/{pool-52-build-7360-rel_easytouch2-8_98360.json => slpy-0100_pool-52-build-7380-rel_easytouch2-8_28.json} (64%) rename tests/data/{pool-52-build-7380-rel_intellitouch-i73_56.json => slpy-0100_pool-52-build-7380-rel_intellitouch-i73_56.json} (92%) diff --git a/tests/conftest.py b/tests/conftest.py index df58457..940ff5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,13 @@ import asyncio from collections.abc import Callable -from dataclasses import asdict -import os +from glob import glob import pytest_asyncio import socket import struct from unittest.mock import DEFAULT, MagicMock, patch -from screenlogicpy import ScreenLogicGateway +from screenlogicpy import ScreenLogicGateway, __version__ as sl_version +from screenlogicpy.cli import file_format from screenlogicpy.data import ( ScreenLogicResponseCollection, deconstruct_response_collection, @@ -32,18 +32,18 @@ DEFAULT_RESPONSE = "slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json" -def load_response_collection(filenames: list[str] | None = None): - dir = "tests/data/" - files = filenames or os.listdir(dir) - resp_colls = [] +def load_response_collections(filenames: list[str] | None = None): + dir = "tests\\data\\" + files = filenames or glob(f"slpy-{file_format(sl_version)}*.json", root_dir=dir) + response_collections = [] for file in files: - resp_colls.append(import_response_collection(f"{dir}{file}")) - return resp_colls + response_collections.append((file, import_response_collection(f"{dir}{file}"))) + return response_collections @pytest_asyncio.fixture() async def response_collection(): - return load_response_collection([DEFAULT_RESPONSE])[0] + return load_response_collections([DEFAULT_RESPONSE])[0][1] @pytest_asyncio.fixture() diff --git a/tests/data/pool-52-build-7380-rel_intellitouch-i53s_60.json b/tests/data/pool-52-build-7380-rel_intellitouch-i53s_60.json deleted file mode 100644 index 551f85a..0000000 --- a/tests/data/pool-52-build-7380-rel_intellitouch-i53s_60.json +++ /dev/null @@ -1,1768 +0,0 @@ -{ - "decoded_complete": { - "adapter": { - "firmware": { - "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 738.0 Rel" - } - }, - "controller": { - "controller_id": 100, - "configuration": { - "body_type": { - "0": { - "min_setpoint": 40, - "max_setpoint": 104 - }, - "1": { - "min_setpoint": 40, - "max_setpoint": 104 - } - }, - "is_celsius": { - "name": "Is Celsius", - "value": 0 - }, - "controller_type": 0, - "hardware_type": 0, - "controller_data": 32, - "generic_circuit_name": "Water Features", - "circuit_count": 6, - "color_count": 8, - "color": [ - { - "name": "White", - "value": [ - 255, - 255, - 255 - ] - }, - { - "name": "Light Green", - "value": [ - 160, - 255, - 160 - ] - }, - { - "name": "Green", - "value": [ - 0, - 255, - 80 - ] - }, - { - "name": "Cyan", - "value": [ - 0, - 255, - 200 - ] - }, - { - "name": "Blue", - "value": [ - 100, - 140, - 255 - ] - }, - { - "name": "Lavender", - "value": [ - 230, - 130, - 255 - ] - }, - { - "name": "Magenta", - "value": [ - 255, - 0, - 128 - ] - }, - { - "name": "Light Magenta", - "value": [ - 255, - 180, - 210 - ] - } - ], - "interface_tab_flags": 127, - "show_alarms": 1, - "remotes": 32, - "unknown_at_offset_09": 0, - "unknown_at_offset_10": 0, - "unknown_at_offset_11": 0 - }, - "model": { - "name": "Model", - "value": "IntelliTouch i5+3S" - }, - "equipment": { - "flags": 60, - "list": [ - "CHLORINATOR", - "INTELLIBRITE", - "INTELLIFLO_0", - "INTELLIFLO_1" - ] - }, - "sensor": { - "state": { - "name": "Controller State", - "value": 1, - "device_type": "enum", - "enum_options": [ - "Unknown", - "Ready", - "Sync", - "Service" - ] - }, - "freeze_mode": { - "name": "Freeze Mode", - "value": 0 - }, - "pool_delay": { - "name": "Pool Delay", - "value": 0 - }, - "spa_delay": { - "name": "Spa Delay", - "value": 0 - }, - "cleaner_delay": { - "name": "Cleaner Delay", - "value": 0 - }, - "air_temperature": { - "name": "Air Temperature", - "value": 79, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement" - }, - "ph": { - "name": "pH", - "value": 0.0, - "unit": "pH", - "state_type": "measurement" - }, - "orp": { - "name": "ORP", - "value": 0, - "unit": "mV", - "state_type": "measurement" - }, - "saturation": { - "name": "Saturation Index", - "value": 0.0, - "unit": "lsi", - "state_type": "measurement" - }, - "salt_ppm": { - "name": "Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement" - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 0, - "state_type": "measurement" - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 0, - "state_type": "measurement" - }, - "active_alert": { - "name": "Active Alert", - "value": 1, - "device_type": "alarm" - } - } - }, - "circuit": { - "500": { - "circuit_id": 500, - "name": "Spa", - "configuration": { - "name_index": 71, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_62": 0, - "unknown_at_offset_63": 0, - "delay": 0 - }, - "function": 1, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 1, - "value": 0 - }, - "501": { - "circuit_id": 501, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_94": 0, - "unknown_at_offset_95": 0, - "delay": 0 - }, - "function": 16, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 2, - "value": 0 - }, - "502": { - "circuit_id": 502, - "name": "Spa Light", - "configuration": { - "name_index": 73, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_126": 0, - "unknown_at_offset_127": 0, - "delay": 0 - }, - "function": 16, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 3, - "value": 0 - }, - "503": { - "circuit_id": 503, - "name": "Cleaner", - "configuration": { - "name_index": 21, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_154": 0, - "unknown_at_offset_155": 0, - "delay": 0 - }, - "function": 5, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 4, - "value": 0 - }, - "504": { - "circuit_id": 504, - "name": "Jets", - "configuration": { - "name_index": 45, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_178": 0, - "unknown_at_offset_179": 0, - "delay": 0 - }, - "function": 0, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 5, - "value": 0 - }, - "505": { - "circuit_id": 505, - "name": "Pool", - "configuration": { - "name_index": 60, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_202": 0, - "unknown_at_offset_203": 0, - "delay": 0 - }, - "function": 2, - "interface": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 6, - "value": 1 - } - }, - "pump": { - "0": { - "data": 134, - "type": 2, - "state": { - "name": "Pool Pump", - "value": 1 - }, - "watts_now": { - "name": "Pool Pump Watts Now", - "value": 68, - "unit": "W", - "device_type": "power", - "state_type": "measurement" - }, - "rpm_now": { - "name": "Pool Pump RPM Now", - "value": 1100, - "unit": "rpm", - "state_type": "measurement" - }, - "unknown_at_offset_16": 0, - "unknown_at_offset_24": 255, - "preset": { - "0": { - "device_id": 6, - "setpoint": 1100, - "is_rpm": 1 - }, - "1": { - "device_id": 2, - "setpoint": 2300, - "is_rpm": 1 - }, - "2": { - "device_id": 1, - "setpoint": 2070, - "is_rpm": 1 - }, - "3": { - "device_id": 4, - "setpoint": 2440, - "is_rpm": 1 - }, - "4": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "5": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "6": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "7": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - } - } - }, - "1": { - "data": 133, - "type": 2, - "state": { - "name": "Jets Pump", - "value": 0 - }, - "watts_now": { - "name": "Jets Pump Watts Now", - "value": 0, - "unit": "W", - "device_type": "power", - "state_type": "measurement" - }, - "rpm_now": { - "name": "Jets Pump RPM Now", - "value": 0, - "unit": "rpm", - "state_type": "measurement" - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Jets Pump GPM Now", - "value": 0, - "unit": "gpm", - "state_type": "measurement" - }, - "unknown_at_offset_24": 255, - "preset": { - "0": { - "device_id": 5, - "setpoint": 2800, - "is_rpm": 1 - }, - "1": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "2": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "3": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "4": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "5": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "6": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "7": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - } - } - }, - "2": { - "data": 0 - }, - "3": { - "data": 0 - }, - "4": { - "data": 0 - }, - "5": { - "data": 0 - }, - "6": { - "data": 0 - }, - "7": { - "data": 0 - } - }, - "body": { - "0": { - "body_type": 0, - "min_setpoint": 40, - "max_setpoint": 104, - "name": "Pool", - "last_temperature": { - "name": "Last Pool Temperature", - "value": 80, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement" - }, - "heat_state": { - "name": "Pool Heat", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Heater", - "Both" - ] - }, - "heat_setpoint": { - "name": "Pool Heat Set Point", - "value": 84, - "unit": "°F", - "device_type": "temperature" - }, - "cool_setpoint": { - "name": "Pool Cool Set Point", - "value": 100, - "unit": "°F", - "device_type": "temperature" - }, - "heat_mode": { - "name": "Pool Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change" - ] - } - }, - "1": { - "body_type": 1, - "min_setpoint": 40, - "max_setpoint": 104, - "name": "Spa", - "last_temperature": { - "name": "Last Spa Temperature", - "value": 83, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement" - }, - "heat_state": { - "name": "Spa Heat", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Heater", - "Both" - ] - }, - "heat_setpoint": { - "name": "Spa Heat Set Point", - "value": 103, - "unit": "°F", - "device_type": "temperature" - }, - "cool_setpoint": { - "name": "Spa Cool Set Point", - "value": 79, - "unit": "°F", - "device_type": "temperature" - }, - "heat_mode": { - "name": "Spa Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change" - ] - } - } - }, - "intellichem": { - "unknown_at_offset_00": 42, - "unknown_at_offset_04": 0, - "sensor": { - "ph_now": { - "name": "pH Now", - "value": 0.0, - "unit": "pH", - "state_type": "measurement" - }, - "orp_now": { - "name": "ORP Now", - "value": 0, - "unit": "mV", - "state_type": "measurement" - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 0, - "state_type": "measurement" - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 0, - "state_type": "measurement" - }, - "saturation": { - "name": "Saturation Index", - "value": 0.0, - "unit": "lsi", - "state_type": "measurement" - }, - "ph_probe_water_temp": { - "name": "pH Probe Water Temperature", - "value": 0, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement" - } - }, - "configuration": { - "ph_setpoint": { - "name": "pH Setpoint", - "value": 0.0, - "unit": "pH" - }, - "orp_setpoint": { - "name": "ORP Setpoint", - "value": 0, - "unit": "mV" - }, - "calcium_harness": { - "name": "Calcium Hardness", - "value": 0, - "unit": "ppm" - }, - "cya": { - "name": "Cyanuric Acid", - "value": 0, - "unit": "ppm" - }, - "total_alkalinity": { - "name": "Total Alkalinity", - "value": 0, - "unit": "ppm" - }, - "salt_tds_ppm": { - "name": "Salt/TDS", - "value": 0, - "unit": "ppm" - }, - "probe_is_celsius": 0, - "flags": 0 - }, - "dose_status": { - "ph_last_dose_time": { - "name": "Last pH Dose Time", - "value": 0, - "unit": "sec", - "device_type": "duration", - "state_type": "total_increasing" - }, - "orp_last_dose_time": { - "name": "Last ORP Dose Time", - "value": 0, - "unit": "sec", - "device_type": "duration", - "state_type": "total_increasing" - }, - "ph_last_dose_volume": { - "name": "Last pH Dose Volume", - "value": 0, - "unit": "mL", - "device_type": "volume", - "state_type": "total_increasing" - }, - "orp_last_dose_volume": { - "name": "Last ORP Dose Volume", - "value": 0, - "unit": "mL", - "device_type": "volume", - "state_type": "total_increasing" - }, - "flags": 0, - "ph_dosing_state": { - "name": "pH Dosing State", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Dosing", - "Mixing", - "Monitoring" - ] - }, - "orp_dosing_state": { - "name": "ORP Dosing State", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Dosing", - "Mixing", - "Monitoring" - ] - } - }, - "alarm": { - "flags": 0, - "flow_alarm": { - "name": "Flow Alarm", - "value": 0, - "device_type": "alarm" - }, - "ph_high_alarm": { - "name": "pH HIGH Alarm", - "value": 0, - "device_type": "alarm" - }, - "ph_low_alarm": { - "name": "pH LOW Alarm", - "value": 0, - "device_type": "alarm" - }, - "orp_high_alarm": { - "name": "ORP HIGH Alarm", - "value": 0, - "device_type": "alarm" - }, - "orp_low_alarm": { - "name": "ORP LOW Alarm", - "value": 0, - "device_type": "alarm" - }, - "ph_supply_alarm": { - "name": "pH Supply Alarm", - "value": 0, - "device_type": "alarm" - }, - "orp_supply_alarm": { - "name": "ORP Supply Alarm", - "value": 0, - "device_type": "alarm" - }, - "probe_fault_alarm": { - "name": "Probe Fault", - "value": 0, - "device_type": "alarm" - } - }, - "alert": { - "flags": 0, - "ph_lockout": { - "name": "pH Lockout", - "value": 0 - }, - "ph_limit": { - "name": "pH Dose Limit Reached", - "value": 0 - }, - "orp_limit": { - "name": "ORP Dose Limit Reached", - "value": 0 - } - }, - "firmware": { - "name": "IntelliChem Firmware", - "value": "0.000" - }, - "water_balance": { - "flags": 0, - "corrosive": { - "name": "SI Corrosive", - "value": 0, - "device_type": "alarm" - }, - "scaling": { - "name": "SI Scaling", - "value": 0, - "device_type": "alarm" - } - }, - "unknown_at_offset_44": 0, - "unknown_at_offset_45": 0, - "unknown_at_offset_46": 0 - }, - "scg": { - "scg_present": 1, - "sensor": { - "state": { - "name": "Chlorinator", - "value": 0 - }, - "salt_ppm": { - "name": "Chlorinator Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement" - } - }, - "configuration": { - "pool_setpoint": { - "name": "Pool Chlorinator Setpoint", - "value": 50, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 5, - "body_type": 0 - }, - "spa_setpoint": { - "name": "Spa Chlorinator Setpoint", - "value": 0, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 5, - "body_type": 1 - }, - "super_chlor_timer": { - "name": "Super Chlorination Timer", - "value": 0, - "unit": "hr", - "min_setpoint": 1, - "max_setpoint": 72, - "step": 1 - } - }, - "flags": 0 - } - }, - "version": null, - "config": { - "raw": { - "__type": "", - "repr": "b'd\\x00\\x00\\x00(h(h\\x00\\x00\\x00 <\\x00\\x00\\x00\\x0e\\x00\\x00\\x00Water Features\\x00\\x00\\x06\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x03\\x00\\x00\\x00Spa\\x00G\\x01\\x01\\x01\\x00\\x00\\x00\\x01\\xd0\\x02\\x00\\x00\\xf5\\x01\\x00\\x00\\n\\x00\\x00\\x00Pool Light\\x00\\x00>\\x10\\x03\\x00\\x00\\x00\\x00\\x02\\xd0\\x02\\x00\\x00\\xf6\\x01\\x00\\x00\\t\\x00\\x00\\x00Spa Light\\x00\\x00\\x00I\\x10\\x03\\x00\\x00\\x00\\x00\\x03\\xd0\\x02\\x00\\x00\\xf7\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cleaner\\x00\\x15\\x05\\x05\\x00\\x00\\x00\\x00\\x04\\xd0\\x02\\x00\\x00\\xf8\\x01\\x00\\x00\\x04\\x00\\x00\\x00Jets-\\x00\\x05\\x00\\x00\\x00\\x00\\x05\\xd0\\x02\\x00\\x00\\xf9\\x01\\x00\\x00\\x04\\x00\\x00\\x00Pool<\\x02\\x00\\x01\\x00\\x00\\x00\\x06\\xd0\\x02\\x00\\x00\\x08\\x00\\x00\\x00\\x05\\x00\\x00\\x00White\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x0b\\x00\\x00\\x00Light Green\\x00\\xa0\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xa0\\x00\\x00\\x00\\x05\\x00\\x00\\x00Green\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00P\\x00\\x00\\x00\\x04\\x00\\x00\\x00Cyan\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xc8\\x00\\x00\\x00\\x04\\x00\\x00\\x00Blued\\x00\\x00\\x00\\x8c\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x08\\x00\\x00\\x00Lavender\\xe6\\x00\\x00\\x00\\x82\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x07\\x00\\x00\\x00Magenta\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\r\\x00\\x00\\x00Light Magenta\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xb4\\x00\\x00\\x00\\xd2\\x00\\x00\\x00\\x86\\x85\\x00\\x00\\x00\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x01\\x00\\x00\\x00'" - }, - "decoded": { - "controller": { - "controller_id": 100, - "configuration": { - "body_type": { - "0": { - "min_setpoint": 40, - "max_setpoint": 104 - }, - "1": { - "min_setpoint": 40, - "max_setpoint": 104 - } - }, - "is_celsius": { - "name": "Is Celsius", - "value": 0 - }, - "controller_type": 0, - "hardware_type": 0, - "controller_data": 32, - "generic_circuit_name": "Water Features", - "circuit_count": 6, - "color_count": 8, - "color": [ - { - "name": "White", - "value": [ - 255, - 255, - 255 - ] - }, - { - "name": "Light Green", - "value": [ - 160, - 255, - 160 - ] - }, - { - "name": "Green", - "value": [ - 0, - 255, - 80 - ] - }, - { - "name": "Cyan", - "value": [ - 0, - 255, - 200 - ] - }, - { - "name": "Blue", - "value": [ - 100, - 140, - 255 - ] - }, - { - "name": "Lavender", - "value": [ - 230, - 130, - 255 - ] - }, - { - "name": "Magenta", - "value": [ - 255, - 0, - 128 - ] - }, - { - "name": "Light Magenta", - "value": [ - 255, - 180, - 210 - ] - } - ], - "interface_tab_flags": 127, - "show_alarms": 1 - }, - "model": { - "name": "Model", - "value": "IntelliTouch i5+3S" - }, - "equipment": { - "flags": 60, - "list": [ - "CHLORINATOR", - "INTELLIBRITE", - "INTELLIFLO_0", - "INTELLIFLO_1" - ] - } - }, - "circuit": { - "500": { - "circuit_id": 500, - "name": "Spa", - "configuration": { - "name_index": 71, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_62": 0, - "unknown_at_offset_63": 0 - }, - "function": 1, - "interface": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 1 - }, - "501": { - "circuit_id": 501, - "name": "Pool Light", - "configuration": { - "name_index": 62, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_94": 0, - "unknown_at_offset_95": 0 - }, - "function": 16, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 2 - }, - "502": { - "circuit_id": 502, - "name": "Spa Light", - "configuration": { - "name_index": 73, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_126": 0, - "unknown_at_offset_127": 0 - }, - "function": 16, - "interface": 3, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 3 - }, - "503": { - "circuit_id": 503, - "name": "Cleaner", - "configuration": { - "name_index": 21, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_154": 0, - "unknown_at_offset_155": 0 - }, - "function": 5, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 4 - }, - "504": { - "circuit_id": 504, - "name": "Jets", - "configuration": { - "name_index": 45, - "flags": 0, - "default_runtime": 720, - "unknown_at_offset_178": 0, - "unknown_at_offset_179": 0 - }, - "function": 0, - "interface": 5, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 5 - }, - "505": { - "circuit_id": 505, - "name": "Pool", - "configuration": { - "name_index": 60, - "flags": 1, - "default_runtime": 720, - "unknown_at_offset_202": 0, - "unknown_at_offset_203": 0 - }, - "function": 2, - "interface": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "device_id": 6 - } - }, - "pump": { - "0": { - "data": 134 - }, - "1": { - "data": 133 - }, - "2": { - "data": 0 - }, - "3": { - "data": 0 - }, - "4": { - "data": 0 - }, - "5": { - "data": 0 - }, - "6": { - "data": 0 - }, - "7": { - "data": 0 - } - } - } - }, - "status": { - "raw": { - "__type": "", - "repr": "b'\\x01\\x00\\x00\\x00\\x00 \\x00\\x00\\x00\\x00\\x00\\x00O\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00P\\x00\\x00\\x00\\x00\\x00\\x00\\x00T\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00S\\x00\\x00\\x00\\x00\\x00\\x00\\x00g\\x00\\x00\\x00O\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00'" - }, - "decoded": { - "controller": { - "sensor": { - "state": { - "name": "Controller State", - "value": 1, - "device_type": "enum", - "enum_options": [ - "Unknown", - "Ready", - "Sync", - "Service" - ] - }, - "freeze_mode": { - "name": "Freeze Mode", - "value": 0 - }, - "pool_delay": { - "name": "Pool Delay", - "value": 0 - }, - "spa_delay": { - "name": "Spa Delay", - "value": 0 - }, - "cleaner_delay": { - "name": "Cleaner Delay", - "value": 0 - }, - "air_temperature": { - "name": "Air Temperature", - "value": 79, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement" - }, - "ph": { - "name": "pH", - "value": 0.0, - "unit": "pH", - "state_type": "measurement" - }, - "orp": { - "name": "ORP", - "value": 0, - "unit": "mV", - "state_type": "measurement" - }, - "saturation": { - "name": "Saturation Index", - "value": 0.0, - "unit": "lsi", - "state_type": "measurement" - }, - "salt_ppm": { - "name": "Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement" - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 0, - "state_type": "measurement" - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 0, - "state_type": "measurement" - }, - "active_alert": { - "name": "Active Alert", - "value": 1, - "device_type": "alarm" - } - }, - "configuration": { - "remotes": 32, - "unknown_at_offset_09": 0, - "unknown_at_offset_10": 0, - "unknown_at_offset_11": 0 - } - }, - "body": { - "0": { - "body_type": 0, - "name": "Pool", - "last_temperature": { - "name": "Last Pool Temperature", - "value": 80, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement" - }, - "heat_state": { - "name": "Pool Heat", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Heater", - "Both" - ] - }, - "heat_setpoint": { - "name": "Pool Heat Set Point", - "value": 84, - "unit": "°F", - "device_type": "temperature" - }, - "cool_setpoint": { - "name": "Pool Cool Set Point", - "value": 100, - "unit": "°F", - "device_type": "temperature" - }, - "heat_mode": { - "name": "Pool Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change" - ] - } - }, - "1": { - "body_type": 1, - "name": "Spa", - "last_temperature": { - "name": "Last Spa Temperature", - "value": 83, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement" - }, - "heat_state": { - "name": "Spa Heat", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Heater", - "Both" - ] - }, - "heat_setpoint": { - "name": "Spa Heat Set Point", - "value": 103, - "unit": "°F", - "device_type": "temperature" - }, - "cool_setpoint": { - "name": "Spa Cool Set Point", - "value": 79, - "unit": "°F", - "device_type": "temperature" - }, - "heat_mode": { - "name": "Spa Heat Mode", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Off", - "Solar", - "Solar Preferred", - "Heater", - "Don't Change" - ] - } - } - }, - "circuit": { - "500": { - "circuit_id": 500, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "configuration": { - "delay": 0 - } - }, - "501": { - "circuit_id": 501, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "configuration": { - "delay": 0 - } - }, - "502": { - "circuit_id": 502, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "configuration": { - "delay": 0 - } - }, - "503": { - "circuit_id": 503, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "configuration": { - "delay": 0 - } - }, - "504": { - "circuit_id": 504, - "value": 0, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "configuration": { - "delay": 0 - } - }, - "505": { - "circuit_id": 505, - "value": 1, - "color": { - "color_set": 0, - "color_position": 0, - "color_stagger": 0 - }, - "configuration": { - "delay": 0 - } - } - } - } - }, - "pumps": [ - { - "raw": { - "__type": "", - "repr": "b'\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00D\\x00\\x00\\x00L\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x06\\x00\\x00\\x00L\\x04\\x00\\x00\\x01\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\xfc\\x08\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x16\\x08\\x00\\x00\\x01\\x00\\x00\\x00\\x04\\x00\\x00\\x00\\x88\\t\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00'" - }, - "decoded": { - "pump": { - "0": { - "type": 2, - "state": { - "name": "Default Pump", - "value": 1 - }, - "watts_now": { - "name": "Default Pump Watts Now", - "value": 68, - "unit": "W", - "device_type": "power", - "state_type": "measurement" - }, - "rpm_now": { - "name": "Default Pump RPM Now", - "value": 1100, - "unit": "rpm", - "state_type": "measurement" - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Default Pump GPM Now", - "value": 255, - "unit": "gpm", - "state_type": "measurement" - }, - "unknown_at_offset_24": 255, - "preset": { - "0": { - "device_id": 6, - "setpoint": 1100, - "is_rpm": 1 - }, - "1": { - "device_id": 2, - "setpoint": 2300, - "is_rpm": 1 - }, - "2": { - "device_id": 1, - "setpoint": 2070, - "is_rpm": 1 - }, - "3": { - "device_id": 4, - "setpoint": 2440, - "is_rpm": 1 - }, - "4": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "5": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "6": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "7": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - } - } - } - } - } - }, - { - "raw": { - "__type": "", - "repr": "b'\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\xf0\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xe8\\x03\\x00\\x00\\x01\\x00\\x00\\x00'" - }, - "decoded": { - "pump": { - "1": { - "type": 2, - "state": { - "name": "Default Pump", - "value": 0 - }, - "watts_now": { - "name": "Default Pump Watts Now", - "value": 0, - "unit": "W", - "device_type": "power", - "state_type": "measurement" - }, - "rpm_now": { - "name": "Default Pump RPM Now", - "value": 0, - "unit": "rpm", - "state_type": "measurement" - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Default Pump GPM Now", - "value": 0, - "unit": "gpm", - "state_type": "measurement" - }, - "unknown_at_offset_24": 255, - "preset": { - "0": { - "device_id": 5, - "setpoint": 2800, - "is_rpm": 1 - }, - "1": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "2": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "3": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "4": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "5": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "6": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - }, - "7": { - "device_id": 0, - "setpoint": 1000, - "is_rpm": 1 - } - } - } - } - } - } - ], - "chemistry": { - "raw": { - "__type": "", - "repr": "b'*\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" - }, - "decoded": { - "intellichem": { - "unknown_at_offset_00": 42, - "unknown_at_offset_04": 0, - "sensor": { - "ph_now": { - "name": "pH Now", - "value": 0.0, - "unit": "pH", - "state_type": "measurement" - }, - "orp_now": { - "name": "ORP Now", - "value": 0, - "unit": "mV", - "state_type": "measurement" - }, - "ph_supply_level": { - "name": "pH Supply Level", - "value": 0, - "state_type": "measurement" - }, - "orp_supply_level": { - "name": "ORP Supply Level", - "value": 0, - "state_type": "measurement" - }, - "saturation": { - "name": "Saturation Index", - "value": 0.0, - "unit": "lsi", - "state_type": "measurement" - }, - "ph_probe_water_temp": { - "name": "pH Probe Water Temperature", - "value": 0, - "unit": "°F", - "device_type": "temperature", - "state_type": "measurement" - } - }, - "configuration": { - "ph_setpoint": { - "name": "pH Setpoint", - "value": 0.0, - "unit": "pH" - }, - "orp_setpoint": { - "name": "ORP Setpoint", - "value": 0, - "unit": "mV" - }, - "calcium_harness": { - "name": "Calcium Hardness", - "value": 0, - "unit": "ppm" - }, - "cya": { - "name": "Cyanuric Acid", - "value": 0, - "unit": "ppm" - }, - "total_alkalinity": { - "name": "Total Alkalinity", - "value": 0, - "unit": "ppm" - }, - "salt_tds_ppm": { - "name": "Salt/TDS", - "value": 0, - "unit": "ppm" - }, - "probe_is_celsius": 0, - "flags": 0 - }, - "dose_status": { - "ph_last_dose_time": { - "name": "Last pH Dose Time", - "value": 0, - "unit": "sec", - "device_type": "duration", - "state_type": "total_increasing" - }, - "orp_last_dose_time": { - "name": "Last ORP Dose Time", - "value": 0, - "unit": "sec", - "device_type": "duration", - "state_type": "total_increasing" - }, - "ph_last_dose_volume": { - "name": "Last pH Dose Volume", - "value": 0, - "unit": "mL", - "device_type": "volume", - "state_type": "total_increasing" - }, - "orp_last_dose_volume": { - "name": "Last ORP Dose Volume", - "value": 0, - "unit": "mL", - "device_type": "volume", - "state_type": "total_increasing" - }, - "flags": 0, - "ph_dosing_state": { - "name": "pH Dosing State", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Dosing", - "Mixing", - "Monitoring" - ] - }, - "orp_dosing_state": { - "name": "ORP Dosing State", - "value": 0, - "device_type": "enum", - "enum_options": [ - "Dosing", - "Mixing", - "Monitoring" - ] - } - }, - "alarm": { - "flags": 0, - "flow_alarm": { - "name": "Flow Alarm", - "value": 0, - "device_type": "alarm" - }, - "ph_high_alarm": { - "name": "pH HIGH Alarm", - "value": 0, - "device_type": "alarm" - }, - "ph_low_alarm": { - "name": "pH LOW Alarm", - "value": 0, - "device_type": "alarm" - }, - "orp_high_alarm": { - "name": "ORP HIGH Alarm", - "value": 0, - "device_type": "alarm" - }, - "orp_low_alarm": { - "name": "ORP LOW Alarm", - "value": 0, - "device_type": "alarm" - }, - "ph_supply_alarm": { - "name": "pH Supply Alarm", - "value": 0, - "device_type": "alarm" - }, - "orp_supply_alarm": { - "name": "ORP Supply Alarm", - "value": 0, - "device_type": "alarm" - }, - "probe_fault_alarm": { - "name": "Probe Fault", - "value": 0, - "device_type": "alarm" - } - }, - "alert": { - "flags": 0, - "ph_lockout": { - "name": "pH Lockout", - "value": 0 - }, - "ph_limit": { - "name": "pH Dose Limit Reached", - "value": 0 - }, - "orp_limit": { - "name": "ORP Dose Limit Reached", - "value": 0 - } - }, - "firmware": { - "name": "IntelliChem Firmware", - "value": "0.000" - }, - "water_balance": { - "flags": 0, - "corrosive": { - "name": "SI Corrosive", - "value": 0, - "device_type": "alarm" - }, - "scaling": { - "name": "SI Scaling", - "value": 0, - "device_type": "alarm" - } - }, - "unknown_at_offset_44": 0, - "unknown_at_offset_45": 0, - "unknown_at_offset_46": 0 - } - } - }, - "scg": { - "raw": { - "__type": "", - "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x002\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" - }, - "decoded": { - "scg": { - "scg_present": 1, - "sensor": { - "state": { - "name": "Chlorinator", - "value": 0 - }, - "salt_ppm": { - "name": "Chlorinator Salt", - "value": 0, - "unit": "ppm", - "state_type": "measurement" - } - }, - "configuration": { - "pool_setpoint": { - "name": "Pool Chlorinator Setpoint", - "value": 50, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 5, - "body_type": 0 - }, - "spa_setpoint": { - "name": "Spa Chlorinator Setpoint", - "value": 0, - "unit": "%", - "min_setpoint": 0, - "max_setpoint": 100, - "step": 5, - "body_type": 1 - }, - "super_chlor_timer": { - "name": "Super Chlorination Timer", - "value": 0, - "unit": "hr", - "min_setpoint": 1, - "max_setpoint": 72, - "step": 1 - } - }, - "flags": 0 - } - } - }, - "color": null -} \ No newline at end of file diff --git a/tests/data/pool-52-build-7360-rel_easytouch2-8_98360.json b/tests/data/slpy-0100_pool-52-build-7380-rel_easytouch2-8_28.json similarity index 64% rename from tests/data/pool-52-build-7360-rel_easytouch2-8_98360.json rename to tests/data/slpy-0100_pool-52-build-7380-rel_easytouch2-8_28.json index 45c2ff5..25e2d97 100644 --- a/tests/data/pool-52-build-7360-rel_easytouch2-8_98360.json +++ b/tests/data/slpy-0100_pool-52-build-7380-rel_easytouch2-8_28.json @@ -3,7 +3,9 @@ "adapter": { "firmware": { "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 736.0 Rel" + "value": "POOL: 5.2 Build 738.0 Rel", + "major": 5.2, + "minor": 738.0 } }, "controller": { @@ -27,7 +29,7 @@ "hardware_type": 0, "controller_data": 0, "generic_circuit_name": "Water Features", - "circuit_count": 11, + "circuit_count": 18, "color_count": 8, "color": [ { @@ -96,7 +98,7 @@ } ], "interface_tab_flags": 127, - "show_alarms": 0, + "show_alarms": 1, "remotes": 0, "unknown_at_offset_09": 0, "unknown_at_offset_10": 0, @@ -107,19 +109,17 @@ "value": "EasyTouch2 8" }, "equipment": { - "flags": 98360, + "flags": 28, "list": [ + "CHLORINATOR", "INTELLIBRITE", - "INTELLIFLO_0", - "INTELLIFLO_1", - "INTELLICHEM", - "HYBRID_HEATER" + "INTELLIFLO_0" ] }, "sensor": { "state": { "name": "Controller State", - "value": 1, + "value": 2, "device_type": "enum", "enum_options": [ "Unknown", @@ -146,20 +146,20 @@ }, "air_temperature": { "name": "Air Temperature", - "value": 69, + "value": 0, "unit": "°F", "device_type": "temperature", "state_type": "measurement" }, "ph": { "name": "pH", - "value": 7.62, + "value": 0.0, "unit": "pH", "state_type": "measurement" }, "orp": { "name": "ORP", - "value": 778, + "value": 0, "unit": "mV", "state_type": "measurement" }, @@ -177,12 +177,12 @@ }, "ph_supply_level": { "name": "pH Supply Level", - "value": 3, + "value": 0, "state_type": "measurement" }, "orp_supply_level": { "name": "ORP Supply Level", - "value": 2, + "value": 0, "state_type": "measurement" }, "active_alert": { @@ -216,9 +216,9 @@ }, "501": { "circuit_id": 501, - "name": "Waterfall", + "name": "Air Blower", "configuration": { - "name_index": 85, + "name_index": 1, "flags": 0, "default_runtime": 720, "unknown_at_offset_94": 0, @@ -237,59 +237,59 @@ }, "502": { "circuit_id": 502, - "name": "Pool Light", + "name": "Lights", "configuration": { - "name_index": 62, + "name_index": 46, "flags": 0, "default_runtime": 720, - "unknown_at_offset_126": 0, - "unknown_at_offset_127": 0, + "unknown_at_offset_122": 0, + "unknown_at_offset_123": 0, "delay": 0 }, "function": 16, "interface": 3, "color": { - "color_set": 2, + "color_set": 0, "color_position": 0, - "color_stagger": 2 + "color_stagger": 10 }, "device_id": 3, "value": 0 }, "503": { "circuit_id": 503, - "name": "Spa Light", + "name": "BUBBLERS ", "configuration": { - "name_index": 73, + "name_index": 101, "flags": 0, "default_runtime": 720, - "unknown_at_offset_158": 0, - "unknown_at_offset_159": 0, + "unknown_at_offset_154": 0, + "unknown_at_offset_155": 0, "delay": 0 }, - "function": 16, - "interface": 3, + "function": 13, + "interface": 2, "color": { - "color_set": 6, - "color_position": 1, - "color_stagger": 10 + "color_set": 0, + "color_position": 0, + "color_stagger": 0 }, "device_id": 4, "value": 0 }, "504": { "circuit_id": 504, - "name": "Cleaner", + "name": "Aux 4", "configuration": { - "name_index": 21, + "name_index": 5, "flags": 0, - "default_runtime": 240, - "unknown_at_offset_186": 0, - "unknown_at_offset_187": 0, + "default_runtime": 720, + "unknown_at_offset_182": 0, + "unknown_at_offset_183": 0, "delay": 0 }, - "function": 5, - "interface": 0, + "function": 0, + "interface": 2, "color": { "color_set": 0, "color_position": 0, @@ -300,13 +300,13 @@ }, "505": { "circuit_id": 505, - "name": "Pool Low", + "name": "Pool", "configuration": { - "name_index": 63, + "name_index": 60, "flags": 1, "default_runtime": 720, - "unknown_at_offset_214": 0, - "unknown_at_offset_215": 0, + "unknown_at_offset_206": 0, + "unknown_at_offset_207": 0, "delay": 0 }, "function": 2, @@ -317,21 +317,21 @@ "color_stagger": 0 }, "device_id": 6, - "value": 1 + "value": 0 }, "506": { "circuit_id": 506, - "name": "Yard Light", + "name": "Aux 5", "configuration": { - "name_index": 91, + "name_index": 6, "flags": 0, "default_runtime": 720, - "unknown_at_offset_246": 0, - "unknown_at_offset_247": 0, + "unknown_at_offset_234": 0, + "unknown_at_offset_235": 0, "delay": 0 }, - "function": 7, - "interface": 4, + "function": 0, + "interface": 2, "color": { "color_set": 0, "color_position": 0, @@ -342,13 +342,13 @@ }, "507": { "circuit_id": 507, - "name": "Cameras", + "name": "Aux 6", "configuration": { - "name_index": 101, + "name_index": 7, "flags": 0, - "default_runtime": 1620, - "unknown_at_offset_274": 0, - "unknown_at_offset_275": 0, + "default_runtime": 720, + "unknown_at_offset_262": 0, + "unknown_at_offset_263": 0, "delay": 0 }, "function": 0, @@ -359,42 +359,42 @@ "color_stagger": 0 }, "device_id": 8, - "value": 1 + "value": 0 }, "508": { "circuit_id": 508, - "name": "Pool High", + "name": "Aux 7", "configuration": { - "name_index": 61, + "name_index": 8, "flags": 0, "default_runtime": 720, - "unknown_at_offset_306": 0, - "unknown_at_offset_307": 0, + "unknown_at_offset_290": 0, + "unknown_at_offset_291": 0, "delay": 0 }, "function": 0, - "interface": 0, + "interface": 2, "color": { "color_set": 0, "color_position": 0, "color_stagger": 0 }, "device_id": 9, - "value": 1 + "value": 0 }, "510": { "circuit_id": 510, - "name": "Spillway", + "name": "Feature 1", "configuration": { - "name_index": 78, + "name_index": 93, "flags": 0, "default_runtime": 720, - "unknown_at_offset_334": 0, - "unknown_at_offset_335": 0, + "unknown_at_offset_322": 0, + "unknown_at_offset_323": 0, "delay": 0 }, - "function": 14, - "interface": 1, + "function": 0, + "interface": 2, "color": { "color_set": 0, "color_position": 0, @@ -405,17 +405,17 @@ }, "511": { "circuit_id": 511, - "name": "Pool High", + "name": "Feature 2", "configuration": { - "name_index": 61, + "name_index": 94, "flags": 0, "default_runtime": 720, - "unknown_at_offset_366": 0, - "unknown_at_offset_367": 0, + "unknown_at_offset_354": 0, + "unknown_at_offset_355": 0, "delay": 0 }, "function": 0, - "interface": 5, + "interface": 2, "color": { "color_set": 0, "color_position": 0, @@ -423,6 +423,153 @@ }, "device_id": 12, "value": 0 + }, + "512": { + "circuit_id": 512, + "name": "Feature 3", + "configuration": { + "name_index": 95, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_386": 0, + "unknown_at_offset_387": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 13, + "value": 0 + }, + "513": { + "circuit_id": 513, + "name": "Feature 4", + "configuration": { + "name_index": 96, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_418": 0, + "unknown_at_offset_419": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 14, + "value": 0 + }, + "514": { + "circuit_id": 514, + "name": "Feature 5", + "configuration": { + "name_index": 97, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_450": 0, + "unknown_at_offset_451": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 15, + "value": 0 + }, + "515": { + "circuit_id": 515, + "name": "Feature 6", + "configuration": { + "name_index": 98, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_482": 0, + "unknown_at_offset_483": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 16, + "value": 0 + }, + "516": { + "circuit_id": 516, + "name": "Feature 7", + "configuration": { + "name_index": 99, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_514": 0, + "unknown_at_offset_515": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 17, + "value": 0 + }, + "517": { + "circuit_id": 517, + "name": "Feature 8", + "configuration": { + "name_index": 100, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_546": 0, + "unknown_at_offset_547": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 18, + "value": 0 + }, + "519": { + "circuit_id": 519, + "name": "AuxEx", + "configuration": { + "name_index": 92, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_574": 0, + "unknown_at_offset_575": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 20, + "value": 0 } }, "pump": { @@ -430,26 +577,26 @@ "data": 70, "type": 3, "state": { - "name": "Pool Low Pump", + "name": "Pool Pump", "value": 1 }, "watts_now": { - "name": "Pool Low Pump Watts Now", - "value": 1548, + "name": "Pool Pump Watts Now", + "value": 1337, "unit": "W", "device_type": "power", "state_type": "measurement" }, "rpm_now": { - "name": "Pool Low Pump RPM Now", - "value": 2887, + "name": "Pool Pump RPM Now", + "value": 2850, "unit": "rpm", "state_type": "measurement" }, "unknown_at_offset_16": 0, "gpm_now": { - "name": "Pool Low Pump GPM Now", - "value": 72, + "name": "Pool Pump GPM Now", + "value": 68, "unit": "gpm", "state_type": "measurement" }, @@ -457,85 +604,14 @@ "preset": { "0": { "device_id": 6, - "setpoint": 63, - "is_rpm": 0 + "setpoint": 2850, + "is_rpm": 1 }, "1": { - "device_id": 9, - "setpoint": 72, - "is_rpm": 0 - }, - "2": { "device_id": 1, - "setpoint": 3450, - "is_rpm": 1 - }, - "3": { - "device_id": 130, - "setpoint": 75, - "is_rpm": 0 - }, - "4": { - "device_id": 12, - "setpoint": 72, - "is_rpm": 0 - }, - "5": { - "device_id": 0, - "setpoint": 30, - "is_rpm": 0 - }, - "6": { - "device_id": 0, - "setpoint": 30, - "is_rpm": 0 - }, - "7": { - "device_id": 0, - "setpoint": 30, - "is_rpm": 0 - } - } - }, - "1": { - "data": 66, - "type": 3, - "state": { - "name": "Waterfall Pump", - "value": 0 - }, - "watts_now": { - "name": "Waterfall Pump Watts Now", - "value": 0, - "unit": "W", - "device_type": "power", - "state_type": "measurement" - }, - "rpm_now": { - "name": "Waterfall Pump RPM Now", - "value": 0, - "unit": "rpm", - "state_type": "measurement" - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Waterfall Pump GPM Now", - "value": 0, - "unit": "gpm", - "state_type": "measurement" - }, - "unknown_at_offset_24": 255, - "preset": { - "0": { - "device_id": 2, - "setpoint": 2700, + "setpoint": 2800, "is_rpm": 1 }, - "1": { - "device_id": 0, - "setpoint": 30, - "is_rpm": 0 - }, "2": { "device_id": 0, "setpoint": 30, @@ -568,6 +644,9 @@ } } }, + "1": { + "data": 0 + }, "2": { "data": 0 }, @@ -595,7 +674,7 @@ "name": "Pool", "last_temperature": { "name": "Last Pool Temperature", - "value": 75, + "value": 0, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -613,13 +692,13 @@ }, "heat_setpoint": { "name": "Pool Heat Set Point", - "value": 83, + "value": 0, "unit": "°F", "device_type": "temperature" }, "cool_setpoint": { "name": "Pool Cool Set Point", - "value": 100, + "value": 0, "unit": "°F", "device_type": "temperature" }, @@ -643,7 +722,7 @@ "name": "Spa", "last_temperature": { "name": "Last Spa Temperature", - "value": 84, + "value": 0, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -661,19 +740,19 @@ }, "heat_setpoint": { "name": "Spa Heat Set Point", - "value": 95, + "value": 0, "unit": "°F", "device_type": "temperature" }, "cool_setpoint": { "name": "Spa Cool Set Point", - "value": 69, + "value": 0, "unit": "°F", "device_type": "temperature" }, "heat_mode": { "name": "Spa Heat Mode", - "value": 3, + "value": 0, "device_type": "enum", "enum_options": [ "Off", @@ -691,24 +770,24 @@ "sensor": { "ph_now": { "name": "pH Now", - "value": 7.62, + "value": 0.0, "unit": "pH", "state_type": "measurement" }, "orp_now": { "name": "ORP Now", - "value": 778, + "value": 0, "unit": "mV", "state_type": "measurement" }, "ph_supply_level": { "name": "pH Supply Level", - "value": 3, + "value": 0, "state_type": "measurement" }, "orp_supply_level": { "name": "ORP Supply Level", - "value": 2, + "value": 0, "state_type": "measurement" }, "saturation": { @@ -719,7 +798,7 @@ }, "ph_probe_water_temp": { "name": "pH Probe Water Temperature", - "value": 75, + "value": 0, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -728,70 +807,82 @@ "configuration": { "ph_setpoint": { "name": "pH Setpoint", - "value": 7.6, - "unit": "pH" + "value": 0.0, + "unit": "pH", + "min_setpoint": 7.2, + "max_setpoint": 7.6 }, "orp_setpoint": { "name": "ORP Setpoint", - "value": 720, - "unit": "mV" + "value": 0, + "unit": "mV", + "min_setpoint": 400, + "max_setpoint": 800 }, - "calcium_harness": { + "calcium_hardness": { "name": "Calcium Hardness", - "value": 800, - "unit": "ppm" + "value": 0, + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 }, "cya": { "name": "Cyanuric Acid", - "value": 45, - "unit": "ppm" + "value": 0, + "unit": "ppm", + "min_setpoint": 0, + "max_setpoint": 201 }, "total_alkalinity": { "name": "Total Alkalinity", - "value": 45, - "unit": "ppm" + "value": 0, + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 }, "salt_tds_ppm": { "name": "Salt/TDS", - "value": 1000, - "unit": "ppm" + "value": 0, + "unit": "ppm", + "min_setpoint": 500, + "max_setpoint": 6500 }, "probe_is_celsius": 0, - "flags": 32 + "flags": 0 }, "dose_status": { "ph_last_dose_time": { "name": "Last pH Dose Time", - "value": 13, + "value": 0, "unit": "sec", "device_type": "duration", "state_type": "total_increasing" }, "orp_last_dose_time": { "name": "Last ORP Dose Time", - "value": 4, + "value": 0, "unit": "sec", "device_type": "duration", "state_type": "total_increasing" }, "ph_last_dose_volume": { "name": "Last pH Dose Volume", - "value": 22, + "value": 0, "unit": "mL", "device_type": "volume", "state_type": "total_increasing" }, "orp_last_dose_volume": { "name": "Last ORP Dose Volume", - "value": 8, + "value": 0, "unit": "mL", "device_type": "volume", "state_type": "total_increasing" }, - "flags": 149, + "flags": 0, "ph_dosing_state": { "name": "pH Dosing State", - "value": 1, + "value": 0, "device_type": "enum", "enum_options": [ "Dosing", @@ -801,7 +892,7 @@ }, "orp_dosing_state": { "name": "ORP Dosing State", - "value": 2, + "value": 0, "device_type": "enum", "enum_options": [ "Dosing", @@ -870,7 +961,9 @@ }, "firmware": { "name": "IntelliChem Firmware", - "value": "1.060" + "value": "0.000", + "major": 0, + "minor": 0 }, "water_balance": { "flags": 0, @@ -890,15 +983,15 @@ "unknown_at_offset_46": 0 }, "scg": { - "scg_present": 0, + "scg_present": 11, "sensor": { "state": { "name": "Chlorinator", - "value": 0 + "value": 1 }, "salt_ppm": { "name": "Chlorinator Salt", - "value": 0, + "value": 3150, "unit": "ppm", "state_type": "measurement" } @@ -906,44 +999,50 @@ "configuration": { "pool_setpoint": { "name": "Pool Chlorinator Setpoint", - "value": 51, + "value": 80, "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 0 }, "spa_setpoint": { "name": "Spa Chlorinator Setpoint", - "value": 0, + "value": 5, "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 1 }, "super_chlor_timer": { "name": "Super Chlorination Timer", "value": 0, "unit": "hr", - "min_setpoint": 1, + "min_setpoint": 0, "max_setpoint": 72, "step": 1 } }, - "flags": 0 + "flags": 128, + "super_chlorinate": { + "name": "Super Chlorinate", + "value": 0 + } } }, "version": { "raw": { "__type": "", - "repr": "b'\\x19\\x00\\x00\\x00POOL: 5.2 Build 736.0 Rel\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x0c\\x00\\x00\\x00'" + "repr": "b'\\x19\\x00\\x00\\x00POOL: 5.2 Build 738.0 Rel\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x0c\\x00\\x00\\x00'" }, "decoded": { "adapter": { "firmware": { "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 736.0 Rel" + "value": "POOL: 5.2 Build 738.0 Rel", + "major": 5.2, + "minor": 738.0 } } } @@ -951,7 +1050,7 @@ "config": { "raw": { "__type": "", - "repr": "b'd\\x00\\x00\\x00(h(h\\x00\\r\\x00\\x008\\x80\\xff\\xff\\x0e\\x00\\x00\\x00Water Features\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x03\\x00\\x00\\x00Spa\\x00G\\x01\\x01\\x01\\x00\\x00\\x00\\x01\\xd0\\x02\\x00\\x00\\xf5\\x01\\x00\\x00\\t\\x00\\x00\\x00Waterfall\\x00\\x00\\x00U\\x00\\x02\\x00\\x00\\x00\\x00\\x02\\xd0\\x02\\x00\\x00\\xf6\\x01\\x00\\x00\\n\\x00\\x00\\x00Pool Light\\x00\\x00>\\x10\\x03\\x00\\x02\\x00\\x02\\x03\\xd0\\x02\\x00\\x00\\xf7\\x01\\x00\\x00\\t\\x00\\x00\\x00Spa Light\\x00\\x00\\x00I\\x10\\x03\\x00\\x06\\x01\\n\\x04\\xd0\\x02\\x00\\x00\\xf8\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cleaner\\x00\\x15\\x05\\x00\\x00\\x00\\x00\\x00\\x05\\xf0\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x08\\x00\\x00\\x00Pool Low?\\x02\\x00\\x01\\x00\\x00\\x00\\x06\\xd0\\x02\\x00\\x00\\xfa\\x01\\x00\\x00\\n\\x00\\x00\\x00Yard Light\\x00\\x00[\\x07\\x04\\x00\\x00\\x00\\x00\\x07\\xd0\\x02\\x00\\x00\\xfb\\x01\\x00\\x00\\x07\\x00\\x00\\x00Cameras\\x00e\\x00\\x02\\x00\\x00\\x00\\x00\\x08T\\x06\\x00\\x00\\xfc\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x00\\x00\\x00\\x00\\x00\\t\\xd0\\x02\\x00\\x00\\xfe\\x01\\x00\\x00\\x08\\x00\\x00\\x00SpillwayN\\x0e\\x01\\x00\\x00\\x00\\x00\\x0b\\xd0\\x02\\x00\\x00\\xff\\x01\\x00\\x00\\t\\x00\\x00\\x00Pool High\\x00\\x00\\x00=\\x00\\x05\\x00\\x00\\x00\\x00\\x0c\\xd0\\x02\\x00\\x00\\x08\\x00\\x00\\x00\\x05\\x00\\x00\\x00White\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x0b\\x00\\x00\\x00Light Green\\x00\\xa0\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xa0\\x00\\x00\\x00\\x05\\x00\\x00\\x00Green\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00P\\x00\\x00\\x00\\x04\\x00\\x00\\x00Cyan\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xc8\\x00\\x00\\x00\\x04\\x00\\x00\\x00Blued\\x00\\x00\\x00\\x8c\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x08\\x00\\x00\\x00Lavender\\xe6\\x00\\x00\\x00\\x82\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x07\\x00\\x00\\x00Magenta\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\r\\x00\\x00\\x00Light Magenta\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xb4\\x00\\x00\\x00\\xd2\\x00\\x00\\x00FB\\x00\\x00\\x00\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + "repr": "b'd\\x00\\x00\\x00(h(h\\x00\\r\\x00\\x00\\x1c\\x00\\x00\\x00\\x0e\\x00\\x00\\x00Water Features\\x00\\x00\\x12\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x03\\x00\\x00\\x00Spa\\x00G\\x01\\x01\\x01\\x00\\x00\\x00\\x01\\xd0\\x02\\x00\\x00\\xf5\\x01\\x00\\x00\\n\\x00\\x00\\x00Air Blower\\x00\\x00\\x01\\x00\\x02\\x00\\x00\\x00\\x00\\x02\\xd0\\x02\\x00\\x00\\xf6\\x01\\x00\\x00\\x06\\x00\\x00\\x00Lights\\x00\\x00.\\x10\\x03\\x00\\x00\\x00\\n\\x03\\xd0\\x02\\x00\\x00\\xf7\\x01\\x00\\x00\\x0b\\x00\\x00\\x00BUBBLERS \\x00e\\r\\x02\\x00\\x00\\x00\\x00\\x04\\xd0\\x02\\x00\\x00\\xf8\\x01\\x00\\x00\\x05\\x00\\x00\\x00Aux 4\\x00\\x00\\x00\\x05\\x00\\x02\\x00\\x00\\x00\\x00\\x05\\xd0\\x02\\x00\\x00\\xf9\\x01\\x00\\x00\\x04\\x00\\x00\\x00Pool<\\x02\\x00\\x01\\x00\\x00\\x00\\x06\\xd0\\x02\\x00\\x00\\xfa\\x01\\x00\\x00\\x05\\x00\\x00\\x00Aux 5\\x00\\x00\\x00\\x06\\x00\\x02\\x00\\x00\\x00\\x00\\x07\\xd0\\x02\\x00\\x00\\xfb\\x01\\x00\\x00\\x05\\x00\\x00\\x00Aux 6\\x00\\x00\\x00\\x07\\x00\\x02\\x00\\x00\\x00\\x00\\x08\\xd0\\x02\\x00\\x00\\xfc\\x01\\x00\\x00\\x05\\x00\\x00\\x00Aux 7\\x00\\x00\\x00\\x08\\x00\\x02\\x00\\x00\\x00\\x00\\t\\xd0\\x02\\x00\\x00\\xfe\\x01\\x00\\x00\\t\\x00\\x00\\x00Feature 1\\x00\\x00\\x00]\\x00\\x02\\x00\\x00\\x00\\x00\\x0b\\xd0\\x02\\x00\\x00\\xff\\x01\\x00\\x00\\t\\x00\\x00\\x00Feature 2\\x00\\x00\\x00^\\x00\\x02\\x00\\x00\\x00\\x00\\x0c\\xd0\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\t\\x00\\x00\\x00Feature 3\\x00\\x00\\x00_\\x00\\x02\\x00\\x00\\x00\\x00\\r\\xd0\\x02\\x00\\x00\\x01\\x02\\x00\\x00\\t\\x00\\x00\\x00Feature 4\\x00\\x00\\x00`\\x00\\x02\\x00\\x00\\x00\\x00\\x0e\\xd0\\x02\\x00\\x00\\x02\\x02\\x00\\x00\\t\\x00\\x00\\x00Feature 5\\x00\\x00\\x00a\\x00\\x02\\x00\\x00\\x00\\x00\\x0f\\xd0\\x02\\x00\\x00\\x03\\x02\\x00\\x00\\t\\x00\\x00\\x00Feature 6\\x00\\x00\\x00b\\x00\\x02\\x00\\x00\\x00\\x00\\x10\\xd0\\x02\\x00\\x00\\x04\\x02\\x00\\x00\\t\\x00\\x00\\x00Feature 7\\x00\\x00\\x00c\\x00\\x02\\x00\\x00\\x00\\x00\\x11\\xd0\\x02\\x00\\x00\\x05\\x02\\x00\\x00\\t\\x00\\x00\\x00Feature 8\\x00\\x00\\x00d\\x00\\x02\\x00\\x00\\x00\\x00\\x12\\xd0\\x02\\x00\\x00\\x07\\x02\\x00\\x00\\x05\\x00\\x00\\x00AuxEx\\x00\\x00\\x00\\\\\\x00\\x02\\x00\\x00\\x00\\x00\\x14\\xd0\\x02\\x00\\x00\\x08\\x00\\x00\\x00\\x05\\x00\\x00\\x00White\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x0b\\x00\\x00\\x00Light Green\\x00\\xa0\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xa0\\x00\\x00\\x00\\x05\\x00\\x00\\x00Green\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00P\\x00\\x00\\x00\\x04\\x00\\x00\\x00Cyan\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xc8\\x00\\x00\\x00\\x04\\x00\\x00\\x00Blued\\x00\\x00\\x00\\x8c\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x08\\x00\\x00\\x00Lavender\\xe6\\x00\\x00\\x00\\x82\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x07\\x00\\x00\\x00Magenta\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\r\\x00\\x00\\x00Light Magenta\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xb4\\x00\\x00\\x00\\xd2\\x00\\x00\\x00F\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\x01\\x00\\x00\\x00'" }, "decoded": { "controller": { @@ -975,7 +1074,7 @@ "hardware_type": 0, "controller_data": 0, "generic_circuit_name": "Water Features", - "circuit_count": 11, + "circuit_count": 18, "color_count": 8, "color": [ { @@ -1044,20 +1143,18 @@ } ], "interface_tab_flags": 127, - "show_alarms": 0 + "show_alarms": 1 }, "model": { "name": "Model", "value": "EasyTouch2 8" }, "equipment": { - "flags": 98360, + "flags": 28, "list": [ + "CHLORINATOR", "INTELLIBRITE", - "INTELLIFLO_0", - "INTELLIFLO_1", - "INTELLICHEM", - "HYBRID_HEATER" + "INTELLIFLO_0" ] } }, @@ -1083,9 +1180,9 @@ }, "501": { "circuit_id": 501, - "name": "Waterfall", + "name": "Air Blower", "configuration": { - "name_index": 85, + "name_index": 1, "flags": 0, "default_runtime": 720, "unknown_at_offset_94": 0, @@ -1102,54 +1199,54 @@ }, "502": { "circuit_id": 502, - "name": "Pool Light", + "name": "Lights", "configuration": { - "name_index": 62, + "name_index": 46, "flags": 0, "default_runtime": 720, - "unknown_at_offset_126": 0, - "unknown_at_offset_127": 0 + "unknown_at_offset_122": 0, + "unknown_at_offset_123": 0 }, "function": 16, "interface": 3, "color": { - "color_set": 2, + "color_set": 0, "color_position": 0, - "color_stagger": 2 + "color_stagger": 10 }, "device_id": 3 }, "503": { "circuit_id": 503, - "name": "Spa Light", + "name": "BUBBLERS ", "configuration": { - "name_index": 73, + "name_index": 101, "flags": 0, "default_runtime": 720, - "unknown_at_offset_158": 0, - "unknown_at_offset_159": 0 + "unknown_at_offset_154": 0, + "unknown_at_offset_155": 0 }, - "function": 16, - "interface": 3, + "function": 13, + "interface": 2, "color": { - "color_set": 6, - "color_position": 1, - "color_stagger": 10 + "color_set": 0, + "color_position": 0, + "color_stagger": 0 }, "device_id": 4 }, "504": { "circuit_id": 504, - "name": "Cleaner", + "name": "Aux 4", "configuration": { - "name_index": 21, + "name_index": 5, "flags": 0, - "default_runtime": 240, - "unknown_at_offset_186": 0, - "unknown_at_offset_187": 0 + "default_runtime": 720, + "unknown_at_offset_182": 0, + "unknown_at_offset_183": 0 }, - "function": 5, - "interface": 0, + "function": 0, + "interface": 2, "color": { "color_set": 0, "color_position": 0, @@ -1159,13 +1256,13 @@ }, "505": { "circuit_id": 505, - "name": "Pool Low", + "name": "Pool", "configuration": { - "name_index": 63, + "name_index": 60, "flags": 1, "default_runtime": 720, - "unknown_at_offset_214": 0, - "unknown_at_offset_215": 0 + "unknown_at_offset_206": 0, + "unknown_at_offset_207": 0 }, "function": 2, "interface": 0, @@ -1178,16 +1275,16 @@ }, "506": { "circuit_id": 506, - "name": "Yard Light", + "name": "Aux 5", "configuration": { - "name_index": 91, + "name_index": 6, "flags": 0, "default_runtime": 720, - "unknown_at_offset_246": 0, - "unknown_at_offset_247": 0 + "unknown_at_offset_234": 0, + "unknown_at_offset_235": 0 }, - "function": 7, - "interface": 4, + "function": 0, + "interface": 2, "color": { "color_set": 0, "color_position": 0, @@ -1197,13 +1294,13 @@ }, "507": { "circuit_id": 507, - "name": "Cameras", + "name": "Aux 6", "configuration": { - "name_index": 101, + "name_index": 7, "flags": 0, - "default_runtime": 1620, - "unknown_at_offset_274": 0, - "unknown_at_offset_275": 0 + "default_runtime": 720, + "unknown_at_offset_262": 0, + "unknown_at_offset_263": 0 }, "function": 0, "interface": 2, @@ -1216,16 +1313,16 @@ }, "508": { "circuit_id": 508, - "name": "Pool High", + "name": "Aux 7", "configuration": { - "name_index": 61, + "name_index": 8, "flags": 0, "default_runtime": 720, - "unknown_at_offset_306": 0, - "unknown_at_offset_307": 0 + "unknown_at_offset_290": 0, + "unknown_at_offset_291": 0 }, "function": 0, - "interface": 0, + "interface": 2, "color": { "color_set": 0, "color_position": 0, @@ -1235,16 +1332,16 @@ }, "510": { "circuit_id": 510, - "name": "Spillway", + "name": "Feature 1", "configuration": { - "name_index": 78, + "name_index": 93, "flags": 0, "default_runtime": 720, - "unknown_at_offset_334": 0, - "unknown_at_offset_335": 0 + "unknown_at_offset_322": 0, + "unknown_at_offset_323": 0 }, - "function": 14, - "interface": 1, + "function": 0, + "interface": 2, "color": { "color_set": 0, "color_position": 0, @@ -1254,22 +1351,155 @@ }, "511": { "circuit_id": 511, - "name": "Pool High", + "name": "Feature 2", "configuration": { - "name_index": 61, + "name_index": 94, "flags": 0, "default_runtime": 720, - "unknown_at_offset_366": 0, - "unknown_at_offset_367": 0 + "unknown_at_offset_354": 0, + "unknown_at_offset_355": 0 }, "function": 0, - "interface": 5, + "interface": 2, "color": { "color_set": 0, "color_position": 0, "color_stagger": 0 }, "device_id": 12 + }, + "512": { + "circuit_id": 512, + "name": "Feature 3", + "configuration": { + "name_index": 95, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_386": 0, + "unknown_at_offset_387": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 13 + }, + "513": { + "circuit_id": 513, + "name": "Feature 4", + "configuration": { + "name_index": 96, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_418": 0, + "unknown_at_offset_419": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 14 + }, + "514": { + "circuit_id": 514, + "name": "Feature 5", + "configuration": { + "name_index": 97, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_450": 0, + "unknown_at_offset_451": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 15 + }, + "515": { + "circuit_id": 515, + "name": "Feature 6", + "configuration": { + "name_index": 98, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_482": 0, + "unknown_at_offset_483": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 16 + }, + "516": { + "circuit_id": 516, + "name": "Feature 7", + "configuration": { + "name_index": 99, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_514": 0, + "unknown_at_offset_515": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 17 + }, + "517": { + "circuit_id": 517, + "name": "Feature 8", + "configuration": { + "name_index": 100, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_546": 0, + "unknown_at_offset_547": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 18 + }, + "519": { + "circuit_id": 519, + "name": "AuxEx", + "configuration": { + "name_index": 92, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_574": 0, + "unknown_at_offset_575": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 20 } }, "pump": { @@ -1277,7 +1507,7 @@ "data": 70 }, "1": { - "data": 66 + "data": 0 }, "2": { "data": 0 @@ -1303,14 +1533,14 @@ "status": { "raw": { "__type": "", - "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00E\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00K\\x00\\x00\\x00\\x00\\x00\\x00\\x00S\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00T\\x00\\x00\\x00\\x00\\x00\\x00\\x00_\\x00\\x00\\x00E\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x0b\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x02\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x06\\x01\\n\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfb\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfc\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfe\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x02\\x00\\x00\\n\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + "repr": "b'\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x12\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf9\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfb\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfc\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfe\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" }, "decoded": { "controller": { "sensor": { "state": { "name": "Controller State", - "value": 1, + "value": 2, "device_type": "enum", "enum_options": [ "Unknown", @@ -1337,20 +1567,20 @@ }, "air_temperature": { "name": "Air Temperature", - "value": 69, + "value": 0, "unit": "°F", "device_type": "temperature", "state_type": "measurement" }, "ph": { "name": "pH", - "value": 7.62, + "value": 0.0, "unit": "pH", "state_type": "measurement" }, "orp": { "name": "ORP", - "value": 778, + "value": 0, "unit": "mV", "state_type": "measurement" }, @@ -1368,12 +1598,12 @@ }, "ph_supply_level": { "name": "pH Supply Level", - "value": 3, + "value": 0, "state_type": "measurement" }, "orp_supply_level": { "name": "ORP Supply Level", - "value": 2, + "value": 0, "state_type": "measurement" }, "active_alert": { @@ -1395,7 +1625,7 @@ "name": "Pool", "last_temperature": { "name": "Last Pool Temperature", - "value": 75, + "value": 0, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -1413,13 +1643,13 @@ }, "heat_setpoint": { "name": "Pool Heat Set Point", - "value": 83, + "value": 0, "unit": "°F", "device_type": "temperature" }, "cool_setpoint": { "name": "Pool Cool Set Point", - "value": 100, + "value": 0, "unit": "°F", "device_type": "temperature" }, @@ -1441,7 +1671,7 @@ "name": "Spa", "last_temperature": { "name": "Last Spa Temperature", - "value": 84, + "value": 0, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -1459,19 +1689,19 @@ }, "heat_setpoint": { "name": "Spa Heat Set Point", - "value": 95, + "value": 0, "unit": "°F", "device_type": "temperature" }, "cool_setpoint": { "name": "Spa Cool Set Point", - "value": 69, + "value": 0, "unit": "°F", "device_type": "temperature" }, "heat_mode": { "name": "Spa Heat Mode", - "value": 3, + "value": 0, "device_type": "enum", "enum_options": [ "Off", @@ -1512,9 +1742,9 @@ "circuit_id": 502, "value": 0, "color": { - "color_set": 2, + "color_set": 0, "color_position": 0, - "color_stagger": 2 + "color_stagger": 10 }, "configuration": { "delay": 0 @@ -1524,9 +1754,9 @@ "circuit_id": 503, "value": 0, "color": { - "color_set": 6, - "color_position": 1, - "color_stagger": 10 + "color_set": 0, + "color_position": 0, + "color_stagger": 0 }, "configuration": { "delay": 0 @@ -1546,7 +1776,7 @@ }, "505": { "circuit_id": 505, - "value": 1, + "value": 0, "color": { "color_set": 0, "color_position": 0, @@ -1570,7 +1800,7 @@ }, "507": { "circuit_id": 507, - "value": 1, + "value": 0, "color": { "color_set": 0, "color_position": 0, @@ -1582,7 +1812,7 @@ }, "508": { "circuit_id": 508, - "value": 1, + "value": 0, "color": { "color_set": 0, "color_position": 0, @@ -1615,6 +1845,90 @@ "configuration": { "delay": 0 } + }, + "512": { + "circuit_id": 512, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "513": { + "circuit_id": 513, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "514": { + "circuit_id": 514, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "515": { + "circuit_id": 515, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "516": { + "circuit_id": 516, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "517": { + "circuit_id": 517, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } + }, + "519": { + "circuit_id": 519, + "value": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "configuration": { + "delay": 0 + } } } } @@ -1623,7 +1937,7 @@ { "raw": { "__type": "", - "repr": "b'\\x03\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x0c\\x06\\x00\\x00G\\x0b\\x00\\x00\\x00\\x00\\x00\\x00H\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x06\\x00\\x00\\x00?\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\t\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00z\\r\\x00\\x00\\x01\\x00\\x00\\x00\\x82\\x00\\x00\\x00K\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x00\\x00\\x00H\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + "repr": "b'\\x03\\x00\\x00\\x00\\x01\\x00\\x00\\x009\\x05\\x00\\x00\"\\x0b\\x00\\x00\\x00\\x00\\x00\\x00D\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x06\\x00\\x00\\x00\"\\x0b\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\xf0\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" }, "decoded": { "pump": { @@ -1635,21 +1949,21 @@ }, "watts_now": { "name": "Default Pump Watts Now", - "value": 1548, + "value": 1337, "unit": "W", "device_type": "power", "state_type": "measurement" }, "rpm_now": { "name": "Default Pump RPM Now", - "value": 2887, + "value": 2850, "unit": "rpm", "state_type": "measurement" }, "unknown_at_offset_16": 0, "gpm_now": { "name": "Default Pump GPM Now", - "value": 72, + "value": 68, "unit": "gpm", "state_type": "measurement" }, @@ -1657,94 +1971,14 @@ "preset": { "0": { "device_id": 6, - "setpoint": 63, - "is_rpm": 0 + "setpoint": 2850, + "is_rpm": 1 }, "1": { - "device_id": 9, - "setpoint": 72, - "is_rpm": 0 - }, - "2": { "device_id": 1, - "setpoint": 3450, - "is_rpm": 1 - }, - "3": { - "device_id": 130, - "setpoint": 75, - "is_rpm": 0 - }, - "4": { - "device_id": 12, - "setpoint": 72, - "is_rpm": 0 - }, - "5": { - "device_id": 0, - "setpoint": 30, - "is_rpm": 0 - }, - "6": { - "device_id": 0, - "setpoint": 30, - "is_rpm": 0 - }, - "7": { - "device_id": 0, - "setpoint": 30, - "is_rpm": 0 - } - } - } - } - } - }, - { - "raw": { - "__type": "", - "repr": "b'\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x8c\\n\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x1e\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" - }, - "decoded": { - "pump": { - "1": { - "type": 3, - "state": { - "name": "Default Pump", - "value": 0 - }, - "watts_now": { - "name": "Default Pump Watts Now", - "value": 0, - "unit": "W", - "device_type": "power", - "state_type": "measurement" - }, - "rpm_now": { - "name": "Default Pump RPM Now", - "value": 0, - "unit": "rpm", - "state_type": "measurement" - }, - "unknown_at_offset_16": 0, - "gpm_now": { - "name": "Default Pump GPM Now", - "value": 0, - "unit": "gpm", - "state_type": "measurement" - }, - "unknown_at_offset_24": 255, - "preset": { - "0": { - "device_id": 2, - "setpoint": 2700, + "setpoint": 2800, "is_rpm": 1 }, - "1": { - "device_id": 0, - "setpoint": 30, - "is_rpm": 0 - }, "2": { "device_id": 0, "setpoint": 30, @@ -1784,7 +2018,7 @@ "chemistry": { "raw": { "__type": "", - "repr": "b'*\\x00\\x00\\x00\\x00\\x02\\xfa\\x03\\n\\x02\\xf8\\x02\\xd0\\x00\\x00\\x00\\r\\x00\\x00\\x00\\x04\\x00\\x16\\x00\\x08\\x03\\x02\\x00\\x03 \\x00-\\x00-\\x14\\x00K\\x00\\x00\\x95 <\\x01\\x00\\x00\\x00\\x00\\x00'" + "repr": "b'*\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" }, "decoded": { "intellichem": { @@ -1793,24 +2027,24 @@ "sensor": { "ph_now": { "name": "pH Now", - "value": 7.62, + "value": 0.0, "unit": "pH", "state_type": "measurement" }, "orp_now": { "name": "ORP Now", - "value": 778, + "value": 0, "unit": "mV", "state_type": "measurement" }, "ph_supply_level": { "name": "pH Supply Level", - "value": 3, + "value": 0, "state_type": "measurement" }, "orp_supply_level": { "name": "ORP Supply Level", - "value": 2, + "value": 0, "state_type": "measurement" }, "saturation": { @@ -1821,7 +2055,7 @@ }, "ph_probe_water_temp": { "name": "pH Probe Water Temperature", - "value": 75, + "value": 0, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -1830,70 +2064,82 @@ "configuration": { "ph_setpoint": { "name": "pH Setpoint", - "value": 7.6, - "unit": "pH" + "value": 0.0, + "unit": "pH", + "min_setpoint": 7.2, + "max_setpoint": 7.6 }, "orp_setpoint": { "name": "ORP Setpoint", - "value": 720, - "unit": "mV" + "value": 0, + "unit": "mV", + "min_setpoint": 400, + "max_setpoint": 800 }, - "calcium_harness": { + "calcium_hardness": { "name": "Calcium Hardness", - "value": 800, - "unit": "ppm" + "value": 0, + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 }, "cya": { "name": "Cyanuric Acid", - "value": 45, - "unit": "ppm" + "value": 0, + "unit": "ppm", + "min_setpoint": 0, + "max_setpoint": 201 }, "total_alkalinity": { "name": "Total Alkalinity", - "value": 45, - "unit": "ppm" + "value": 0, + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 }, "salt_tds_ppm": { "name": "Salt/TDS", - "value": 1000, - "unit": "ppm" + "value": 0, + "unit": "ppm", + "min_setpoint": 500, + "max_setpoint": 6500 }, "probe_is_celsius": 0, - "flags": 32 + "flags": 0 }, "dose_status": { "ph_last_dose_time": { "name": "Last pH Dose Time", - "value": 13, + "value": 0, "unit": "sec", "device_type": "duration", "state_type": "total_increasing" }, "orp_last_dose_time": { "name": "Last ORP Dose Time", - "value": 4, + "value": 0, "unit": "sec", "device_type": "duration", "state_type": "total_increasing" }, "ph_last_dose_volume": { "name": "Last pH Dose Volume", - "value": 22, + "value": 0, "unit": "mL", "device_type": "volume", "state_type": "total_increasing" }, "orp_last_dose_volume": { "name": "Last ORP Dose Volume", - "value": 8, + "value": 0, "unit": "mL", "device_type": "volume", "state_type": "total_increasing" }, - "flags": 149, + "flags": 0, "ph_dosing_state": { "name": "pH Dosing State", - "value": 1, + "value": 0, "device_type": "enum", "enum_options": [ "Dosing", @@ -1903,7 +2149,7 @@ }, "orp_dosing_state": { "name": "ORP Dosing State", - "value": 2, + "value": 0, "device_type": "enum", "enum_options": [ "Dosing", @@ -1972,7 +2218,9 @@ }, "firmware": { "name": "IntelliChem Firmware", - "value": "1.060" + "value": "0.000", + "major": 0, + "minor": 0 }, "water_balance": { "flags": 0, @@ -1996,19 +2244,19 @@ "scg": { "raw": { "__type": "", - "repr": "b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x003\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + "repr": "b'\\x0b\\x00\\x00\\x00\\x81\\x00\\x00\\x00P\\x00\\x00\\x00\\x05\\x00\\x00\\x00?\\x00\\x00\\x00\\x80\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" }, "decoded": { "scg": { - "scg_present": 0, + "scg_present": 11, "sensor": { "state": { "name": "Chlorinator", - "value": 0 + "value": 1 }, "salt_ppm": { "name": "Chlorinator Salt", - "value": 0, + "value": 3150, "unit": "ppm", "state_type": "measurement" } @@ -2016,34 +2264,39 @@ "configuration": { "pool_setpoint": { "name": "Pool Chlorinator Setpoint", - "value": 51, + "value": 80, "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 0 }, "spa_setpoint": { "name": "Spa Chlorinator Setpoint", - "value": 0, + "value": 5, "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 1 }, "super_chlor_timer": { "name": "Super Chlorination Timer", "value": 0, "unit": "hr", - "min_setpoint": 1, + "min_setpoint": 0, "max_setpoint": 72, "step": 1 } }, - "flags": 0 + "flags": 128, + "super_chlorinate": { + "name": "Super Chlorinate", + "value": 0 + } } } }, - "color": null + "color": null, + "date_time": null } \ No newline at end of file diff --git a/tests/data/pool-52-build-7380-rel_intellitouch-i73_56.json b/tests/data/slpy-0100_pool-52-build-7380-rel_intellitouch-i73_56.json similarity index 92% rename from tests/data/pool-52-build-7380-rel_intellitouch-i73_56.json rename to tests/data/slpy-0100_pool-52-build-7380-rel_intellitouch-i73_56.json index 8e1dddc..fdceed5 100644 --- a/tests/data/pool-52-build-7380-rel_intellitouch-i73_56.json +++ b/tests/data/slpy-0100_pool-52-build-7380-rel_intellitouch-i73_56.json @@ -3,7 +3,9 @@ "adapter": { "firmware": { "name": "Protocol Adapter Firmware", - "value": "POOL: 5.2 Build 738.0 Rel" + "value": "POOL: 5.2 Build 738.0 Rel", + "major": 5.2, + "minor": 738.0 } }, "controller": { @@ -144,7 +146,7 @@ }, "air_temperature": { "name": "Air Temperature", - "value": 100, + "value": 91, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -361,6 +363,12 @@ "state_type": "measurement" }, "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Pool Pump GPM Now", + "value": 255, + "unit": "gpm", + "state_type": "measurement" + }, "unknown_at_offset_24": 255, "preset": { "0": { @@ -503,7 +511,7 @@ "name": "Pool", "last_temperature": { "name": "Last Pool Temperature", - "value": 92, + "value": 86, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -551,7 +559,7 @@ "name": "Spa", "last_temperature": { "name": "Last Spa Temperature", - "value": 93, + "value": 84, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -569,19 +577,19 @@ }, "heat_setpoint": { "name": "Spa Heat Set Point", - "value": 101, + "value": 102, "unit": "°F", "device_type": "temperature" }, "cool_setpoint": { "name": "Spa Cool Set Point", - "value": 100, + "value": 91, "unit": "°F", "device_type": "temperature" }, "heat_mode": { "name": "Spa Heat Mode", - "value": 0, + "value": 3, "device_type": "enum", "enum_options": [ "Off", @@ -637,32 +645,44 @@ "ph_setpoint": { "name": "pH Setpoint", "value": 0.0, - "unit": "pH" + "unit": "pH", + "min_setpoint": 7.2, + "max_setpoint": 7.6 }, "orp_setpoint": { "name": "ORP Setpoint", "value": 0, - "unit": "mV" + "unit": "mV", + "min_setpoint": 400, + "max_setpoint": 800 }, - "calcium_harness": { + "calcium_hardness": { "name": "Calcium Hardness", "value": 0, - "unit": "ppm" + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 }, "cya": { "name": "Cyanuric Acid", "value": 0, - "unit": "ppm" + "unit": "ppm", + "min_setpoint": 0, + "max_setpoint": 201 }, "total_alkalinity": { "name": "Total Alkalinity", "value": 0, - "unit": "ppm" + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 }, "salt_tds_ppm": { "name": "Salt/TDS", "value": 0, - "unit": "ppm" + "unit": "ppm", + "min_setpoint": 500, + "max_setpoint": 6500 }, "probe_is_celsius": 0, "flags": 0 @@ -778,7 +798,9 @@ }, "firmware": { "name": "IntelliChem Firmware", - "value": "0.000" + "value": "0.000", + "major": 0, + "minor": 0 }, "water_balance": { "flags": 0, @@ -818,7 +840,7 @@ "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 0 }, "spa_setpoint": { @@ -827,22 +849,41 @@ "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 1 }, "super_chlor_timer": { "name": "Super Chlorination Timer", "value": 0, "unit": "hr", - "min_setpoint": 1, + "min_setpoint": 0, "max_setpoint": 72, "step": 1 } }, - "flags": 0 + "flags": 0, + "super_chlorinate": { + "name": "Super Chlorinate", + "value": 0 + } + } + }, + "version": { + "raw": { + "__type": "", + "repr": "b'\\x19\\x00\\x00\\x00POOL: 5.2 Build 738.0 Rel\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x0c\\x00\\x00\\x00'" + }, + "decoded": { + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 738.0 Rel", + "major": 5.2, + "minor": 738.0 + } + } } }, - "version": null, "config": { "raw": { "__type": "", @@ -1120,7 +1161,7 @@ "status": { "raw": { "__type": "", - "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00c\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\\\\\x00\\x00\\x00\\x00\\x00\\x00\\x00U\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00]\\x00\\x00\\x00\\x00\\x00\\x00\\x00e\\x00\\x00\\x00c\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x07\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\n\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" + "repr": "b'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00[\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00V\\x00\\x00\\x00\\x00\\x00\\x00\\x00U\\x00\\x00\\x00d\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00T\\x00\\x00\\x00\\x00\\x00\\x00\\x00f\\x00\\x00\\x00[\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x07\\x00\\x00\\x00\\xf4\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf5\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf7\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\n\\x00\\xf8\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\n\\x00\\xf9\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xfa\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'" }, "decoded": { "controller": { @@ -1154,7 +1195,7 @@ }, "air_temperature": { "name": "Air Temperature", - "value": 99, + "value": 91, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -1212,7 +1253,7 @@ "name": "Pool", "last_temperature": { "name": "Last Pool Temperature", - "value": 92, + "value": 86, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -1258,7 +1299,7 @@ "name": "Spa", "last_temperature": { "name": "Last Spa Temperature", - "value": 93, + "value": 84, "unit": "°F", "device_type": "temperature", "state_type": "measurement" @@ -1276,19 +1317,19 @@ }, "heat_setpoint": { "name": "Spa Heat Set Point", - "value": 101, + "value": 102, "unit": "°F", "device_type": "temperature" }, "cool_setpoint": { "name": "Spa Cool Set Point", - "value": 99, + "value": 91, "unit": "°F", "device_type": "temperature" }, "heat_mode": { "name": "Spa Heat Mode", - "value": 0, + "value": 3, "device_type": "enum", "enum_options": [ "Off", @@ -1600,32 +1641,44 @@ "ph_setpoint": { "name": "pH Setpoint", "value": 0.0, - "unit": "pH" + "unit": "pH", + "min_setpoint": 7.2, + "max_setpoint": 7.6 }, "orp_setpoint": { "name": "ORP Setpoint", "value": 0, - "unit": "mV" + "unit": "mV", + "min_setpoint": 400, + "max_setpoint": 800 }, - "calcium_harness": { + "calcium_hardness": { "name": "Calcium Hardness", "value": 0, - "unit": "ppm" + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 }, "cya": { "name": "Cyanuric Acid", "value": 0, - "unit": "ppm" + "unit": "ppm", + "min_setpoint": 0, + "max_setpoint": 201 }, "total_alkalinity": { "name": "Total Alkalinity", "value": 0, - "unit": "ppm" + "unit": "ppm", + "min_setpoint": 25, + "max_setpoint": 800 }, "salt_tds_ppm": { "name": "Salt/TDS", "value": 0, - "unit": "ppm" + "unit": "ppm", + "min_setpoint": 500, + "max_setpoint": 6500 }, "probe_is_celsius": 0, "flags": 0 @@ -1741,7 +1794,9 @@ }, "firmware": { "name": "IntelliChem Firmware", - "value": "0.000" + "value": "0.000", + "major": 0, + "minor": 0 }, "water_balance": { "flags": 0, @@ -1789,7 +1844,7 @@ "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 0 }, "spa_setpoint": { @@ -1798,21 +1853,26 @@ "unit": "%", "min_setpoint": 0, "max_setpoint": 100, - "step": 5, + "step": 1, "body_type": 1 }, "super_chlor_timer": { "name": "Super Chlorination Timer", "value": 0, "unit": "hr", - "min_setpoint": 1, + "min_setpoint": 0, "max_setpoint": 72, "step": 1 } }, - "flags": 0 + "flags": 0, + "super_chlorinate": { + "name": "Super Chlorinate", + "value": 0 + } } } }, - "color": null + "color": null, + "date_time": null } \ No newline at end of file diff --git a/tests/test_decode.py b/tests/test_decode.py index 3c589f0..eb6b8c2 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +import pytest from screenlogicpy.data import ScreenLogicResponseCollection from screenlogicpy.requests.config import decode_pool_config @@ -9,65 +9,71 @@ from screenlogicpy.requests.utility import makeMessage, takeMessages from screenlogicpy.requests.gateway import decode_version +from tests.conftest import load_response_collections -def test_decode_version(response_collection: ScreenLogicResponseCollection): - data = {} - decode_version(response_collection.version.raw, data) - assert data == response_collection.version.decoded +def get_rc_id(rc_tup: tuple[str, ScreenLogicResponseCollection]): + return rc_tup[0] -def test_decode_config(response_collection: ScreenLogicResponseCollection): - data = {} - decode_pool_config(response_collection.config.raw, data) +@pytest.fixture(params=load_response_collections(), ids=get_rc_id) +def response_collection(request): + return request.param[1] - assert data == response_collection.config.decoded +class TestDecode: + def test_decode_version(self, response_collection: ScreenLogicResponseCollection): + data = {} + decode_version(response_collection.version.raw, data) -def test_decode_status(response_collection: ScreenLogicResponseCollection): - data = {} - decode_pool_status(response_collection.status.raw, data) + assert data == response_collection.version.decoded - assert data == response_collection.status.decoded + def test_decode_config(self, response_collection: ScreenLogicResponseCollection): + data = {} + decode_pool_config(response_collection.config.raw, data) + assert data == response_collection.config.decoded -def test_decode_pump(response_collection: ScreenLogicResponseCollection): - for pump_num, pump_response in enumerate(response_collection.pumps): + def test_decode_status(self, response_collection: ScreenLogicResponseCollection): data = {} - decode_pump_status(pump_response.raw, data, pump_num) - - assert data == pump_response.decoded + decode_pool_status(response_collection.status.raw, data) + assert data == response_collection.status.decoded -def test_decode_chemistry(response_collection: ScreenLogicResponseCollection): - data = {} - decode_chemistry(response_collection.chemistry.raw, data) + def test_decode_pump(self, response_collection: ScreenLogicResponseCollection): + for pump_num, pump_response in enumerate(response_collection.pumps): + data = {} + decode_pump_status(pump_response.raw, data, pump_num) - assert data == response_collection.chemistry.decoded + assert data == pump_response.decoded + def test_decode_chemistry(self, response_collection: ScreenLogicResponseCollection): + data = {} + decode_chemistry(response_collection.chemistry.raw, data) -def test_decode_scg(response_collection: ScreenLogicResponseCollection): - data = {} - decode_scg_config(response_collection.scg.raw, data) + assert data == response_collection.chemistry.decoded - assert data == response_collection.scg.decoded + def test_decode_scg(self, response_collection: ScreenLogicResponseCollection): + data = {} + decode_scg_config(response_collection.scg.raw, data) + assert data == response_collection.scg.decoded -def test_takeMessages(response_collection: ScreenLogicResponseCollection): - mID1 = 27 - mCD1 = 12593 - mDT1 = response_collection.chemistry.raw - mID2 = 28 - mCD2 = 12573 - mDT2 = response_collection.scg.raw + def test_takeMessages(self, response_collection: ScreenLogicResponseCollection): + mID1 = 27 + mCD1 = 12593 + mDT1 = response_collection.chemistry.raw + mID2 = 28 + mCD2 = 12573 + mDT2 = response_collection.scg.raw - joined = makeMessage(mID1, mCD1, mDT1) + makeMessage(mID2, mCD2, mDT2) - messages = takeMessages(joined) + joined = makeMessage(mID1, mCD1, mDT1) + makeMessage(mID2, mCD2, mDT2) + messages = takeMessages(joined) - assert messages[0][0] == mID1 - assert messages[0][1] == mCD1 - assert messages[0][2] == mDT1 + assert messages[0][0] == mID1 + assert messages[0][1] == mCD1 + assert messages[0][2] == mDT1 - assert messages[1][0] == mID2 - assert messages[1][1] == mCD2 - assert messages[1][2] == mDT2 + assert messages[1][0] == mID2 + assert messages[1][1] == mCD2 + assert messages[1][2] == mDT2 From 344cf75e63030c38ba7df81c3291c9ab133c6734 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:25:34 -0800 Subject: [PATCH 50/53] Remove unintended spacing --- .github/workflows/python-package.yml | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 20e42e4..3889e5f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -17,22 +17,22 @@ jobs: python-version: ["3.10", 3.11] steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install flake8 pytest pytest-cov pytest-asyncio async_timeout - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test and check coverage with pytest - run: | - pytest --cov=screenlogicpy --cov-report term-missing + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest pytest-cov pytest-asyncio async_timeout + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test and check coverage with pytest + run: | + pytest --cov=screenlogicpy --cov-report term-missing From 17ea75e01cf3b200a79bcc244503fada9e3a7168 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:35:07 -0800 Subject: [PATCH 51/53] Update actions --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3889e5f..d3824f6 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -17,9 +17,9 @@ jobs: python-version: ["3.10", 3.11] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 0970a23b5c4eb942a9432c9b7a8c4702f8c574c6 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:44:41 -0800 Subject: [PATCH 52/53] Fix path for response collections --- tests/conftest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 940ff5b..eda30fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ from screenlogicpy import ScreenLogicGateway, __version__ as sl_version from screenlogicpy.cli import file_format +from screenlogicpy.const.common import ScreenLogicError from screenlogicpy.data import ( ScreenLogicResponseCollection, deconstruct_response_collection, @@ -33,11 +34,16 @@ def load_response_collections(filenames: list[str] | None = None): - dir = "tests\\data\\" - files = filenames or glob(f"slpy-{file_format(sl_version)}*.json", root_dir=dir) + dir = "tests/data/" + file_filter = f"slpy-{file_format(sl_version)}*.json" + files = filenames or glob(file_filter, root_dir=dir) response_collections = [] for file in files: response_collections.append((file, import_response_collection(f"{dir}{file}"))) + if not response_collections: + raise ScreenLogicError( + f"No response collections imported from {dir}{file_filter}" + ) return response_collections From 14faae67e5197f939204ed5bce40e283c08cc01e Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Tue, 28 Nov 2023 00:42:54 -0800 Subject: [PATCH 53/53] Force all datetime objects to utc --- screenlogicpy/cli.py | 12 +++++++----- screenlogicpy/requests/datetime.py | 4 ++-- screenlogicpy/requests/utility.py | 10 +++++++--- ...00_pool-52-build-7380-rel_easytouch2-8_32824.json | 4 ++-- tests/test_cli.py | 6 +++--- tests/test_utility.py | 8 +++++--- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/screenlogicpy/cli.py b/screenlogicpy/cli.py index 597f085..47e26fc 100644 --- a/screenlogicpy/cli.py +++ b/screenlogicpy/cli.py @@ -1,6 +1,6 @@ import argparse import asyncio -from datetime import datetime +from datetime import datetime, timezone import json import logging import string @@ -257,9 +257,9 @@ async def async_get_date_time(): DEVICE.CONTROLLER, GROUP.DATE_TIME, VALUE.TIMESTAMP, strict=True ) if format is None: - print(datetime.fromtimestamp(timestamp)) + print(datetime.fromtimestamp(timestamp, tz=timezone.utc).ctime()) else: - print(datetime.fromtimestamp(timestamp).strftime(format)) + print(datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime(format)) return 0 async def async_get_auto_dst(): @@ -285,7 +285,7 @@ async def async_set_date_time(): auto_dst is None, ) ): - date_time = datetime.now() + date_time = datetime.now(tz=timezone.utc) await gateway.async_get_datetime() await gateway.async_set_date_time(date_time=date_time, auto_dst=auto_dst) @@ -294,7 +294,9 @@ async def async_set_date_time(): timestamp = gateway.get_data( DEVICE.CONTROLLER, GROUP.DATE_TIME, VALUE.TIMESTAMP, strict=True ) - print(f"Controller time now: {datetime.fromtimestamp(timestamp)}") + print( + f"Controller time now: {datetime.fromtimestamp(timestamp, tz=timezone.utc).ctime()}" + ) return 0 async def async_export_data_collection(): diff --git a/screenlogicpy/requests/datetime.py b/screenlogicpy/requests/datetime.py index 0caa04f..934c385 100644 --- a/screenlogicpy/requests/datetime.py +++ b/screenlogicpy/requests/datetime.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone import struct from ..const.common import ScreenLogicResponseError @@ -20,7 +20,7 @@ async def async_request_date_time( def decode_date_time(buffer: bytes, data: dict): - host_time = datetime.now() + host_time = datetime.now(tz=timezone.utc) controller: dict = data.setdefault(DEVICE.CONTROLLER, {}) date_time: dict = controller.setdefault(GROUP.DATE_TIME, {}) diff --git a/screenlogicpy/requests/utility.py b/screenlogicpy/requests/utility.py index dcdeef6..306f2a1 100644 --- a/screenlogicpy/requests/utility.py +++ b/screenlogicpy/requests/utility.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone import struct import sys from typing import Any @@ -94,7 +94,9 @@ def encodeMessageTime(time_to_encode: datetime): def decodeMessageTime(data: bytes) -> datetime: year, month, _, day, hour, minute, second, millisecond = struct.unpack("<8H", data) - return datetime(year, month, day, hour, minute, second, millisecond * 1000) + return datetime( + year, month, day, hour, minute, second, millisecond * 1000, tzinfo=timezone.utc + ) def getSome(format, buff, offset) -> tuple[Any, int]: @@ -136,7 +138,9 @@ def getTime(buff: bytes, offset: int) -> tuple[datetime, int]: ) new_offset = offset + struct.calcsize(fmt) return ( - datetime(year, month, day, hour, minute, second, millisecond * 1000), + datetime( + year, month, day, hour, minute, second, millisecond * 1000, timezone.utc + ), new_offset, ) diff --git a/tests/data/slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json b/tests/data/slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json index caa124f..02913a7 100644 --- a/tests/data/slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json +++ b/tests/data/slpy-0100_pool-52-build-7380-rel_easytouch2-8_32824.json @@ -193,7 +193,7 @@ } }, "date_time": { - "timestamp": 1700517969.0, + "timestamp": 1700489169.0, "timestamp_host": 1700517812.0, "auto_dst": { "name": "Automatic Daylight Saving Time", @@ -2100,7 +2100,7 @@ "decoded": { "controller": { "date_time": { - "timestamp": 1700517969.0, + "timestamp": 1700489169.0, "timestamp_host": 1700517812.0, "auto_dst": { "name": "Automatic Daylight Saving Time", diff --git a/tests/test_cli.py b/tests/test_cli.py index d45de5a..e12ad81 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -302,7 +302,7 @@ async def test_set_chemistry( @pytest.mark.parametrize( "arguments, return_code, expected_output", [ - ("get date-time", 0, "2023-11-20 14:06:09"), + ("get date-time", 0, "Mon Nov 20 14:06:09 2023"), ( "-v get dt -f %I:%M%p", 0, @@ -313,12 +313,12 @@ async def test_set_chemistry( ( "set date-time --date-time 2023-11-12T16:24:00", 0, - "Controller time now: 2023-11-20 14:06:09", + "Controller time now: Mon Nov 20 14:06:09 2023", ), ( "set date-time", 0, - "Controller time now: 2023-11-20 14:06:09", + "Controller time now: Mon Nov 20 14:06:09 2023", ), ], ) diff --git a/tests/test_utility.py b/tests/test_utility.py index a17ae95..1ceb1fc 100644 --- a/tests/test_utility.py +++ b/tests/test_utility.py @@ -1,12 +1,14 @@ import asyncio -from datetime import datetime +from datetime import datetime, timezone import pytest from screenlogicpy.requests.utility import decodeMessageTime, encodeMessageTime def test_encode_decode_time(): - time = datetime(2023, 11, 19, 18, 15, 36, 175759) + time = datetime(2023, 11, 19, 18, 15, 36, 175759, tzinfo=timezone.utc) data = encodeMessageTime(time) assert data == b"\xe7\x07\x0b\x00\x06\x00\x13\x00\x12\x00\x0f\x00\x00\x00\xaf\x00" - assert decodeMessageTime(data) == datetime(2023, 11, 19, 18, 15, 0, 175000) + assert decodeMessageTime(data) == datetime( + 2023, 11, 19, 18, 15, 0, 175000, tzinfo=timezone.utc + )