spgroup 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.
Files changed (65) hide show
  1. spgroup-0.1.0/.github/workflows/release.yml +113 -0
  2. spgroup-0.1.0/.gitignore +10 -0
  3. spgroup-0.1.0/.postman/resources.yaml +7 -0
  4. spgroup-0.1.0/AGENTS.md +56 -0
  5. spgroup-0.1.0/LICENSE +21 -0
  6. spgroup-0.1.0/MANIFEST.in +2 -0
  7. spgroup-0.1.0/PKG-INFO +52 -0
  8. spgroup-0.1.0/README.md +36 -0
  9. spgroup-0.1.0/docs/adr/adr-two-mode-sdk-vendoring.md +248 -0
  10. spgroup-0.1.0/docs/api/README.md +88 -0
  11. spgroup-0.1.0/docs/api/auth-profile-parity-inventory.md +63 -0
  12. spgroup-0.1.0/docs/api/auth0-api.md +231 -0
  13. spgroup-0.1.0/docs/api/auth0-flow.md +191 -0
  14. spgroup-0.1.0/docs/api/content-api.md +64 -0
  15. spgroup-0.1.0/docs/api/ev-api.md +102 -0
  16. spgroup-0.1.0/docs/api/frieza-api.md +159 -0
  17. spgroup-0.1.0/docs/api/greenup-api.md +74 -0
  18. spgroup-0.1.0/docs/api/jarvis-api.md +318 -0
  19. spgroup-0.1.0/docs/api/jarvis-parity-inventory.md +59 -0
  20. spgroup-0.1.0/docs/api/live-e2e.md +132 -0
  21. spgroup-0.1.0/docs/api/models.md +235 -0
  22. spgroup-0.1.0/docs/api/network-layer.md +133 -0
  23. spgroup-0.1.0/docs/api/payment-api.md +268 -0
  24. spgroup-0.1.0/docs/api/queries.md +198 -0
  25. spgroup-0.1.0/docs/api/response-schemas.md +306 -0
  26. spgroup-0.1.0/docs/api/sdk-foundations.md +83 -0
  27. spgroup-0.1.0/docs/api/skalbox-api.md +133 -0
  28. spgroup-0.1.0/docs/api/utility-apis.md +85 -0
  29. spgroup-0.1.0/docs/api/yggdrasil-api.md +219 -0
  30. spgroup-0.1.0/docs/context-tracker.md +67 -0
  31. spgroup-0.1.0/docs/plans/sdk-vendoring.md +534 -0
  32. spgroup-0.1.0/docs/postman-collection.json +543 -0
  33. spgroup-0.1.0/docs/sdk-parity-roadmap.md +204 -0
  34. spgroup-0.1.0/docs/sp-mobile-api.postman_collection.json +1246 -0
  35. spgroup-0.1.0/mise.toml +30 -0
  36. spgroup-0.1.0/postman/collections/energy-monitoring-api.json +577 -0
  37. spgroup-0.1.0/postman/collections/sp-mobile-api.json +1395 -0
  38. spgroup-0.1.0/postman/environments/local.json +91 -0
  39. spgroup-0.1.0/pyproject.toml +54 -0
  40. spgroup-0.1.0/resources.yaml +7 -0
  41. spgroup-0.1.0/setup.cfg +4 -0
  42. spgroup-0.1.0/src/spgroup/__init__.py +21 -0
  43. spgroup-0.1.0/src/spgroup/auth.py +14 -0
  44. spgroup-0.1.0/src/spgroup/client.py +17 -0
  45. spgroup-0.1.0/src/spgroup/mobile/__init__.py +31 -0
  46. spgroup-0.1.0/src/spgroup/mobile/auth.py +336 -0
  47. spgroup-0.1.0/src/spgroup/mobile/constants.py +51 -0
  48. spgroup-0.1.0/src/spgroup/mobile/errors.py +65 -0
  49. spgroup-0.1.0/src/spgroup/mobile/jarvis.py +302 -0
  50. spgroup-0.1.0/src/spgroup/mobile/models.py +26 -0
  51. spgroup-0.1.0/src/spgroup/mobile/profile.py +178 -0
  52. spgroup-0.1.0/src/spgroup/mobile/request.py +201 -0
  53. spgroup-0.1.0/src/spgroup/mobile/resource.py +116 -0
  54. spgroup-0.1.0/src/spgroup/mobile/session.py +213 -0
  55. spgroup-0.1.0/src/spgroup/mobile/transport.py +96 -0
  56. spgroup-0.1.0/src/spgroup/mobile/yggdrasil.py +235 -0
  57. spgroup-0.1.0/src/spgroup/models.py +16 -0
  58. spgroup-0.1.0/src/spgroup/py.typed +1 -0
  59. spgroup-0.1.0/src/spgroup.egg-info/PKG-INFO +52 -0
  60. spgroup-0.1.0/src/spgroup.egg-info/SOURCES.txt +63 -0
  61. spgroup-0.1.0/src/spgroup.egg-info/dependency_links.txt +1 -0
  62. spgroup-0.1.0/src/spgroup.egg-info/requires.txt +1 -0
  63. spgroup-0.1.0/src/spgroup.egg-info/scm_file_list.json +72 -0
  64. spgroup-0.1.0/src/spgroup.egg-info/scm_version.json +8 -0
  65. spgroup-0.1.0/src/spgroup.egg-info/top_level.txt +1 -0
