pysmarlaapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # Ignore config file with secrets
132
+ /config.py
@@ -0,0 +1,59 @@
1
+ ci:
2
+ autoupdate_commit_msg: "chore: update pre-commit hooks"
3
+ autofix_commit_msg: "style: pre-commit fixes"
4
+
5
+ repos:
6
+ - repo: https://github.com/pre-commit/pre-commit-hooks
7
+ rev: v4.1.0
8
+ hooks:
9
+ - id: check-added-large-files
10
+ - id: check-case-conflict
11
+ - id: check-merge-conflict
12
+ - id: check-symlinks
13
+ - id: check-yaml
14
+ - id: debug-statements
15
+ - id: end-of-file-fixer
16
+ - id: mixed-line-ending
17
+ - id: requirements-txt-fixer
18
+ - id: trailing-whitespace
19
+
20
+ - repo: https://github.com/PyCQA/isort
21
+ rev: 5.12.0
22
+ hooks:
23
+ - id: isort
24
+
25
+ - repo: https://github.com/asottile/pyupgrade
26
+ rev: v2.31.0
27
+ hooks:
28
+ - id: pyupgrade
29
+ args: [--py37-plus]
30
+
31
+ - repo: https://github.com/hadialqattan/pycln
32
+ rev: v1.2.5
33
+ hooks:
34
+ - id: pycln
35
+ args: [--config=pyproject.toml]
36
+ stages: [manual]
37
+
38
+ - repo: https://github.com/codespell-project/codespell
39
+ rev: v2.1.0
40
+ hooks:
41
+ - id: codespell
42
+
43
+ - repo: https://github.com/pre-commit/pygrep-hooks
44
+ rev: v1.9.0
45
+ hooks:
46
+ - id: python-check-blanket-noqa
47
+ - id: python-check-blanket-type-ignore
48
+ - id: python-no-log-warn
49
+ - id: python-no-eval
50
+ - id: python-use-type-annotations
51
+ - id: rst-backticks
52
+ - id: rst-directive-colons
53
+ - id: rst-inline-touching-normal
54
+
55
+ - repo: https://github.com/mgedmin/check-manifest
56
+ rev: "0.47"
57
+ hooks:
58
+ - id: check-manifest
59
+ stages: [manual]
@@ -0,0 +1,10 @@
1
+ [distutils]
2
+ index-servers =
3
+ pypi
4
+ testpypi
5
+
6
+ [pypi]
7
+ repository = https://upload.pypi.org/legacy/
8
+
9
+ [testpypi]
10
+ repository = https://test.pypi.org/legacy/
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "configurations": [
4
+ {
5
+ "name": "Python: Debug Tests",
6
+ "type": "debugpy",
7
+ "request": "launch",
8
+ "program": "${file}",
9
+ "purpose": [
10
+ "debug-test"
11
+ ],
12
+ "console": "integratedTerminal",
13
+ "justMyCode": false,
14
+ "env": {
15
+ "PYTEST_ADDOPTS": "--no-cov -n0 --dist no"
16
+ }
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "editor.formatOnSave": true,
3
+ "editor.formatOnPaste": true,
4
+ "files.trimTrailingWhitespace": true,
5
+ "files.autoSave": "onFocusChange",
6
+ "git.autofetch": true,
7
+ "[jsonc]": {
8
+ "editor.defaultFormatter": "vscode.json-language-features"
9
+ },
10
+ "[python]": {
11
+ "editor.defaultFormatter": "ms-python.black-formatter"
12
+ },
13
+ "python.defaultInterpreterPath": "/usr/local/bin/python",
14
+ "python.formatting.provider": "black",
15
+ "python.testing.unittestEnabled": false,
16
+ "python.testing.pytestEnabled": true,
17
+ "pylint.args": [
18
+ "--rcfile=pyproject.toml"
19
+ ],
20
+ "black-formatter.args": [
21
+ "--config=pyproject.toml"
22
+ ],
23
+ "flake8.args": [
24
+ "--toml-config=pyproject.toml"
25
+ ],
26
+ "isort.args": [
27
+ "--settings-path=pyproject.toml"
28
+ ]
29
+ }
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.3
2
+ Name: pysmarlaapi
3
+ Version: 0.1.0
4
+ Summary: Swing2Sleep Smarla API via websocket with signalr protocol
5
+ Author-email: Robin Lintermann <robin.lintermann@explicatis.com>
6
+ Requires-Python: >=3.11.9
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Development Status :: 1 - Planning
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Requires-Dist: aiohttp~=3.11.11
12
+ Requires-Dist: jsonpickle~=4.0.0
13
+ Requires-Dist: pysignalr~=1.1.0
14
+
15
+ # Federwiege Python API
16
+
17
+ Swing2Sleep Smarla API via websocket with signalr protocol.
18
+
19
+ See tests for example usage.
20
+
21
+ ## Development Setup
22
+
23
+ - `pip3 install -r requirements_dev.txt`
24
+ - `pre-commit install`
25
+
26
+ ## Publishing
27
+
28
+ - Use flit to publish package to pypi
29
+
@@ -0,0 +1,14 @@
1
+ # Federwiege Python API
2
+
3
+ Swing2Sleep Smarla API via websocket with signalr protocol.
4
+
5
+ See tests for example usage.
6
+
7
+ ## Development Setup
8
+
9
+ - `pip3 install -r requirements_dev.txt`
10
+ - `pre-commit install`
11
+
12
+ ## Publishing
13
+
14
+ - Use flit to publish package to pypi
@@ -0,0 +1,210 @@
1
+ [build-system]
2
+ requires = ["flit_core >=2,<4"]
3
+ build-backend = "flit_core.buildapi"
4
+
5
+ [project]
6
+ name = "pysmarlaapi"
7
+ authors = [
8
+ {name = "Robin Lintermann", email = "robin.lintermann@explicatis.com"},
9
+ ]
10
+ dependencies = [
11
+ "aiohttp~=3.11.11",
12
+ "jsonpickle~=4.0.0",
13
+ "pysignalr~=1.1.0",
14
+ ]
15
+ description = "Swing2Sleep Smarla API via websocket with signalr protocol"
16
+ readme = "README.md"
17
+ classifiers = [
18
+ "Development Status :: 1 - Planning",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.11"
21
+ ]
22
+ requires-python = ">=3.11.9"
23
+ dynamic = ["version"]
24
+
25
+ [tool.flit.module]
26
+ name = "pysmarlaapi"
27
+
28
+ [tool.black]
29
+ line-length = 120
30
+ fast = true
31
+
32
+ [tool.flake8]
33
+ max-line-length = 120
34
+ select = "F,E,W,B,B901,B902,B903"
35
+ exclude = [
36
+ ".eggs",
37
+ ".git",
38
+ ".tox",
39
+ "nssm",
40
+ "obj",
41
+ "out",
42
+ "packages",
43
+ "pywin32",
44
+ "tests",
45
+ "swagger_client"
46
+ ]
47
+ ignore = [
48
+ "E722",
49
+ "B001",
50
+ "W503",
51
+ "E203"
52
+ ]
53
+
54
+ [tool.pylint]
55
+ extension-pkg-whitelist= [
56
+ "numpy",
57
+ "torch",
58
+ "cv2",
59
+ "pyodbc",
60
+ "pydantic",
61
+ "ciso8601",
62
+ "netcdf4",
63
+ "scipy"
64
+ ]
65
+ ignore="CVS"
66
+ ignore-patterns="test.*?py,conftest.py"
67
+ init-hook='import sys; sys.setrecursionlimit(8 * sys.getrecursionlimit())'
68
+ jobs=0
69
+ limit-inference-results=100
70
+ persistent="yes"
71
+ suggestion-mode="yes"
72
+ unsafe-load-any-extension="no"
73
+
74
+ [tool.pylint.'MESSAGES CONTROL']
75
+ enable="c-extension-no-member"
76
+
77
+ [tool.pylint.'REPORTS']
78
+ evaluation="10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)"
79
+ output-format="text"
80
+ reports="no"
81
+ score="yes"
82
+
83
+ [tool.pylint.'REFACTORING']
84
+ max-nested-blocks=5
85
+ never-returning-functions="sys.exit"
86
+
87
+ [tool.pylint.'BASIC']
88
+ argument-naming-style="snake_case"
89
+ attr-naming-style="snake_case"
90
+ bad-names= [
91
+ "foo",
92
+ "bar"
93
+ ]
94
+ class-attribute-naming-style="any"
95
+ class-naming-style="PascalCase"
96
+ const-naming-style="UPPER_CASE"
97
+ docstring-min-length=-1
98
+ function-naming-style="snake_case"
99
+ good-names= [
100
+ "i",
101
+ "j",
102
+ "k",
103
+ "ex",
104
+ "Run",
105
+ "_"
106
+ ]
107
+ include-naming-hint="yes"
108
+ inlinevar-naming-style="any"
109
+ method-naming-style="snake_case"
110
+ module-naming-style="any"
111
+ no-docstring-rgx="^_"
112
+ property-classes="abc.abstractproperty"
113
+ variable-naming-style="snake_case"
114
+
115
+ [tool.pylint.'FORMAT']
116
+ ignore-long-lines="^\\s*(# )?.*['\"]?<?https?://\\S+>?"
117
+ indent-after-paren=4
118
+ indent-string=' '
119
+ max-line-length=120
120
+ max-module-lines=1000
121
+ single-line-class-stmt="no"
122
+ single-line-if-stmt="no"
123
+
124
+ [tool.pylint.'LOGGING']
125
+ logging-format-style="old"
126
+ logging-modules="logging"
127
+
128
+ [tool.pylint.'MISCELLANEOUS']
129
+ notes= [
130
+ "FIXME",
131
+ "XXX",
132
+ "TODO"
133
+ ]
134
+
135
+ [tool.pylint.'SIMILARITIES']
136
+ ignore-comments="yes"
137
+ ignore-docstrings="yes"
138
+ ignore-imports="yes"
139
+ min-similarity-lines=7
140
+
141
+ [tool.pylint.'SPELLING']
142
+ max-spelling-suggestions=4
143
+ spelling-store-unknown-words="no"
144
+
145
+ [tool.pylint.'STRING']
146
+ check-str-concat-over-line-jumps="no"
147
+
148
+ [tool.pylint.'TYPECHECK']
149
+ contextmanager-decorators="contextlib.contextmanager"
150
+ generated-members="numpy.*,np.*,pyspark.sql.functions,collect_list"
151
+ ignore-mixin-members="yes"
152
+ ignore-none="yes"
153
+ ignore-on-opaque-inference="yes"
154
+ ignored-classes="optparse.Values,thread._local,_thread._local,numpy,torch,swagger_client"
155
+ ignored-modules="numpy,torch,swagger_client,netCDF4,scipy"
156
+ missing-member-hint="yes"
157
+ missing-member-hint-distance=1
158
+ missing-member-max-choices=1
159
+
160
+ [tool.pylint.'VARIABLES']
161
+ additional-builtins="dbutils"
162
+ allow-global-unused-variables="yes"
163
+ callbacks= [
164
+ "cb_",
165
+ "_cb"
166
+ ]
167
+ dummy-variables-rgx="_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_"
168
+ ignored-argument-names="_.*|^ignored_|^unused_"
169
+ init-import="no"
170
+ redefining-builtins-modules="six.moves,past.builtins,future.builtins,builtins,io"
171
+
172
+ [tool.pylint.'CLASSES']
173
+ defining-attr-methods= [
174
+ "__init__",
175
+ "__new__",
176
+ "setUp",
177
+ "__post_init__"
178
+ ]
179
+ exclude-protected= [
180
+ "_asdict",
181
+ "_fields",
182
+ "_replace",
183
+ "_source",
184
+ "_make"
185
+ ]
186
+ valid-classmethod-first-arg="cls"
187
+ valid-metaclass-classmethod-first-arg="cls"
188
+
189
+ [tool.pylint.'DESIGN']
190
+ max-args=5
191
+ max-attributes=7
192
+ max-bool-expr=5
193
+ max-branches=12
194
+ max-locals=15
195
+ max-parents=7
196
+ max-public-methods=20
197
+ max-returns=6
198
+ max-statements=50
199
+ min-public-methods=2
200
+
201
+ [tool.pylint.'IMPORTS']
202
+ allow-wildcard-with-all="no"
203
+ analyse-fallback-blocks="no"
204
+ deprecated-modules="optparse,tkinter.tix"
205
+
206
+ [tool.pylint.'EXCEPTIONS']
207
+ overgeneral-exceptions= [
208
+ "BaseException",
209
+ "Exception"
210
+ ]
@@ -0,0 +1,4 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .classes import AuthToken, Connection
4
+ from .connection_hub import ConnectionHub
@@ -0,0 +1,2 @@
1
+ from .auth_token import AuthToken
2
+ from .connection import Connection
@@ -0,0 +1,27 @@
1
+ from dataclasses import dataclass
2
+ from typing import Self
3
+
4
+ import jsonpickle
5
+
6
+
7
+ @dataclass
8
+ class AuthToken:
9
+ refreshToken: str
10
+ token: str
11
+ dateCreated: str
12
+ appIdentifier: str
13
+ serialNumber: str
14
+ appVersion: str
15
+ appCulture: str
16
+
17
+ @classmethod
18
+ def from_json(cls, value) -> Self:
19
+ value["py/object"] = "pysmarlaapi.classes.auth_token.AuthToken"
20
+ return jsonpickle.decode(str(value))
21
+
22
+ @classmethod
23
+ def from_string(cls, value) -> Self:
24
+ return AuthToken.from_json(jsonpickle.decode(value))
25
+
26
+ def get_string(self) -> str:
27
+ return jsonpickle.encode(self, unpicklable=False)
@@ -0,0 +1,38 @@
1
+ import aiohttp
2
+ import jsonpickle
3
+
4
+ from . import AuthToken
5
+
6
+
7
+ class Connection:
8
+
9
+ def __init__(self, url: str, token: AuthToken = None, token_str=None, token_json=None):
10
+ self.url = url
11
+ if token is not None:
12
+ self.token = token
13
+ elif token_json is not None:
14
+ self.token = AuthToken.from_json(token_json)
15
+ elif token_str is not None:
16
+ self.token = AuthToken.from_string(token_str)
17
+ else:
18
+ self.token = None
19
+
20
+ async def get_token(self) -> AuthToken:
21
+ try:
22
+ async with aiohttp.ClientSession(self.url) as session:
23
+ async with session.post(
24
+ "/api/AppParing/getToken",
25
+ headers={"accept": "*/*", "Content-Type": "application/json"},
26
+ data=jsonpickle.encode(self.token, unpicklable=False),
27
+ ) as response:
28
+ if response.status != 200:
29
+ return None
30
+ json_body = await response.json()
31
+ except ValueError:
32
+ return None
33
+ try:
34
+ new_token = AuthToken.from_json(json_body)
35
+ except ValueError:
36
+ return None
37
+ self.token = new_token
38
+ return self.token
@@ -0,0 +1,148 @@
1
+ import asyncio
2
+ import logging
3
+ import random
4
+ import uuid
5
+
6
+ from pysignalr.client import SignalRClient
7
+ from pysignalr.transport.abstract import ConnectionState
8
+
9
+ from ..classes import Connection
10
+
11
+
12
+ async def event_wait(event, timeout):
13
+ try:
14
+ await asyncio.wait_for(event.wait(), timeout)
15
+ except asyncio.TimeoutError:
16
+ return
17
+
18
+
19
+ class ConnectionHub:
20
+ """SignalRCore Hub
21
+ Provides interface via websocket for the controller using the SignalRCore protocol.
22
+ """
23
+
24
+ @property
25
+ def running(self):
26
+ return self._running
27
+
28
+ @property
29
+ def connected(self):
30
+ return self.client._transport._state == ConnectionState.connected if self.client else False
31
+
32
+ def __init__(
33
+ self,
34
+ async_loop: asyncio.AbstractEventLoop,
35
+ connection: Connection,
36
+ interval: int = 60,
37
+ backoff: int = 300,
38
+ ):
39
+ self.connection: Connection = connection
40
+ self._loop: asyncio.AbstractEventLoop = async_loop
41
+ self._interval = interval
42
+ self._backoff = backoff
43
+
44
+ self.logger = logging.getLogger(f"{__package__}[{self.connection.token.serialNumber}]")
45
+
46
+ self.listeners = set()
47
+
48
+ self._running = False
49
+ self._wake = asyncio.Event()
50
+
51
+ self.client = None
52
+ self.setup()
53
+
54
+ async def notifycontrollerconnection(self, args):
55
+ value = args[0]
56
+ if value == "ControllerConnected":
57
+ await self.notify_listeners(1)
58
+ else:
59
+ await self.notify_listeners(0)
60
+
61
+ def setup(self):
62
+ self.client = SignalRClient(self.connection.url + "/MobileAppHub", retry_count=1)
63
+ self.client.on_open(self.on_open_function)
64
+ self.client.on_close(self.on_close_function)
65
+ self.client.on_error(self.on_error)
66
+ self.client.on("SetNotifyAppConnectionCallback", self.notifycontrollerconnection)
67
+
68
+ def add_listener(self, listener):
69
+ if self.running:
70
+ return
71
+ self.listeners.add(listener)
72
+
73
+ def remove_listener(self, listener):
74
+ if self.running:
75
+ return
76
+ self.listeners.remove(listener)
77
+
78
+ async def notify_listeners(self, value):
79
+ for listener in self.listeners:
80
+ await listener(value)
81
+
82
+ async def on_open_function(self):
83
+ self.logger.info("Connection to server established")
84
+
85
+ async def on_close_function(self):
86
+ self.logger.info("Connection to server closed")
87
+
88
+ async def on_error(self, message):
89
+ self.logger.error("Connection error occurred: " + str(message))
90
+
91
+ def start(self):
92
+ if self.running:
93
+ return
94
+ self._running = True
95
+ asyncio.run_coroutine_threadsafe(self.connection_watcher(), self._loop)
96
+
97
+ def stop(self):
98
+ if not self.running:
99
+ return
100
+ self._running = False
101
+ self.close_connection()
102
+ self.wake_up()
103
+
104
+ async def connection_watcher(self):
105
+ while self.running:
106
+ await self.refresh_token()
107
+ try:
108
+ await self.client.run()
109
+ except Exception as e:
110
+ self.logger.warning(f"Error during connection: {type(e).__name__}: {str(e)}")
111
+
112
+ # Random backoff to avoid simultaneous connection attempts
113
+ backoff = random.randint(0, self._backoff)
114
+ await event_wait(self._wake, self._interval + backoff)
115
+ self._wake.clear()
116
+
117
+ def wake_up(self):
118
+ self._wake.set()
119
+
120
+ def close_connection(self):
121
+ if not self.connected:
122
+ return
123
+ asyncio.run_coroutine_threadsafe(self.client._transport._ws.close(), self._loop)
124
+
125
+ async def refresh_token(self):
126
+ await self.connection.get_token()
127
+ auth_token = self.connection.token.token
128
+ self.client._transport._headers["Authorization"] = f"Bearer {auth_token}"
129
+ self.logger.info("Auth token refreshed")
130
+
131
+ def send_serialized_data(self, event, value=None):
132
+ serialized_result = {
133
+ "callIdentifier": {
134
+ "requestNonce": str(uuid.uuid4()),
135
+ },
136
+ }
137
+ if value is not None:
138
+ serialized_result["value"] = value
139
+
140
+ self.logger.debug(f"Sending data, Event: {event}, Payload: {str(serialized_result)}")
141
+
142
+ asyncio.run_coroutine_threadsafe(self.send_data(event, [serialized_result]), self._loop)
143
+
144
+ async def send_data(self, event, data):
145
+ try:
146
+ await self.client.send(event, data)
147
+ except Exception:
148
+ pass
@@ -0,0 +1,2 @@
1
+ from .services.analyser_service import AnalyserService
2
+ from .services.babywiege_service import BabywiegeService
@@ -0,0 +1,2 @@
1
+ from .property import Property
2
+ from .service import Service
@@ -0,0 +1,46 @@
1
+ import asyncio
2
+ from typing import Generic, TypeVar
3
+
4
+ from ...connection_hub import ConnectionHub
5
+
6
+ _VT = TypeVar("_VT")
7
+
8
+
9
+ class Property(Generic[_VT]):
10
+
11
+ def __init__(self, connection_hub: ConnectionHub):
12
+ self.hub = connection_hub
13
+ self.value: _VT = None
14
+ self.listeners = set()
15
+ self.lock = asyncio.Lock()
16
+
17
+ async def add_listener(self, listener):
18
+ async with self.lock:
19
+ self.listeners.add(listener)
20
+
21
+ async def remove_listener(self, listener):
22
+ async with self.lock:
23
+ self.listeners.remove(listener)
24
+
25
+ async def notify_listeners(self, value):
26
+ async with self.lock:
27
+ for listener in self.listeners:
28
+ await listener(value)
29
+
30
+ def get(self) -> _VT:
31
+ return self.value
32
+
33
+ def set(self, new_value: _VT, push=True):
34
+ if push:
35
+ self.push(new_value)
36
+ else:
37
+ self.value = new_value
38
+
39
+ def push(self):
40
+ pass
41
+
42
+ def pull(self):
43
+ pass
44
+
45
+ def register(self):
46
+ pass
@@ -0,0 +1,33 @@
1
+ from ...connection_hub import ConnectionHub
2
+ from .property import Property
3
+
4
+
5
+ class Service:
6
+
7
+ async def on_connection_change(self, value):
8
+ if value:
9
+ self.sync()
10
+
11
+ def __init__(self, connection_hub: ConnectionHub):
12
+ self.hub = connection_hub
13
+ self.registered = False
14
+ self.props: dict[str, Property] = {}
15
+
16
+ def add_property(self, key: str, prop: Property):
17
+ self.props[key] = prop
18
+
19
+ def get_properties(self):
20
+ return self.props
21
+
22
+ def get_property(self, key: str):
23
+ return self.props[key]
24
+
25
+ def register(self):
26
+ for prop in self.props.values():
27
+ prop.register()
28
+ self.hub.add_listener(self.on_connection_change)
29
+ self.registered = True
30
+
31
+ def sync(self):
32
+ for prop in self.props.values():
33
+ prop.pull()
@@ -0,0 +1,47 @@
1
+ from ...connection_hub import ConnectionHub
2
+ from ..classes import Property, Service
3
+
4
+
5
+ class AnalyserService(Service):
6
+
7
+ def __init__(self, connection_hub: ConnectionHub):
8
+ super().__init__(connection_hub)
9
+ self.add_property("oscillation", OscillationProperty(self.hub))
10
+ self.add_property("activity", ActivityProperty(self.hub))
11
+ self.register()
12
+
13
+
14
+ class OscillationProperty(Property[list[int, int]]):
15
+
16
+ async def on_callback(self, args):
17
+ value = args[0]["value"]
18
+ self.set(value, push=False)
19
+ await self.notify_listeners(value)
20
+
21
+ def __init__(self, parent: Service):
22
+ super().__init__(parent)
23
+ self.value = [0, 0]
24
+
25
+ def pull(self):
26
+ self.hub.send_serialized_data("GetOscillation")
27
+
28
+ def register(self):
29
+ self.hub.client.on("GetOscillationCallback", self.on_callback)
30
+
31
+
32
+ class ActivityProperty(Property[int]):
33
+
34
+ async def on_callback(self, args):
35
+ value = args[0]["value"]
36
+ self.set(value, push=False)
37
+ await self.notify_listeners(value)
38
+
39
+ def __init__(self, parent: Service):
40
+ super().__init__(parent)
41
+ self.value = 0
42
+
43
+ def pull(self):
44
+ self.hub.send_serialized_data("GetActivity")
45
+
46
+ def register(self):
47
+ self.hub.client.on("GetActivityCallback", self.on_callback)
@@ -0,0 +1,75 @@
1
+ from ...connection_hub import ConnectionHub
2
+ from ..classes import Property, Service
3
+
4
+
5
+ class BabywiegeService(Service):
6
+
7
+ def __init__(self, connection_hub: ConnectionHub):
8
+ super().__init__(connection_hub)
9
+ self.add_property("swing_active", SwingActiveProperty(self.hub))
10
+ self.add_property("intensity", IntensityProperty(self.hub))
11
+ self.add_property("smartmode", SmartModeProperty(self.hub))
12
+ self.register()
13
+
14
+
15
+ class SwingActiveProperty(Property[bool]):
16
+
17
+ async def on_callback(self, args):
18
+ value = args[0]["value"]
19
+ self.set(value, push=False)
20
+ await self.notify_listeners(value)
21
+
22
+ def __init__(self, parent: Service):
23
+ super().__init__(parent)
24
+ self.value = False
25
+
26
+ def pull(self):
27
+ self.hub.send_serialized_data("GetSwingActive")
28
+
29
+ def push(self, value):
30
+ self.hub.send_serialized_data("SetSwingActive", value)
31
+
32
+ def register(self):
33
+ self.hub.client.on("GetSwingActiveCallback", self.on_callback)
34
+
35
+
36
+ class IntensityProperty(Property[int]):
37
+
38
+ async def on_callback(self, args):
39
+ value = args[0]["value"]
40
+ self.set(value, push=False)
41
+ await self.notify_listeners(value)
42
+
43
+ def __init__(self, parent: Service):
44
+ super().__init__(parent)
45
+ self.value = 0
46
+
47
+ def pull(self):
48
+ self.hub.send_serialized_data("GetIntensity")
49
+
50
+ def push(self, value):
51
+ self.hub.send_serialized_data("SetIntensity", value)
52
+
53
+ def register(self):
54
+ self.hub.client.on("GetIntensityCallback", self.on_callback)
55
+
56
+
57
+ class SmartModeProperty(Property[bool]):
58
+
59
+ async def on_callback(self, args):
60
+ value = args[0]["value"]
61
+ self.set(value, push=False)
62
+ await self.notify_listeners(value)
63
+
64
+ def __init__(self, parent: Service):
65
+ super().__init__(parent)
66
+ self.value = False
67
+
68
+ def pull(self):
69
+ self.hub.send_serialized_data("GetSmartMode")
70
+
71
+ def push(self, value):
72
+ self.hub.send_serialized_data("SetSmartMode", value)
73
+
74
+ def register(self):
75
+ self.hub.client.on("GetSmartModeCallback", self.on_callback)
@@ -0,0 +1,3 @@
1
+ aiohttp==3.11.11
2
+ jsonpickle==4.0.0
3
+ pysignalr==1.1.0
@@ -0,0 +1,5 @@
1
+ aiohttp==3.11.11
2
+ flit==3.10.1
3
+ jsonpickle==4.0.0
4
+ pre-commit==4.0.1
5
+ pysignalr==1.1.0
@@ -0,0 +1,79 @@
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+ import threading
5
+ import time
6
+
7
+ sys.path.append("../")
8
+ logging.basicConfig(level=20)
9
+
10
+ from pysmarlaapi import Connection, ConnectionHub
11
+ from pysmarlaapi.federwiege import AnalyserService, BabywiegeService
12
+
13
+ try:
14
+ from config import AUTH_TOKEN_PERSONAL, HOST
15
+ except ImportError:
16
+ print("config.py or mandatory variables missing, please add in root folder...")
17
+ exit()
18
+
19
+ loop = asyncio.get_event_loop()
20
+ async_thread = threading.Thread(target=loop.run_forever)
21
+
22
+ connection = Connection(url=HOST, token_json=AUTH_TOKEN_PERSONAL)
23
+
24
+ hub = ConnectionHub(loop, connection, interval=10, backoff=0)
25
+ babywiege_svc = BabywiegeService(hub)
26
+ analyser_svc = AnalyserService(hub)
27
+
28
+
29
+ def main():
30
+ async_thread.start()
31
+ hub.start()
32
+
33
+ while not hub.connected:
34
+ time.sleep(1)
35
+
36
+ swing_active_prop = babywiege_svc.get_property("swing_active")
37
+ intensity_prop = babywiege_svc.get_property("intensity")
38
+ oscillation_prop = analyser_svc.get_property("oscillation")
39
+
40
+ time.sleep(1)
41
+
42
+ value = swing_active_prop.get()
43
+ print(f"Swing Active: {value}")
44
+ intensity = intensity_prop.get()
45
+ print(f"Intensity: {intensity}%")
46
+ oscillation = oscillation_prop.get()
47
+ print(f"Amplitude: {oscillation[0]}mm Period: {oscillation[1]}ms")
48
+
49
+ swing_active_prop.set(True)
50
+ intensity_prop.set(60)
51
+
52
+ time.sleep(1)
53
+
54
+ value = swing_active_prop.get()
55
+ print(f"Swing Active: {value}")
56
+ intensity = intensity_prop.get()
57
+ print(f"Intensity: {intensity}%")
58
+ oscillation = oscillation_prop.get()
59
+ print(f"Amplitude: {oscillation[0]}mm Period: {oscillation[1]}ms")
60
+
61
+ time.sleep(1)
62
+
63
+ while True:
64
+ value = swing_active_prop.get()
65
+ print(f"Swing Active: {value}")
66
+ intensity = intensity_prop.get()
67
+ print(f"Intensity: {intensity}%")
68
+ oscillation = oscillation_prop.get()
69
+ print(f"Amplitude: {oscillation[0]}mm Period: {oscillation[1]}ms")
70
+ time.sleep(1)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ try:
75
+ main()
76
+ except BaseException:
77
+ pass
78
+ hub.stop()
79
+ loop.call_soon_threadsafe(loop.stop)