From 8fb544ab0a1627ad0fd15de032e34a54e73abc05 Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sat, 18 Dec 2021 18:11:46 -0500 Subject: [PATCH 1/8] Added API handling --- .vscode/settings.json | 5 +- lib/flickerstrip.py | 253 ++++++++++++++++++++++++++++++++++++++++++ lib/pattern.py | 17 +++ lib/status.py | 46 ++++++++ poetry.lock | 149 ++++++++++++++++++++++++- pyproject.toml | 3 + 6 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 lib/pattern.py create mode 100644 lib/status.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 9b38853..a022376 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,8 @@ "tests" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "python.pythonPath": "/home/evanf/.cache/pypoetry/virtualenvs/flickerstrip-py-zxDi65bL-py3.9/bin/python", + "python.linting.flake8Enabled": true, + "python.linting.enabled": true } \ No newline at end of file diff --git a/lib/flickerstrip.py b/lib/flickerstrip.py index e69de29..1044c1c 100644 --- a/lib/flickerstrip.py +++ b/lib/flickerstrip.py @@ -0,0 +1,253 @@ +import requests +from pattern import PatternMeta +from status import Status + + +class Flickerstrip: + def __init__(self, ip_address): + """The initializer for the Flickerstrip class. + + Args: + ip_address (string): The ip address of the flickerstrip. + """ + self.ip_address = ip_address + self.status = None + + def __checkResponse(self, response): + """Check if the request was succesful. + + Args: + response (requests.Response): The response from the request. + + Returns: + boolean: If the request was successful + """ + json = response.json() + respType = json["type"] + if respType == "error": + message = json["message"] + print(f"ERROR: Request failed with message \"{message}\".") + return False + elif respType == "OK": + return True + elif respType == "status": + self.status = Status.fromJSON(json) + return True + else: + print(f"ERROR: Unrecognized response type: {respType}.") + return False + + def __getRequest(self, path, params=None): + """Send a GET request to the strip. + + Args: + path (string): The path for the URL to request. + params (dictionary, optional): The query string params for the + request. Defaults to None. + + Returns: + boolean: If the request was successful + """ + resp = requests.get(f"http://{self.ip_address}/{path}", params=params) + return self.__checkResponse(resp) + + def __postRequest(self, path, json=None, data=None): + """Send a POST request to the strip. + + Args: + path (string): The path for the URL to request. + json (dictionary, optional): The JSON encodable body for the + request. Defaults to None. + data (string, optional): The raw data body. Defaults to None. + + Returns: + boolean: If the request was successful + """ + resp = requests.post(f"http://{self.ip_address}/{path}", + json=json, data=data) + return self.__checkResponse(resp) + + def forceUpdate(self): + """Force updates the status of the strip. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("status") + + def powerOn(self): + """Trigger the strip to turn the lights on. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("power/on") + + def powerOff(self): + """Trigger the strip to turn the lights off. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("power/off") + + def powerToggle(self): + """Trigger the strip to toggle the power status. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("power/toggle") + + def nextPattern(self): + """Trigger the strip to switch to the next pattern in memory. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("pattern/next") + + def freezeFrame(self, frame): + """Freeze the animation at the specified frame. + + Args: + frame (int): The frame number to freeze on. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("pattern/frame", {"value": frame}) + + def setPattern(self, index): + """Change the current pattern to the pattern at the specified index. + + Args: + index (int): The index of the pattern. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("pattern/select", {"index": index}) + + def deletePattern(self, pattern=None, index=None, id=None): + if pattern is not None: + if not isinstance(pattern, PatternMeta): + raise TypeError( + "Expected a PatternMeta object for the pattern arg.") + return self.deletePattern(id=pattern.id) + elif index is not None: + return self.__getRequest("pattern/forget", {"index": index}) + elif id is not None: + return self.__getRequest("pattern/forget", {"id": id}) + else: + raise TypeError( + "Deleting a pattern requires one of the three args.") + + def setBrightness(self, value): + """Set the brightness of the flickerstrip. + + Args: + value (int): The brightness level as an int from 0 to 100. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("brightness", {"value": value}) + + def setName(self, value): + """Set the name of the flickerstrip. + + Args: + value (string): The new name. + + Returns: + boolean: If the request was succesful + """ + return self.__postRequest("config/name", {"name": value}) + + def setGroup(self, value): + """Set the group of the flickerstrip. + + Args: + value (string): The new group. + + Returns: + boolean: If the request was succesful + """ + return self.__postRequest("config/group", {"name": value}) + + def setCycle(self, value): + """Set the cycle timer of the flickerstrip. + + Args: + value (int): How long to cycle to next pattern. + Value is handled in seconds. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("config/cycle", {"value": value}) + + def setFadeDuration(self, value): + """Set the fade timer of the flickerstrip. + + Args: + value (int): How long to fade to next pattern. + Value is handled in seconds. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("config/fade", {"value": value}) + + def setStripLength(self, value): + """Set the length of the flickerstrip. + + Args: + value (int): The length of the strip. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("config/length", {"value": value}) + + def setStripStart(self, value): + """Set the start pixel of the strip. + + This will make the flickerstrip ignore all pixels before + the given number, set to 0 for the first pixel. + + Args: + value (int): The new first pixel of the strip. + + Returns: + boolean: If the request was successful. + """ + return self.__getRequest("config/start", {"value": value}) + + def setStripEnd(self, value): + """Set the end pixel of the strip. + + This will make the flickerstrip ignore all pixels after + the given number, set to -1 for the last pixel. + + Args: + value (int): The new last pixel of the strip. + + Returns: + boolean: If the request was successful. + """ + return self.__getRequest("config/end", {"value": value}) + + def setReversed(self, value): + """Set the reversed state of the flickerstrip. + + Args: + value (boolean): If the flickerstrip should animate + patterns in reverse. + + Returns: + boolean: If the request was succesful + """ + return self.__getRequest("config/reversed", + {"value": 1 if value else 0}) diff --git a/lib/pattern.py b/lib/pattern.py new file mode 100644 index 0000000..dd7fac2 --- /dev/null +++ b/lib/pattern.py @@ -0,0 +1,17 @@ +class PatternMeta: + def __init__(self, id, name, frames, pixels, flags, fps): + self.id = id + self.name = name + self.frames = frames + self.pixels = pixels + self.flags = flags + self.fps = fps + + def fromJSON(json): + return PatternMeta( + json["id"], json["name"], json["frames"], + json["pixels"], json["flags"], json["fps"] + ) + + def fromJSONArray(array): + return map(PatternMeta.fromJSON, array) diff --git a/lib/status.py b/lib/status.py new file mode 100644 index 0000000..a5ba03a --- /dev/null +++ b/lib/status.py @@ -0,0 +1,46 @@ +from pattern import PatternMeta + + +class MemoryUsage: + def __init__(self, used, free, total): + self.used + self.free + self.total + + def fromJSON(json): + return MemoryUsage(json["used"], json["free"], json["total"]) + + +class Status: + def __init__( + self, ap, name, group, firmware, power, mac, + selectedPattern, brightness, length, start, end, + fade, isReversed, cycle, uptime, memory, patterns + ): + self.ap = ap + self.name = name + self.group = group + self.firmware = firmware + self.power = power + self.mac = mac + self.selectedPattern = selectedPattern + self.brightness = brightness + self.length = length + self.start = start + self.end = end + self.fade = fade + self.isReversed = isReversed + self.cycle = cycle + self.uptime = uptime + self.memory = memory + self.patterns = patterns + + def fromJSON(json): + return Status( + json["ap"], json["name"], json["group"], json["firmware"], + json["power"] == 1, json["mac"], json["selectedPattern"], + json["brightness"], json["length"], json["start"], json["end"], + json["fade"], json["reversed"] == 1, json["cycle"] == 1, + json["uptime"], MemoryUsage.fromJSON(json["memory"]), + PatternMeta.fromJSONArray(json["patterns"]) + ) diff --git a/poetry.lock b/poetry.lock index bfd86a2..893ba44 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,6 +20,37 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] +[[package]] +name = "autopep8" +version = "1.6.0" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycodestyle = ">=2.8.0" +toml = "*" + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.9" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + [[package]] name = "colorama" version = "0.4.4" @@ -28,6 +59,27 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "iniconfig" version = "1.1.1" @@ -36,6 +88,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "packaging" version = "21.3" @@ -67,6 +127,22 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pyparsing" version = "3.0.6" @@ -111,6 +187,24 @@ python-versions = ">=3.6" docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-virtualenv", "pytest-black (>=0.3.7)", "pytest-mypy"] +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + [[package]] name = "toml" version = "0.10.2" @@ -119,10 +213,23 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "571d381d2064ea8457d036b507628156c663014be7afe579d6f8f0247353665d" +content-hash = "7a9fae5232c9103cdf55efa82df2a2e366af9c25bc9e95144193e5d33674b486" [metadata.files] atomicwrites = [ @@ -133,14 +240,38 @@ attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] +autopep8 = [ + {file = "autopep8-1.6.0-py2.py3-none-any.whl", hash = "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f"}, + {file = "autopep8-1.6.0.tar.gz", hash = "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, +] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +flake8 = [ + {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, + {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -153,6 +284,14 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] pyparsing = [ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, @@ -165,7 +304,15 @@ pytest-runner = [ {file = "pytest-runner-5.3.1.tar.gz", hash = "sha256:0fce5b8dc68760f353979d99fdd6b3ad46330b6b1837e2077a89ebcf204aac91"}, {file = "pytest_runner-5.3.1-py3-none-any.whl", hash = "sha256:85f93af814438ee322b4ea08fe3f5c2ad53b253577f3bd84b2ad451fee450ac5"}, ] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +urllib3 = [ + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, +] diff --git a/pyproject.toml b/pyproject.toml index 0b3dec3..8f2f6c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,10 +6,13 @@ authors = ["Evan Fiordeliso "] [tool.poetry.dependencies] python = "^3.9" +requests = "^2.26.0" [tool.poetry.dev-dependencies] pytest = "^6.2.5" pytest-runner = "^5.3.1" +flake8 = "^4.0.1" +autopep8 = "^1.6.0" [build-system] requires = ["poetry-core>=1.0.0"] From fa438c826668c96678fc3b9713e93628eac3761f Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sat, 18 Dec 2021 19:19:09 -0500 Subject: [PATCH 2/8] Move src folder and added ssdp discovery --- .gitignore | 183 +++++++++++++++++++++++ {lib => flickerstrip_py}/__init__.py | 0 flickerstrip_py/discovery.py | 18 +++ {lib => flickerstrip_py}/flickerstrip.py | 0 {lib => flickerstrip_py}/pattern.py | 0 flickerstrip_py/ssdp.py | 66 ++++++++ {lib => flickerstrip_py}/status.py | 0 poetry.lock | 14 +- pyproject.toml | 1 + 9 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 .gitignore rename {lib => flickerstrip_py}/__init__.py (100%) create mode 100644 flickerstrip_py/discovery.py rename {lib => flickerstrip_py}/flickerstrip.py (100%) rename {lib => flickerstrip_py}/pattern.py (100%) create mode 100644 flickerstrip_py/ssdp.py rename {lib => flickerstrip_py}/status.py (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf7cd9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,183 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,python +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,linux,python + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +!.vscode/*.code-snippets + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,python + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + diff --git a/lib/__init__.py b/flickerstrip_py/__init__.py similarity index 100% rename from lib/__init__.py rename to flickerstrip_py/__init__.py diff --git a/flickerstrip_py/discovery.py b/flickerstrip_py/discovery.py new file mode 100644 index 0000000..2bba902 --- /dev/null +++ b/flickerstrip_py/discovery.py @@ -0,0 +1,18 @@ +from ssdpy import SSDPClient + + +class FlickerstripDiscoveryClient: + def __init__(self): + self.client = SSDPClient() + + def discover(self): + print("Discovering devices...") + devices = self.client.m_search("ssdp:all") + print(f"Discovered {len(devices)} devices.") + for device in devices: + print(device) + + +if __name__ == "__main__": + client = FlickerstripDiscoveryClient() + client.discover() diff --git a/lib/flickerstrip.py b/flickerstrip_py/flickerstrip.py similarity index 100% rename from lib/flickerstrip.py rename to flickerstrip_py/flickerstrip.py diff --git a/lib/pattern.py b/flickerstrip_py/pattern.py similarity index 100% rename from lib/pattern.py rename to flickerstrip_py/pattern.py diff --git a/flickerstrip_py/ssdp.py b/flickerstrip_py/ssdp.py new file mode 100644 index 0000000..9bfe9bb --- /dev/null +++ b/flickerstrip_py/ssdp.py @@ -0,0 +1,66 @@ +# Copyright 2014 Dan Krause, Python 3 hack 2016 Adam Baxter +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import socket +import http.client +import io + + +class SSDPResponse(object): + class _FakeSocket(io.BytesIO): + def makefile(self, *args, **kw): + return self + + def __init__(self, response): + r = http.client.HTTPResponse(self._FakeSocket(response)) + r.begin() + self.location = r.getheader("location") + self.usn = r.getheader("usn") + self.st = r.getheader("st") + self.cache = r.getheader("cache-control").split("=")[1] + + def __repr__(self): + return ""\ + .format(**self.__dict__) + + +def discover(service, timeout=5, retries=1, mx=3): + group = ("239.255.255.250", 1900) + message = "\r\n".join([ + 'M-SEARCH * HTTP/1.1', + 'HOST: {0}:{1}', + 'MAN: "ssdp:discover"', + 'ST: {st}', 'MX: {mx}', '', '']) + socket.setdefaulttimeout(timeout) + responses = {} + for _ in range(retries): + sock = socket.socket( + socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + message_bytes = message.format( + *group, st=service, mx=mx).encode('utf-8') + sock.sendto(message_bytes, group) + + while True: + try: + response = SSDPResponse(sock.recv(1024)) + responses[response.location] = response + except socket.timeout: + break + return list(responses.values()) + +# Example: +# import ssdp +# ssdp.discover("roku:ecp") diff --git a/lib/status.py b/flickerstrip_py/status.py similarity index 100% rename from lib/status.py rename to flickerstrip_py/status.py diff --git a/poetry.lock b/poetry.lock index 893ba44..0a6e2ab 100644 --- a/poetry.lock +++ b/poetry.lock @@ -205,6 +205,14 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "ssdpy" +version = "0.4.1" +description = "Python SSDP library" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<4" + [[package]] name = "toml" version = "0.10.2" @@ -229,7 +237,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "7a9fae5232c9103cdf55efa82df2a2e366af9c25bc9e95144193e5d33674b486" +content-hash = "3a12befea5c0f2cdf0b7779be1a2af0ff1d396566df3dfc771bf2e93c5f83c38" [metadata.files] atomicwrites = [ @@ -308,6 +316,10 @@ requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] +ssdpy = [ + {file = "ssdpy-0.4.1-py2.py3-none-any.whl", hash = "sha256:f2a84915140a8df9d9432e35a487c8712fe23158d278f90f72ed5bd49fcf2bb5"}, + {file = "ssdpy-0.4.1.tar.gz", hash = "sha256:4971c6a010f77f147ecdbec9593f2d9187c3fb63658b0f5ec08f4c7be2387425"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, diff --git a/pyproject.toml b/pyproject.toml index 8f2f6c0..ab984a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ authors = ["Evan Fiordeliso "] [tool.poetry.dependencies] python = "^3.9" requests = "^2.26.0" +ssdpy = "^0.4.1" [tool.poetry.dev-dependencies] pytest = "^6.2.5" From 5665d57191745413dc0570545a80ba7d6f724d2c Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sat, 18 Dec 2021 20:52:51 -0500 Subject: [PATCH 3/8] Finish discovery method and rename all methods --- .vscode/settings.json | 1 - flickerstrip_py/discovery.py | 23 ++++++-- flickerstrip_py/flickerstrip.py | 96 ++++++++++++++++----------------- flickerstrip_py/pattern.py | 14 +++-- flickerstrip_py/ssdp.py | 66 ----------------------- flickerstrip_py/status.py | 39 ++++++++++---- 6 files changed, 104 insertions(+), 135 deletions(-) delete mode 100644 flickerstrip_py/ssdp.py diff --git a/.vscode/settings.json b/.vscode/settings.json index a022376..290193e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,6 @@ ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "python.pythonPath": "/home/evanf/.cache/pypoetry/virtualenvs/flickerstrip-py-zxDi65bL-py3.9/bin/python", "python.linting.flake8Enabled": true, "python.linting.enabled": true } \ No newline at end of file diff --git a/flickerstrip_py/discovery.py b/flickerstrip_py/discovery.py index 2bba902..b51933c 100644 --- a/flickerstrip_py/discovery.py +++ b/flickerstrip_py/discovery.py @@ -1,18 +1,31 @@ from ssdpy import SSDPClient +import re +import json +from flickerstrip import Flickerstrip class FlickerstripDiscoveryClient: def __init__(self): - self.client = SSDPClient() + self.client = SSDPClient(timeout=10) + + def __discovered(self, device): + loc = device["location"] + result = re.search("http://(.*):80/description.xml", loc) + ip_address = result.group(1) + return Flickerstrip(ip_address) def discover(self): print("Discovering devices...") devices = self.client.m_search("ssdp:all") - print(f"Discovered {len(devices)} devices.") - for device in devices: - print(device) + print(f"Discovered {len(devices)} device(s).") + filtered = [d for d in devices if "Flickerstrip" in d["server"]] + print(f"Discovered {len(filtered)} flickerstrip(s).") + mapped = [self.__discovered(d) for d in filtered] + return mapped if __name__ == "__main__": client = FlickerstripDiscoveryClient() - client.discover() + flickerstrips = client.discover() + flickerstrips[0].force_update() + print(json.dumps(flickerstrips[0].status.to_json())) diff --git a/flickerstrip_py/flickerstrip.py b/flickerstrip_py/flickerstrip.py index 1044c1c..83f8d45 100644 --- a/flickerstrip_py/flickerstrip.py +++ b/flickerstrip_py/flickerstrip.py @@ -1,6 +1,6 @@ import requests -from pattern import PatternMeta from status import Status +from pattern import PatternMeta class Flickerstrip: @@ -13,7 +13,7 @@ class Flickerstrip: self.ip_address = ip_address self.status = None - def __checkResponse(self, response): + def __check_response(self, response): """Check if the request was succesful. Args: @@ -22,22 +22,22 @@ class Flickerstrip: Returns: boolean: If the request was successful """ - json = response.json() - respType = json["type"] + data = response.json() + respType = data["type"] if respType == "error": - message = json["message"] + message = data["message"] print(f"ERROR: Request failed with message \"{message}\".") return False elif respType == "OK": return True elif respType == "status": - self.status = Status.fromJSON(json) + self.status = Status.from_json(data) return True else: print(f"ERROR: Unrecognized response type: {respType}.") return False - def __getRequest(self, path, params=None): + def __get_request(self, path, params=None): """Send a GET request to the strip. Args: @@ -49,9 +49,9 @@ class Flickerstrip: boolean: If the request was successful """ resp = requests.get(f"http://{self.ip_address}/{path}", params=params) - return self.__checkResponse(resp) + return self.__check_response(resp) - def __postRequest(self, path, json=None, data=None): + def __post_request(self, path, json=None, data=None): """Send a POST request to the strip. Args: @@ -65,49 +65,49 @@ class Flickerstrip: """ resp = requests.post(f"http://{self.ip_address}/{path}", json=json, data=data) - return self.__checkResponse(resp) + return self.__check_response(resp) - def forceUpdate(self): + def force_update(self): """Force updates the status of the strip. Returns: boolean: If the request was succesful """ - return self.__getRequest("status") + return self.__get_request("status") - def powerOn(self): + def power_on(self): """Trigger the strip to turn the lights on. Returns: boolean: If the request was succesful """ - return self.__getRequest("power/on") + return self.__get_request("power/on") - def powerOff(self): + def power_off(self): """Trigger the strip to turn the lights off. Returns: boolean: If the request was succesful """ - return self.__getRequest("power/off") + return self.__get_request("power/off") - def powerToggle(self): + def power_toggle(self): """Trigger the strip to toggle the power status. Returns: boolean: If the request was succesful """ - return self.__getRequest("power/toggle") + return self.__get_request("power/toggle") - def nextPattern(self): + def next_pattern(self): """Trigger the strip to switch to the next pattern in memory. Returns: boolean: If the request was succesful """ - return self.__getRequest("pattern/next") + return self.__get_request("pattern/next") - def freezeFrame(self, frame): + def freeze_frame(self, frame): """Freeze the animation at the specified frame. Args: @@ -116,9 +116,9 @@ class Flickerstrip: Returns: boolean: If the request was succesful """ - return self.__getRequest("pattern/frame", {"value": frame}) + return self.__get_request("pattern/frame", {"value": frame}) - def setPattern(self, index): + def select_pattern(self, index): """Change the current pattern to the pattern at the specified index. Args: @@ -127,23 +127,23 @@ class Flickerstrip: Returns: boolean: If the request was succesful """ - return self.__getRequest("pattern/select", {"index": index}) + return self.__get_request("pattern/select", {"index": index}) - def deletePattern(self, pattern=None, index=None, id=None): + def delete_pattern(self, pattern=None, index=None, id=None): if pattern is not None: if not isinstance(pattern, PatternMeta): raise TypeError( "Expected a PatternMeta object for the pattern arg.") - return self.deletePattern(id=pattern.id) + return self.delete_pattern(id=pattern.id) elif index is not None: - return self.__getRequest("pattern/forget", {"index": index}) + return self.__get_request("pattern/forget", {"index": index}) elif id is not None: - return self.__getRequest("pattern/forget", {"id": id}) + return self.__get_request("pattern/forget", {"id": id}) else: raise TypeError( "Deleting a pattern requires one of the three args.") - def setBrightness(self, value): + def set_brightness(self, value): """Set the brightness of the flickerstrip. Args: @@ -152,9 +152,9 @@ class Flickerstrip: Returns: boolean: If the request was succesful """ - return self.__getRequest("brightness", {"value": value}) + return self.__get_request("brightness", {"value": value}) - def setName(self, value): + def set_name(self, value): """Set the name of the flickerstrip. Args: @@ -163,9 +163,9 @@ class Flickerstrip: Returns: boolean: If the request was succesful """ - return self.__postRequest("config/name", {"name": value}) + return self.__post_request("config/name", {"name": value}) - def setGroup(self, value): + def set_group(self, value): """Set the group of the flickerstrip. Args: @@ -174,9 +174,9 @@ class Flickerstrip: Returns: boolean: If the request was succesful """ - return self.__postRequest("config/group", {"name": value}) + return self.__post_request("config/group", {"name": value}) - def setCycle(self, value): + def set_cycle(self, value): """Set the cycle timer of the flickerstrip. Args: @@ -186,9 +186,9 @@ class Flickerstrip: Returns: boolean: If the request was succesful """ - return self.__getRequest("config/cycle", {"value": value}) + return self.__get_request("config/cycle", {"value": value}) - def setFadeDuration(self, value): + def set_fade_duration(self, value): """Set the fade timer of the flickerstrip. Args: @@ -198,20 +198,20 @@ class Flickerstrip: Returns: boolean: If the request was succesful """ - return self.__getRequest("config/fade", {"value": value}) + return self.__get_request("config/fade", {"value": value}) - def setStripLength(self, value): + def set_strip_length(self, value): """Set the length of the flickerstrip. Args: - value (int): The length of the strip. + value (int): The length of the strip in pixels. Returns: boolean: If the request was succesful """ - return self.__getRequest("config/length", {"value": value}) + return self.__get_request("config/length", {"value": value}) - def setStripStart(self, value): + def set_strip_start(self, value): """Set the start pixel of the strip. This will make the flickerstrip ignore all pixels before @@ -223,9 +223,9 @@ class Flickerstrip: Returns: boolean: If the request was successful. """ - return self.__getRequest("config/start", {"value": value}) + return self.__get_request("config/start", {"value": value}) - def setStripEnd(self, value): + def set_strip_end(self, value): """Set the end pixel of the strip. This will make the flickerstrip ignore all pixels after @@ -237,9 +237,9 @@ class Flickerstrip: Returns: boolean: If the request was successful. """ - return self.__getRequest("config/end", {"value": value}) + return self.__get_request("config/end", {"value": value}) - def setReversed(self, value): + def set_reversed(self, value): """Set the reversed state of the flickerstrip. Args: @@ -249,5 +249,5 @@ class Flickerstrip: Returns: boolean: If the request was succesful """ - return self.__getRequest("config/reversed", - {"value": 1 if value else 0}) + return self.__get_request("config/reversed", + {"value": 1 if value else 0}) diff --git a/flickerstrip_py/pattern.py b/flickerstrip_py/pattern.py index dd7fac2..7cb4921 100644 --- a/flickerstrip_py/pattern.py +++ b/flickerstrip_py/pattern.py @@ -7,11 +7,15 @@ class PatternMeta: self.flags = flags self.fps = fps - def fromJSON(json): - return PatternMeta( + def to_json(self): + return { + "id": self.id, "name": self.name, "frames": self.frames, + "pixels": self.pixels, "flags": self.flags, "fps": self.fps + } + + @classmethod + def from_json(cls, json): + return cls( json["id"], json["name"], json["frames"], json["pixels"], json["flags"], json["fps"] ) - - def fromJSONArray(array): - return map(PatternMeta.fromJSON, array) diff --git a/flickerstrip_py/ssdp.py b/flickerstrip_py/ssdp.py deleted file mode 100644 index 9bfe9bb..0000000 --- a/flickerstrip_py/ssdp.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2014 Dan Krause, Python 3 hack 2016 Adam Baxter -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import socket -import http.client -import io - - -class SSDPResponse(object): - class _FakeSocket(io.BytesIO): - def makefile(self, *args, **kw): - return self - - def __init__(self, response): - r = http.client.HTTPResponse(self._FakeSocket(response)) - r.begin() - self.location = r.getheader("location") - self.usn = r.getheader("usn") - self.st = r.getheader("st") - self.cache = r.getheader("cache-control").split("=")[1] - - def __repr__(self): - return ""\ - .format(**self.__dict__) - - -def discover(service, timeout=5, retries=1, mx=3): - group = ("239.255.255.250", 1900) - message = "\r\n".join([ - 'M-SEARCH * HTTP/1.1', - 'HOST: {0}:{1}', - 'MAN: "ssdp:discover"', - 'ST: {st}', 'MX: {mx}', '', '']) - socket.setdefaulttimeout(timeout) - responses = {} - for _ in range(retries): - sock = socket.socket( - socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) - message_bytes = message.format( - *group, st=service, mx=mx).encode('utf-8') - sock.sendto(message_bytes, group) - - while True: - try: - response = SSDPResponse(sock.recv(1024)) - responses[response.location] = response - except socket.timeout: - break - return list(responses.values()) - -# Example: -# import ssdp -# ssdp.discover("roku:ecp") diff --git a/flickerstrip_py/status.py b/flickerstrip_py/status.py index a5ba03a..b0319b8 100644 --- a/flickerstrip_py/status.py +++ b/flickerstrip_py/status.py @@ -3,12 +3,18 @@ from pattern import PatternMeta class MemoryUsage: def __init__(self, used, free, total): - self.used - self.free - self.total + self.used = used + self.free = free + self.total = total - def fromJSON(json): - return MemoryUsage(json["used"], json["free"], json["total"]) + def to_json(self): + return { + "used": self.used, "free": self.free, "total": self.total + } + + @classmethod + def from_json(cls, json): + return cls(json["used"], json["free"], json["total"]) class Status: @@ -35,12 +41,25 @@ class Status: self.memory = memory self.patterns = patterns - def fromJSON(json): - return Status( + def to_json(self): + return { + "ap": self.ap, "name": self.name, "group": self.group, + "firmware": self.firmware, "power": 1 if self.power else 0, + "mac": self.mac, "selectedPattern": self.selectedPattern, + "brightness": self.brightness, "length": self.length, + "start": self.start, "end": self.end, "fade": self.fade, + "reversed": 1 if self.isReversed else 0, "cycle": self.cycle, + "uptime": self.uptime, "memory": self.memory.to_json(), + "patterns": [p.to_json() for p in self.patterns] + } + + @classmethod + def from_json(cls, json): + return cls( json["ap"], json["name"], json["group"], json["firmware"], json["power"] == 1, json["mac"], json["selectedPattern"], json["brightness"], json["length"], json["start"], json["end"], - json["fade"], json["reversed"] == 1, json["cycle"] == 1, - json["uptime"], MemoryUsage.fromJSON(json["memory"]), - PatternMeta.fromJSONArray(json["patterns"]) + json["fade"], json["reversed"] == 1, json["cycle"], + json["uptime"], MemoryUsage.from_json(json["memory"]), + [PatternMeta.from_json(p) for p in json["patterns"]] ) From f2883531dfb3874e5770f69ed3a98700411dab01 Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sat, 18 Dec 2021 20:57:54 -0500 Subject: [PATCH 4/8] Added if to discovery main --- flickerstrip_py/discovery.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flickerstrip_py/discovery.py b/flickerstrip_py/discovery.py index b51933c..bfb52f5 100644 --- a/flickerstrip_py/discovery.py +++ b/flickerstrip_py/discovery.py @@ -27,5 +27,6 @@ class FlickerstripDiscoveryClient: if __name__ == "__main__": client = FlickerstripDiscoveryClient() flickerstrips = client.discover() - flickerstrips[0].force_update() - print(json.dumps(flickerstrips[0].status.to_json())) + if len(flickerstrips) > 0: + flickerstrips[0].force_update() + print(json.dumps(flickerstrips[0].status.to_json())) From bdc8d6983be07f80dd7a03c375268e2e2e1c2c3c Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sat, 18 Dec 2021 21:22:32 -0500 Subject: [PATCH 5/8] Increase timeout --- flickerstrip_py/discovery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flickerstrip_py/discovery.py b/flickerstrip_py/discovery.py index bfb52f5..5d0a118 100644 --- a/flickerstrip_py/discovery.py +++ b/flickerstrip_py/discovery.py @@ -6,7 +6,7 @@ from flickerstrip import Flickerstrip class FlickerstripDiscoveryClient: def __init__(self): - self.client = SSDPClient(timeout=10) + self.client = SSDPClient(timeout=15) def __discovered(self, device): loc = device["location"] From 7db59c75881b7cdc1826e9af6d1c862bf9701f43 Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sat, 18 Dec 2021 21:28:53 -0500 Subject: [PATCH 6/8] Fix venv settings --- .vscode/settings.json | 3 ++- poetry.toml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 poetry.toml diff --git a/.vscode/settings.json b/.vscode/settings.json index 290193e..e265b41 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.linting.flake8Enabled": true, - "python.linting.enabled": true + "python.linting.enabled": true, + "python.pythonPath": ".venv/bin/python", } \ No newline at end of file diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true From 74c6ee0987afda4293232932d579132e973de7de Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sat, 18 Dec 2021 22:30:14 -0500 Subject: [PATCH 7/8] Add create pattern --- flickerstrip_py/discovery.py | 24 +++++---- flickerstrip_py/flickerstrip.py | 94 ++++++++++++++++++++++++--------- flickerstrip_py/pattern.py | 27 ++++++++++ flickerstrip_py/status.py | 2 +- 4 files changed, 112 insertions(+), 35 deletions(-) diff --git a/flickerstrip_py/discovery.py b/flickerstrip_py/discovery.py index 5d0a118..fd69576 100644 --- a/flickerstrip_py/discovery.py +++ b/flickerstrip_py/discovery.py @@ -1,12 +1,13 @@ from ssdpy import SSDPClient import re import json -from flickerstrip import Flickerstrip +from flickerstrip_py.flickerstrip import Flickerstrip class FlickerstripDiscoveryClient: - def __init__(self): - self.client = SSDPClient(timeout=15) + def __init__(self, max_attempts=3): + self.max_attempts = max_attempts + self.client = SSDPClient() def __discovered(self, device): loc = device["location"] @@ -15,13 +16,16 @@ class FlickerstripDiscoveryClient: return Flickerstrip(ip_address) def discover(self): - print("Discovering devices...") - devices = self.client.m_search("ssdp:all") - print(f"Discovered {len(devices)} device(s).") - filtered = [d for d in devices if "Flickerstrip" in d["server"]] - print(f"Discovered {len(filtered)} flickerstrip(s).") - mapped = [self.__discovered(d) for d in filtered] - return mapped + for i in range(self.max_attempts): + print(f"Discovering devices... (Attempt {i + 1})") + devices = self.client.m_search("ssdp:all") + print(f"Discovered {len(devices)} device(s).") + filtered = [d for d in devices if "Flickerstrip" in d["server"]] + print(f"Discovered {len(filtered)} flickerstrip(s).") + if len(filtered) > 0: + mapped = [self.__discovered(d) for d in filtered] + return mapped + return [] if __name__ == "__main__": diff --git a/flickerstrip_py/flickerstrip.py b/flickerstrip_py/flickerstrip.py index 83f8d45..8e17369 100644 --- a/flickerstrip_py/flickerstrip.py +++ b/flickerstrip_py/flickerstrip.py @@ -1,6 +1,6 @@ import requests -from status import Status -from pattern import PatternMeta +from flickerstrip_py.status import Status +from flickerstrip_py.pattern import PatternMeta, PatternBuilder class Flickerstrip: @@ -20,9 +20,17 @@ class Flickerstrip: response (requests.Response): The response from the request. Returns: - boolean: If the request was successful + bool: If the request was successful """ data = response.json() + + if "type" not in data: + if "id" in data: # create pattern response. + return True + print("ERROR: Unable to get response type!") + print(data) + return False + respType = data["type"] if respType == "error": message = data["message"] @@ -35,6 +43,7 @@ class Flickerstrip: return True else: print(f"ERROR: Unrecognized response type: {respType}.") + print(data) return False def __get_request(self, path, params=None): @@ -46,32 +55,34 @@ class Flickerstrip: request. Defaults to None. Returns: - boolean: If the request was successful + bool: If the request was successful """ resp = requests.get(f"http://{self.ip_address}/{path}", params=params) return self.__check_response(resp) - def __post_request(self, path, json=None, data=None): + def __post_request(self, path, json=None, params=None, data=None): """Send a POST request to the strip. Args: path (string): The path for the URL to request. json (dictionary, optional): The JSON encodable body for the request. Defaults to None. + params (dictionary, optional): The query string params for the + request. Defaults to None. data (string, optional): The raw data body. Defaults to None. Returns: - boolean: If the request was successful + bool: If the request was successful """ resp = requests.post(f"http://{self.ip_address}/{path}", - json=json, data=data) + params=params, json=json, data=data) return self.__check_response(resp) def force_update(self): """Force updates the status of the strip. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("status") @@ -79,7 +90,7 @@ class Flickerstrip: """Trigger the strip to turn the lights on. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("power/on") @@ -87,7 +98,7 @@ class Flickerstrip: """Trigger the strip to turn the lights off. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("power/off") @@ -95,7 +106,7 @@ class Flickerstrip: """Trigger the strip to toggle the power status. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("power/toggle") @@ -103,7 +114,7 @@ class Flickerstrip: """Trigger the strip to switch to the next pattern in memory. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("pattern/next") @@ -114,18 +125,19 @@ class Flickerstrip: frame (int): The frame number to freeze on. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("pattern/frame", {"value": frame}) def select_pattern(self, index): """Change the current pattern to the pattern at the specified index. +s of the flickerstrip. Args: index (int): The index of the pattern. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("pattern/select", {"index": index}) @@ -143,6 +155,40 @@ class Flickerstrip: raise TypeError( "Deleting a pattern requires one of the three args.") + def create_pattern(self, pattern, preview=True): + """Create a pattern on the flickerstrip. + + Args: + pattern (PatternBuilder): The pattern to send. + preview (bool, optional): Whether to only preview the pattern. + Defaults to True. + + Returns: + bool: If the request was succesful + """ + return self.__post_request("pattern/create", params={ + "name": pattern.name, + "fps": pattern.fps, + "frames": pattern.frame_count, + "pixels": pattern.pixel_count, + "preview": preview + }, data=pattern.to_binary_string()) + + def set_color(self, r, g, b): + """Sets the strip to a solid color (given as an RGB value). + + Args: + r (int): The red value (0 - 255) + g (int): The green value (0 - 255) + b (int): The blue value (0 - 255) + + Returns: + bool: If the request was successful + """ + pattern = PatternBuilder("Solid") + pattern.add_pixel(r, g, b) + return self.create_pattern(pattern) + def set_brightness(self, value): """Set the brightness of the flickerstrip. @@ -150,7 +196,7 @@ class Flickerstrip: value (int): The brightness level as an int from 0 to 100. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("brightness", {"value": value}) @@ -161,7 +207,7 @@ class Flickerstrip: value (string): The new name. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__post_request("config/name", {"name": value}) @@ -172,7 +218,7 @@ class Flickerstrip: value (string): The new group. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__post_request("config/group", {"name": value}) @@ -184,7 +230,7 @@ class Flickerstrip: Value is handled in seconds. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("config/cycle", {"value": value}) @@ -196,7 +242,7 @@ class Flickerstrip: Value is handled in seconds. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("config/fade", {"value": value}) @@ -207,7 +253,7 @@ class Flickerstrip: value (int): The length of the strip in pixels. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("config/length", {"value": value}) @@ -221,7 +267,7 @@ class Flickerstrip: value (int): The new first pixel of the strip. Returns: - boolean: If the request was successful. + bool: If the request was successful. """ return self.__get_request("config/start", {"value": value}) @@ -235,7 +281,7 @@ class Flickerstrip: value (int): The new last pixel of the strip. Returns: - boolean: If the request was successful. + bool: If the request was successful. """ return self.__get_request("config/end", {"value": value}) @@ -243,11 +289,11 @@ class Flickerstrip: """Set the reversed state of the flickerstrip. Args: - value (boolean): If the flickerstrip should animate + value (bool): If the flickerstrip should animate patterns in reverse. Returns: - boolean: If the request was succesful + bool: If the request was succesful """ return self.__get_request("config/reversed", {"value": 1 if value else 0}) diff --git a/flickerstrip_py/pattern.py b/flickerstrip_py/pattern.py index 7cb4921..4a3018a 100644 --- a/flickerstrip_py/pattern.py +++ b/flickerstrip_py/pattern.py @@ -19,3 +19,30 @@ class PatternMeta: json["id"], json["name"], json["frames"], json["pixels"], json["flags"], json["fps"] ) + + +class PatternBuilder: + def __init__(self, name, fps=1): + self.pixels = [] + self.name = name + self.fps = fps + self.frame_count = 1 + self.pixel_count = 0 + self.frame_pixels = 0 + + def add_pixel(self, r, g, b): + if self.frame_count == 1: + self.pixel_count += 1 + + self.frame_pixels += 1 + self.pixels += [r, g, b] + + def next_frame(self): + self.frame_count += 1 + self.frame_pixels = 0 + + def is_valid(self): + self.frame_pixels == self.pixel_count + + def to_binary_string(self): + return ''.join([chr(item) for item in self.pixels]) diff --git a/flickerstrip_py/status.py b/flickerstrip_py/status.py index b0319b8..055bb30 100644 --- a/flickerstrip_py/status.py +++ b/flickerstrip_py/status.py @@ -1,4 +1,4 @@ -from pattern import PatternMeta +from flickerstrip_py.pattern import PatternMeta class MemoryUsage: From 71237837683d4ae77d17cd9ae431682eb764894b Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sun, 19 Dec 2021 10:39:50 -0500 Subject: [PATCH 8/8] Add connection failure handling --- flickerstrip_py/flickerstrip.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/flickerstrip_py/flickerstrip.py b/flickerstrip_py/flickerstrip.py index 8e17369..e3d720c 100644 --- a/flickerstrip_py/flickerstrip.py +++ b/flickerstrip_py/flickerstrip.py @@ -57,7 +57,12 @@ class Flickerstrip: Returns: bool: If the request was successful """ - resp = requests.get(f"http://{self.ip_address}/{path}", params=params) + try: + resp = requests.get(f"http://{self.ip_address}/{path}", + params=params) + except requests.exceptions.ConnectionError: + print("Unable to connect to flickerstrip, is it online?") + return False return self.__check_response(resp) def __post_request(self, path, json=None, params=None, data=None): @@ -74,8 +79,12 @@ class Flickerstrip: Returns: bool: If the request was successful """ - resp = requests.post(f"http://{self.ip_address}/{path}", - params=params, json=json, data=data) + try: + resp = requests.post(f"http://{self.ip_address}/{path}", + params=params, json=json, data=data) + except requests.exceptions.ConnectionError: + print("Unable to connect to flickerstrip, is it online?") + return False return self.__check_response(resp) def force_update(self):