@@ -0,0 +1,113 @@
1
+ name: Publish spgroup to PyPI
2
+ run-name: ${{ github.actor }} is publishing spgroup to PyPI
3
+
4
+ on: [push]
5
+
6
+ jobs:
7
+ build:
8
+ name: Build distribution
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - name: Set up Python
14
+ uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.13"
17
+ - name: Install build tools
18
+ run: |
19
+ python -m pip install build
20
+ python -m pip install -e . pytest
21
+ - name: Build a binary wheel and a source tarball
22
+ run: python -m build
23
+ - name: Verify wheel and sdist integrity
24
+ run: python -m pytest tests/test_sdk_packaging.py -v
25
+ - name: Store the distribution packages
26
+ uses: actions/upload-artifact@v4
27
+ with:
28
+ name: python-package-distributions
29
+ path: dist/
30
+
31
+ publish-to-testpypi:
32
+ name: Publish to TestPyPI
33
+ if: github.ref_name == 'main' && !startsWith(github.ref, 'refs/tags/')
34
+ needs: [build]
35
+ runs-on: ubuntu-latest
36
+
37
+ environment:
38
+ name: testpypi
39
+ url: https://test.pypi.org/p/spgroup
40
+
41
+ permissions:
42
+ id-token: write
43
+
44
+ steps:
45
+ - name: Download all the dists
46
+ uses: actions/download-artifact@v4
47
+ with:
48
+ name: python-package-distributions
49
+ path: dist/
50
+ - name: Publish distribution to TestPyPI
51
+ uses: pypa/gh-action-pypi-publish@release/v1
52
+ with:
53
+ repository-url: https://test.pypi.org/legacy/
54
+
55
+ publish-to-pypi:
56
+ name: Publish to PyPI
57
+ if: startsWith(github.ref, 'refs/tags/')
58
+ needs: [build]
59
+ runs-on: ubuntu-latest
60
+
61
+ environment:
62
+ name: pypi
63
+ url: https://pypi.org/p/spgroup
64
+
65
+ permissions:
66
+ id-token: write
67
+
68
+ steps:
69
+ - name: Download all the dists
70
+ uses: actions/download-artifact@v4
71
+ with:
72
+ name: python-package-distributions
73
+ path: dist/
74
+ - name: Publish distribution to PyPI
75
+ uses: pypa/gh-action-pypi-publish@release/v1
76
+
77
+ github-release:
78
+ name: Sign and upload to GitHub Release
79
+ if: startsWith(github.ref, 'refs/tags/')
80
+ needs: [publish-to-pypi]
81
+ runs-on: ubuntu-latest
82
+
83
+ permissions:
84
+ contents: write
85
+ id-token: write
86
+
87
+ steps:
88
+ - name: Download all the dists
89
+ uses: actions/download-artifact@v4
90
+ with:
91
+ name: python-package-distributions
92
+ path: dist/
93
+ - name: Sign the dists with Sigstore
94
+ uses: sigstore/gh-action-sigstore-python@v2.1.1
95
+ with:
96
+ inputs: >-
97
+ ./dist/*.tar.gz
98
+ ./dist/*.whl
99
+ - name: Create GitHub Release
100
+ env:
101
+ GITHUB_TOKEN: ${{ github.token }}
102
+ run: >-
103
+ gh release create
104
+ '${{ github.ref_name }}'
105
+ --repo '${{ github.repository }}'
106
+ --notes ""
107
+ - name: Upload artifact signatures to GitHub Release
108
+ env:
109
+ GITHUB_TOKEN: ${{ github.token }}
110
+ run: >-
111
+ gh release upload
112
+ '${{ github.ref_name }}' dist/**
113
+ --repo '${{ github.repository }}'
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .DS_Store
7
+ node_modules/
8
+ .runtime/
9
+ .pytest_cache/
10
+ prototype/
@@ -0,0 +1,7 @@
1
+ workspace:
2
+ id: 030fd6da-c0cc-428e-8c68-0b63593aa702
3
+ collections:
4
+ - path: postman/collections/energy-monitoring-api.json
5
+ - path: postman/collections/sp-mobile-api.json
6
+ environments:
7
+ - path: postman/environments/local.json
@@ -0,0 +1,56 @@
1
+ # spgroup SDK — Repository Guidelines
2
+
3
+ Async Python SDK for SP Group (Singapore Power) mobile APIs.
4
+
5
+ ## Project Structure
6
+
7
+ - `src/spgroup/` — SDK source package
8
+ - `tests/` — Hermetic SDK tests (default `mise run python:test`)
9
+ - `tests/e2e/` — Opt-in live tests against production SP Group endpoints
10
+ - `docs/api/` — Reverse-engineered API reference (APK v15.9.0)
11
+ - `docs/` — SDK-specific docs, roadmaps, plans
12
+ - `prototype/` — Original TypeScript reverse-engineering harness
13
+ - `postman/` — Postman collections for manual API testing
14
+
15
+ The HA integration that consumes this SDK lives in
16
+ [energy-monitoring](../energy-monitoring/).
17
+
18
+ ## Commands
19
+
20
+ Use `python`, not `python3`, when invoking Python directly.
21
+
22
+ - `mise run python:install` — Install SDK in editable mode with test/lint deps
23
+ - `mise run python:test` — Run hermetic SDK tests
24
+ - `mise run python:test:e2e` — Run opt-in live e2e tests
25
+ - `mise run python:lint` — Run Ruff and mypy
26
+ - `mise run python:build` — Build wheel for publishing
27
+
28
+ ## Publishing
29
+
30
+ The primary publishing path is **GitHub Actions CI** using Trusted Publishing.
31
+ Push a `vX.Y.Z` tag to trigger a production PyPI release.
32
+
33
+ ### Local/Developer Publishing (TestPyPI dry-run)
34
+
35
+ ```bash
36
+ mise run python:build
37
+ mise run python:publish:testpypi
38
+ ```
39
+
40
+ Only after verifying the TestPyPI install:
41
+
42
+ ```bash
43
+ mise run python:publish
44
+ ```
45
+
46
+ **Never run `python -m twine upload dist/*` without first confirming
47
+ via TestPyPI.**
48
+
49
+ ## Testing
50
+
51
+ Hermetic tests use fake transports and must not call live services.
52
+ Live tests require `--spgroup-live` and a cached mobile session.
53
+
54
+ ## Agent Instructions
55
+
56
+ Append discoveries and decisions to `docs/context-tracker.md`.
spgroup-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Aw Sheng Xiang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ exclude tests
2
+ recursive-exclude tests *
spgroup-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: spgroup
3
+ Version: 0.1.0
4
+ Summary: Async Python SDK scaffold for SP Group APIs
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/ashe0047/spgroup
7
+ Project-URL: Source, https://github.com/ashe0047/spgroup
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: aiohttp>=3.9
15
+ Dynamic: license-file
16
+
17
+ # spgroup
18
+
19
+ Async Python SDK for SP Group (Singapore Power) utility APIs.
20
+
21
+ Reverse-engineered from the SP Group Android mobile app (v15.9.0).
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install spgroup
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```python
32
+ import asyncio
33
+ from spgroup import MobileAuthClient, JarvisClient
34
+
35
+ async def main():
36
+ auth = MobileAuthClient(session=None)
37
+ print(f"Client ID: {auth._client_id}")
38
+
39
+ asyncio.run(main())
40
+ ```
41
+
42
+ ## Documentation
43
+
44
+ API reference and usage guides are available on
45
+ [GitHub](https://github.com/ashe0047/spgroup).
46
+
47
+ For developers working on the SDK: see `docs/`, `prototype/`, and `postman/`
48
+ in the [GitHub repository](https://github.com/ashe0047/spgroup).
49
+
50
+ ## License
51
+
52
+ MIT
@@ -0,0 +1,36 @@
1
+ # spgroup
2
+
3
+ Async Python SDK for SP Group (Singapore Power) utility APIs.
4
+
5
+ Reverse-engineered from the SP Group Android mobile app (v15.9.0).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install spgroup
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ import asyncio
17
+ from spgroup import MobileAuthClient, JarvisClient
18
+
19
+ async def main():
20
+ auth = MobileAuthClient(session=None)
21
+ print(f"Client ID: {auth._client_id}")
22
+
23
+ asyncio.run(main())
24
+ ```
25
+
26
+ ## Documentation
27
+
28
+ API reference and usage guides are available on
29
+ [GitHub](https://github.com/ashe0047/spgroup).
30
+
31
+ For developers working on the SDK: see `docs/`, `prototype/`, and `postman/`
32
+ in the [GitHub repository](https://github.com/ashe0047/spgroup).
33
+
34
+ ## License
35
+
36
+ MIT
@@ -0,0 +1,248 @@
1
+ # ADR: Two-Mode SDK Vendoring — Dev Vendor vs. Production Pip
2
+
3
+ ## Status
4
+
5
+ Revised — 2026-07-17 (replaced try/except fallback with unconditional vendor path)
6
+
7
+ ## Context
8
+
9
+ The SP Group HA custom integration depends on a reusable Python SDK
10
+ (`../python-packages/spgroup/src/spgroup/`) that provides Auth0 authentication,
11
+ SP Group API clients (Jarvis, Yggdrasil, Profile), and mobile session management.
12
+
13
+ The SDK lives in its own repo at `../python-packages/spgroup/`. During
14
+ development, the vendored copy at `custom_components/sp_group/vendor/spgroup/`
15
+ provides the SDK. For production, `manifest.json` declares
16
+ `"requirements": ["spgroup>=0.1.0"]` and HA installs the SDK from PyPI.
17
+
18
+ The integration has two distinct deployment scenarios:
19
+
20
+ | Scenario | SDK source | Import path | Works today? |
21
+ |----------|-----------|-------------|--------------|
22
+ | Development (local checkout) | Vendored copy in vendor/spgroup/ | `from spgroup.mobile import ...` | Yes |
23
+ | Production (HACS / PyPI) | Manifest requirements → pip install | `from spgroup.mobile import ...` | Yes |
24
+
25
+ The core challenge is making `from spgroup.mobile import ...` resolve correctly
26
+ in both modes without changing the import statement.
27
+
28
+ ### What the authoritative docs say
29
+
30
+ **Python Language Reference (import system):**
31
+ `sys.path` is a list of directories searched for modules. Programs can modify
32
+ it at runtime. Inserting a directory at position 0 makes it the first search
33
+ location. A non-existent path in `sys.path` is silently ignored during import
34
+ resolution.
35
+
36
+ **HA Developer Docs (manifest requirements):**
37
+ The `requirements` field in `manifest.json` is the official HA mechanism for
38
+ declaring pip dependencies. HA installs them automatically at startup.
39
+
40
+ **Repository split (2026-07-17):**
41
+ The SDK was extracted from the HA integration repo into its own repository at
42
+ `../python-packages/spgroup/`. The vendored copy is refreshed from this repo.
43
+
44
+ ### Constraints
45
+
46
+ 1. The import statement must be `from spgroup.mobile import ...` in all modes
47
+ 2. Dev workflow must not require pip-installing the SDK — the vendor directory
48
+ provides it directly
49
+ 3. Production installs must resolve via `manifest.json` requirements (pip)
50
+ 4. The existing `test_packaging_boundary.py` test allows `from spgroup.mobile
51
+ import ...` but blocks bare `import spgroup` and `from spgroup.mobile.auth
52
+ import ...` — the import pattern must not violate this contract
53
+
54
+ ### Risks
55
+
56
+ 1. **sys.path is process-global**: Adding a directory to `sys.path` affects all
57
+ Python code in the HA process. The name `spgroup` is narrow enough that
58
+ conflicts are unlikely, but it must be documented.
59
+ 2. **Stale vendored copy**: The vendored SDK is a snapshot. If the upstream SDK
60
+ is updated, the vendored copy must be manually refreshed:
61
+ `cp -a ../python-packages/spgroup/src/spgroup/* vendor/spgroup/`
62
+
63
+ ## Decision
64
+
65
+ **Use an unconditional sys.path insert** in the integration's `__init__.py`.
66
+ The vendor path is always added. In development (local checkout), the vendor
67
+ directory exists on disk and provides the SDK. In production (HACS/PyPI), the
68
+ vendor directory is not shipped and the pip-installed SDK takes over.
69
+
70
+ ```python
71
+ from __future__ import annotations
72
+
73
+ from pathlib import Path
74
+
75
+ # Dev mode: vendor directory provides the SDK directly.
76
+ # Production: vendor not shipped; manifest.json installs spgroup from pip.
77
+ import sys
78
+ _VENDOR_PATH = str(Path(__file__).resolve().parent / "vendor")
79
+ if _VENDOR_PATH not in sys.path:
80
+ sys.path.insert(0, _VENDOR_PATH)
81
+
82
+ from spgroup.mobile import JarvisClient, MobileAuthClient, ...
83
+ ```
84
+
85
+ ### Mode 1: Development (local checkout)
86
+
87
+ The vendored SDK copy lives at `custom_components/sp_group/vendor/spgroup/`.
88
+ When `import spgroup` is attempted:
89
+
90
+ 1. `sys.path` includes the vendor directory (unconditionally inserted)
91
+ 2. Python finds `spgroup` at `vendor/spgroup/` — returns it
92
+ 3. `from spgroup.mobile import ...` resolves to the **vendored copy**
93
+ 4. To update the vendored copy: `cp -a ../python-packages/spgroup/src/spgroup/* vendor/spgroup/`
94
+ 5. If the SDK is also pip-installed, the vendored copy takes priority
95
+ (sys.path[0] is checked before site-packages)
96
+
97
+ **sys.path is modified once at import time, then cached for the process lifetime.**
98
+
99
+ ### Mode 2: Production / HACS (pip-installed SDK)
100
+
101
+ The vendored copy is not shipped with the production package. When
102
+ `import spgroup` is attempted:
103
+
104
+ 1. `sys.path` includes the vendor directory (unconditionally inserted, but the
105
+ path does not exist on disk — harmless)
106
+ 2. Python does not find `spgroup` at the non-existent vendor path
107
+ 3. Python continues searching `sys.path` — finds `spgroup` in site-packages
108
+ (installed via `manifest.json` requirements)
109
+ 4. `from spgroup.mobile import ...` resolves to the **pip-installed SDK**
110
+
111
+ Adding a non-existent path to `sys.path` is silently ignored by Python's
112
+ import machinery.
113
+
114
+ ### Import path resolution summary
115
+
116
+ ```
117
+ sys.path[0] = vendor/ (always inserted)
118
+
119
+ ├── vendor/spgroup/ exists on disk? ─── Yes ──→ Import from vendored copy
120
+ │ (development)
121
+
122
+ └── vendor/spgroup/ does not exist
123
+
124
+ └── sys.path search continues
125
+
126
+ └── spgroup found in site-packages?
127
+
128
+ ├── Yes ──→ Import from pip-installed SDK (production)
129
+
130
+ └── No ──→ ImportError (misconfigured install)
131
+ ```
132
+
133
+ ### Comparison to previous approach
134
+
135
+ The previous approach used a try/except fallback pattern:
136
+
137
+ ```python
138
+ try:
139
+ import spgroup # pip/editable install exists
140
+ except ImportError:
141
+ _VENDOR_PATH = ...
142
+ if _VENDOR_PATH not in sys.path:
143
+ sys.path.insert(0, _VENDOR_PATH)
144
+ ```
145
+
146
+ This had dual-mode priority where pip-installed took precedence and the
147
+ vendored copy was a fallback. The unconditional approach inverts and
148
+ simplifies this:
149
+
150
+ | Installation | SDK source |
151
+ |-------------|-----------|
152
+ | Dev checkout (vendor exists on disk) | Vendored copy (sys.path[0]) |
153
+ | Production (vendor not shipped) | Pip-installed SDK (site-packages) |
154
+
155
+ Reasons for the change:
156
+
157
+ - **Clear decision split**: Vendor = dev, pip = production. No ambiguous
158
+ try/except priority.
159
+ - **No import overhead**: The unconditional insert costs ~0.01ms (one sys.path
160
+ mutation) vs ~0.5ms for a failed import + mutation in the try/except approach.
161
+ - **Manifest requirements now declared**: The manifest explicitly requires
162
+ `spgroup>=0.1.0`, so production installs fail fast if the SDK is missing.
163
+ - **SDK in its own repo**: After the repo split, the vendored copy is a
164
+ deliberate snapshot, refreshed from `../python-packages/spgroup/`.
165
+
166
+ ### Implementation rules
167
+
168
+ 1. **Self-containment**: The vendored SDK lives at
169
+ `custom_components/sp_group/vendor/spgroup/` and is a complete, regular
170
+ Python package with `__init__.py` files, not a namespace package.
171
+
172
+ 2. **sys.path hygiene**: Insert only once, using the `if path not in
173
+ sys.path` guard. No cleanup needed — the path entry is harmless for the
174
+ process lifetime.
175
+
176
+ 3. **Test compatibility**: The `test_packaging_boundary.py` test enforces that
177
+ HA code must not use `import spgroup` (bare top-level) or
178
+ `from spgroup.mobile.auth import ...` (deep submodule). The allowed
179
+ `from spgroup.mobile import ...` pattern matches our usage.
180
+
181
+ 4. **No import changes**: The same `from spgroup.mobile import ...` statement
182
+ works in both modes. No `from .vendor.spgroup.mobile import ...` or other
183
+ path-manipulation imports are needed.
184
+
185
+ 5. **Manifest requirements**: `manifest.json` declares
186
+ `"requirements": ["spgroup>=0.1.0"]`. In production, HA installs the SDK
187
+ via pip. In development, the vendored copy shadows the pip install.
188
+
189
+ ## Consequences
190
+
191
+ ### Positive
192
+
193
+ - Single, clean import path everywhere: `from spgroup.mobile import ...`
194
+ - Development workflow does not require pip-installing the SDK
195
+ - Production installs get the SDK via HA's built-in dependency resolution
196
+ - The vendored copy is an explicit dev-mode decision, not a fallback
197
+ - The manifest explicitly declares the dependency
198
+
199
+ ### Negative
200
+
201
+ - `sys.path` manipulation is process-global; the `spgroup` name is sufficiently
202
+ unique but must be documented
203
+ - The vendored copy must be manually refreshed from the SDK repo when the
204
+ SDK source changes
205
+ - If the vendored copy and pip-installed SDK are both present, the vendored
206
+ copy wins (sys.path[0] priority) — this is correct for dev but could
207
+ mask version mismatches
208
+
209
+ ## Alternatives Considered
210
+
211
+ ### try/except fallback (previous approach, now superseded)
212
+
213
+ The initial approach used `try: import spgroup / except ImportError` to
214
+ prefer the pip-installed SDK, falling back to the vendored copy. Superseded
215
+ because:
216
+ - The fallback logic added import overhead for production installs
217
+ - The pip-installed SDK could silently shadow a stale vendored copy during
218
+ development if the developer forgot to uninstall the pip version
219
+ - The manifest had empty requirements — no explicit dependency declaration
220
+ - After the SDK repo split, the unconditional approach makes the dev vs.
221
+ production boundary clearer
222
+
223
+ ### Relative imports via `.vendor.spgroup.mobile` (rejected)
224
+
225
+ The initial vendoring implementation used `from .vendor.spgroup.mobile import
226
+ ...` — a direct relative import into the vendored copy. This was rejected
227
+ because:
228
+ - Import path differs from the natural `from spgroup.mobile import ...`
229
+ - Tests need a different import path
230
+ - Cannot support the dual-mode (dev vs production) pattern
231
+ - No graceful fallback when the SDK is published to PyPI
232
+
233
+ ### Copy SDK files directly into the custom component root (rejected)
234
+
235
+ Placing `spgroup/` as a sibling of `__init__.py` would make it importable as
236
+ `from spgroup.mobile import ...` without `sys.path` manipulation. Rejected
237
+ because:
238
+ - The SDK `spgroup/` dir and the HA integration code (`__init__.py`, `auth.py`,
239
+ etc.) would intermingle in the same directory, creating confusion
240
+ - The `spgroup/` top-level import name would shadow any pip-installed version
241
+ - The `__init__.py` files would be easy to confuse between the SDK and the HA
242
+ integration
243
+
244
+ ### Copy-once via a setup script (rejected)
245
+
246
+ A build script that copies the SDK into the custom component at release time.
247
+ Rejected because it adds build complexity and the unconditional path approach
248
+ handles both modes more simply.
@@ -0,0 +1,88 @@
1
+ # SP Group Mobile API Reference
2
+
3
+ > Reconstructed from `sg.com.singaporepower.spservices` APK v15.9.0
4
+ > Decompiled via JADX from production APK splits (base + arm64_v8a).
5
+
6
+ ## Document Structure
7
+
8
+ | File | Covers |
9
+ |------|--------|
10
+ | `auth0-flow.md` | Authentication flow: password-realm login, MFA, token refresh |
11
+ | `auth0-api.md` | Auth0 `/oauth/token` grant types, `/mfa/*` endpoints |
12
+ | `jarvis-api.md` | Utility accounts, usage charts, meter readings, green goals |
13
+ | `skalbox-api.md` | Meter reading, billing preferences, customer consent |
14
+ | `yggdrasil-api.md` | Premises management, sharing, invites |
15
+ | `payment-api.md` | Njord/Nomisma/Sersi payments, subscriptions, UND |
16
+ | `ev-api.md` | EV charging: locations, sessions, subscriptions |
17
+ | `greenup-api.md` | GreenUp quests, OneUp GraphQL, advocado rewards |
18
+ | `frieza-api.md` | Tengah cooling, FCU pairing, appointments |
19
+ | `content-api.md` | Magnolia banners/themes, CMS, system status |
20
+ | `utility-apis.md` | Paladin PPMS, Titan EMA, Mera, Halp, minor APIs |
21
+ | `models.md` | Data models: Credentials, JWT, Premise, Accounts, Charts |
22
+ | `network-layer.md` | Retrofit builders, interceptor chain, error handling |
23
+ | `queries.md` | Full GraphQL query/mutation strings from the APK |
24
+
25
+ ## Base URLs
26
+
27
+ | Context | Base URL |
28
+ |---------|----------|
29
+ | Auth0 (mobile) | `https://identity.spdigital.sg` |
30
+ | Authenticated MuleSoft | `https://b2c.api.spdigital.sg` |
31
+ | Public MuleSoft | `https://public.api.spdigital.sg` |
32
+ | Mera API | `https://mera.api.spdigital.sg` |
33
+ | SPC-HC Gateway | `https://sphc-gateway.api.spdigital.sg` |
34
+
35
+ ## Authentication Headers
36
+
37
+ All authenticated requests to `b2c.api.spdigital.sg` include:
38
+
39
+ ```
40
+ Authorization: Bearer <access_token>
41
+ X-id-token: <id_token>
42
+ ```
43
+
44
+ ## Response Wrapper
45
+
46
+ Most endpoints return a `ResourceV2<T>` envelope:
47
+
48
+ ```json
49
+ {
50
+ "data": { ... },
51
+ "error": { "code": "ERROR_CODE", "message": "..." }
52
+ }
53
+ ```
54
+
55
+ ## Domain Glossary
56
+
57
+ | Term | Meaning |
58
+ |------|---------|
59
+ | Premise | Physical address with utility services |
60
+ | BP Number | Billing Party number |
61
+ | MSSL | Meter Serial Number |
62
+ | EBS | Enterprise Billing System reference |
63
+ | PPMS | Pre-Payment Metering System |
64
+ | FCU | Fan Coil Unit (Tengah cooling) |
65
+ | UND | UniDollar payment token |
66
+ | EVSE | Electric Vehicle Supply Equipment |
67
+ | CPO | Charge Point Operator |
68
+ | YCS | Yggdrasil Core Services |
69
+ | OOB | Out-of-Band (SMS MFA) |
70
+ | AMI | Advanced Metering Infrastructure |
71
+ | SMRD | Self-Meter Reading Data |
72
+
73
+ ## Service-to-Domain Mapping
74
+
75
+ | Service | Backend | Domain |
76
+ |---------|---------|--------|
77
+ | Jarvis | MuleSoft (b2c) | Utility accounts, charts, meters |
78
+ | Skalbox | MuleSoft (b2c + public) | Meter reading, billing |
79
+ | Yggdrasil | MuleSoft (b2c) | Premises, sharing |
80
+ | Njord | MuleSoft (b2c) | Payments, payables |
81
+ | Nomisma | MuleSoft (b2c) | EV subscriptions |
82
+ | EVA | MuleSoft (b2c) | EV charging |
83
+ | Frosty | MuleSoft (b2c) | Tengah cooling |
84
+ | Frieza | MuleSoft (b2c) | Servicing, warranty |
85
+ | OneUp | MuleSoft (b2c + public) | GreenUp quests |
86
+ | Paladin | MuleSoft (b2c) | PPMS |
87
+ | Magnolia | Contents CDN | Banners, themes |
88
+ | Sersi | MuleSoft (b2c) | UND authorization |