diff --git a/.devcontainer.json b/.devcontainer.json index 2de1e36..b49949b 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -1,6 +1,6 @@ { "name": "Python 3", - "image": "mcr.microsoft.com/devcontainers/python:0-3.10-bullseye", + "image": "mcr.microsoft.com/devcontainers/python:3.12", "postCreateCommand": "scripts/setup", "customizations": { "vscode": { @@ -37,6 +37,8 @@ "dialout" ], "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": {} + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "moby": false + } } } \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 74767fc..fb7f472 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10"] + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/.vscode/settings.json b/.vscode/settings.json index fb237af..7099f9c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,5 @@ { "python.formatting.provider": "black", - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, "python.pythonPath": "/usr/local/bin/python", "python.testing.pytestEnabled": true, "mos.port": "/dev/ttyUSB0", diff --git a/pyproject.toml b/pyproject.toml index 6e0399f..c6cb1af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,11 @@ build-backend = "setuptools.build_meta" [project] name = "pytboss" # version = "2023.4.0" -description = "Python library for interacting with Pitboss grills and smokers." +description = "Python library for interacting with PitBoss grills and smokers." authors = [ {name = "David Knowles", email = "dknowles2@gmail.com"}, ] -dependencies = ["bleak", "bleak_retry_connector", "js2py"] +dependencies = ["bleak", "bleak_retry_connector", "dukpy"] requires-python = ">=3.10" dynamic = ["readme", "version"] license = {text = "Apache License 2.0"} @@ -20,9 +20,9 @@ classifiers = [ ] [project.urls] -"Homepage" = "https://github.com/dknowles2/pyschlage" -"Source Code" = "https://github.com/dknowles2/pyschlage" -"Bug Reports" = "https://github.com/dknowles2/pyschlage/issues" +"Homepage" = "https://github.com/dknowles2/pytboss" +"Source Code" = "https://github.com/dknowles2/pytboss" +"Bug Reports" = "https://github.com/dknowles2/pytboss/issues" [tool.setuptools] platforms = ["any"] diff --git a/pytboss/api.py b/pytboss/api.py index e6cb252..b7088eb 100644 --- a/pytboss/api.py +++ b/pytboss/api.py @@ -194,7 +194,7 @@ async def _on_state_received(self, payload: str): return async with self._lock: - self._state.update(state.to_dict()) + self._state.update(state) # TODO: Run callbacks concurrently # TODO: Send copies of state so subscribers can't modify it for callback in self._state_callbacks: diff --git a/pytboss/grills.py b/pytboss/grills.py index 91da43a..8a849af 100644 --- a/pytboss/grills.py +++ b/pytboss/grills.py @@ -7,47 +7,45 @@ import json from typing import Any -import js2py +import dukpy from .exceptions import InvalidGrill _COMMAND_JS_TMPL = """\ -function() { - let formatHex = function(n) { - let t = '0' + parseInt(n).toString(16); +function command() { + var formatHex = function(n) { + var t = '0' + parseInt(n).toString(16); return t.substring(t.length - 2) }; - let formatDecimal = function(n) { - let t = '000' + parseInt(n).toString(10); + var formatDecimal = function(n) { + var t = '000' + parseInt(n).toString(10); return t.substring(t.length - 3); }; %s } +command.apply(null, dukpy['args']); """ _CONTROLLER_JS_TMPL = """\ -// Basic polyfill for String.startsWith. -String.prototype.startsWith = function(search, pos){ - return this.slice(pos || 0, search.length) === search; -}; -function(message) { - let convertTemperature = function(parts, startIndex) { - let temp = ( +function parse(message) { + var convertTemperature = function(parts, startIndex) { + var temp = ( parts[startIndex] * 100 + parts[startIndex + 1] * 10 + parts[startIndex + 2] ); return temp === 960 ? null : temp; }; - let parseHexMessage = function(_data) { - const parsed = []; - for (let i = 0; i < _data.length; i+=2) { - parsed.push(parseInt(_data.substring(i, i+2), 16)); + var parseHexMessage = function(data) { + var parsed = []; + for (var i = 0; i < data.length; i+=2) { + parsed.push(parseInt(data.substring(i, i+2), 16)); } return parsed; }; %s } +parse(dukpy['message']); """ @@ -70,11 +68,14 @@ class Command: @classmethod def from_dict(cls, cmd_dict) -> "Command": """Creates a Command from a JSON dict.""" + js_func = cmd_dict["function"] + if js_func: + js_func = js_func.replace("let ", "var ") return cls( name=cmd_dict["name"], slug=cmd_dict["slug"], _hex=cmd_dict["hexadecimal"], - _js_func=cmd_dict["function"], + _js_func=js_func, ) def __call__(self, *args) -> str: @@ -85,7 +86,7 @@ def __call__(self, *args) -> str: if self._js_func is None: raise NotImplementedError - return js2py.eval_js(_COMMAND_JS_TMPL % self._js_func)(*args) + return dukpy.evaljs(_COMMAND_JS_TMPL % self._js_func, args=args) @dataclass @@ -107,27 +108,31 @@ class ControlBoard: @classmethod def from_dict(cls, ctrl_dict) -> "ControlBoard": """Creates a ControlBoard from a JSON dict.""" + status_js_func = ctrl_dict["status_function"] + temperatures_js_func = ctrl_dict["temperature_function"] return cls( name=ctrl_dict["name"], commands={ c["slug"]: Command.from_dict(c) for c in ctrl_dict["control_board_commands"] }, - _status_js_func=ctrl_dict["status_function"], - _temperatures_js_func=ctrl_dict["temperature_function"], + _status_js_func=status_js_func.replace("let ", "var "), + _temperatures_js_func=temperatures_js_func.replace("let ", "var "), ) def parse_status(self, message) -> dict | None: """Parses a status message.""" if not self._status_js_func: raise NotImplementedError - return js2py.eval_js(_CONTROLLER_JS_TMPL % self._status_js_func)(message) + return dukpy.evaljs(_CONTROLLER_JS_TMPL % self._status_js_func, message=message) def parse_temperatures(self, message) -> dict | None: """Parses a temperatures message.""" if not self._temperatures_js_func: raise NotImplementedError - return js2py.eval_js(_CONTROLLER_JS_TMPL % self._temperatures_js_func)(message) + return dukpy.evaljs( + _CONTROLLER_JS_TMPL % self._temperatures_js_func, message=message + ) @dataclass diff --git a/requirements.txt b/requirements.txt index 31a0771..cd0f8bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ bleak==0.21.1 bleak_retry_connector==3.4.0 -js2py==0.74 +dukpy==0.3.0 diff --git a/tests/test_ble.py b/tests/test_ble.py index f31dccc..559a1d7 100644 --- a/tests/test_ble.py +++ b/tests/test_ble.py @@ -2,6 +2,7 @@ import json from unittest import mock +import bleak from bleak_retry_connector import BleakClientWithServiceCache from pytboss import ble @@ -29,19 +30,13 @@ async def test_connect_disconnect( @mock.patch("bleak_retry_connector.establish_connection") -@mock.patch("bleak.BleakClient", spec=True) -@mock.patch("bleak.BleakClient", spec=True) -@mock.patch("bleak.BLEDevice", spec=True) -@mock.patch("bleak.BLEDevice", spec=True) -async def test_reset_device( - mock_old_device, - mock_new_device, - mock_old_bleak_client, - mock_new_bleak_client, - mock_establish_connection, -): +async def test_reset_device(mock_establish_connection): + mock_old_device = mock.create_autospec(bleak.BLEDevice) + mock_new_device = mock.create_autospec(bleak.BLEDevice) mock_old_device.name = "OLD DEVICE NAME" mock_new_device.name = "NEW DEVICE NAME" + mock_old_bleak_client = mock.create_autospec(bleak.BleakClient) + mock_new_bleak_client = mock.create_autospec(bleak.BleakClient) mock_establish_connection.return_value = mock_old_bleak_client conn = ble.BleConnection(mock_old_device) @@ -75,19 +70,13 @@ async def test_reset_device( @mock.patch("bleak_retry_connector.establish_connection") -@mock.patch("bleak.BleakClient", spec=True) -@mock.patch("bleak.BleakClient", spec=True) -@mock.patch("bleak.BLEDevice", spec=True) -@mock.patch("bleak.BLEDevice", spec=True) -async def test_reset_device_with_debug_log_subscription( - mock_old_device, - mock_new_device, - mock_old_bleak_client, - mock_new_bleak_client, - mock_establish_connection, -): +async def test_reset_device_with_debug_log_subscription(mock_establish_connection): + mock_old_device = mock.create_autospec(bleak.BLEDevice) + mock_new_device = mock.create_autospec(bleak.BLEDevice) mock_old_device.name = "OLD DEVICE NAME" mock_new_device.name = "NEW DEVICE NAME" + mock_old_bleak_client = mock.create_autospec(bleak.BleakClient) + mock_new_bleak_client = mock.create_autospec(bleak.BleakClient) mock_establish_connection.return_value = mock_old_bleak_client conn = ble.BleConnection(mock_old_device)