spiderwire 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,36 @@
1
+ # Byte-compiled / cache
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ wheels/
12
+
13
+ # Virtual envs
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Tooling caches
19
+ .ruff_cache/
20
+ .pytest_cache/
21
+ .mypy_cache/
22
+ .tox/
23
+ .coverage
24
+ htmlcov/
25
+
26
+ # uv
27
+ uv.lock.local
28
+
29
+ # Local secrets / env
30
+ .env
31
+ .env.local
32
+
33
+ # IDE / OS
34
+ .idea/
35
+ .vscode/
36
+ .DS_Store
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-05-09
11
+
12
+ Initial release.
13
+
14
+ ### Added
15
+
16
+ - `spiderwire` library: Modbus RTU protocol (`protocol`), RS-485 transport
17
+ (`transport`), per-device register map and decoders (`registers`), and
18
+ tiered bus master with setpoint heartbeat (`bus`).
19
+ - `gss-ctrl` CLI: `scan`, `poll`, `read`, `write`, `fan`, `blower`,
20
+ `light` commands. Acts as a stand-in master for the OEM SpiderFarmer
21
+ GSS hub on a USB-RS485 adapter.
22
+ - Reference docs: `docs/protocol-analysis.md`, `docs/device-map.md`,
23
+ `docs/hw-notes.md`.
24
+
25
+ [Unreleased]: https://github.com/1am/spiderwire/compare/v0.1.0...HEAD
26
+ [0.1.0]: https://github.com/1am/spiderwire/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1AM
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,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: spiderwire
3
+ Version: 0.1.0
4
+ Summary: SpiderWire — SpiderFarmer GSS Modbus protocol library + gss-ctrl CLI
5
+ Project-URL: Homepage, https://github.com/1am/spiderwire
6
+ Project-URL: Source, https://github.com/1am/spiderwire
7
+ Project-URL: Issues, https://github.com/1am/spiderwire/issues
8
+ Project-URL: Changelog, https://github.com/1am/spiderwire/blob/main/CHANGELOG.md
9
+ Author: 1AM
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: grow,gss,home-assistant,horticulture,modbus,modbus-rtu,rs485,spiderfarmer
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Home Automation
25
+ Classifier: Topic :: System :: Hardware :: Hardware Drivers
26
+ Classifier: Topic :: Terminals :: Serial
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Requires-Dist: pyserial>=3.5
30
+ Description-Content-Type: text/markdown
31
+
32
+ # SpiderWire
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/spiderwire.svg)](https://pypi.org/project/spiderwire/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/spiderwire.svg)](https://pypi.org/project/spiderwire/)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
37
+ [![CI](https://github.com/1am/spiderwire/actions/workflows/ci.yml/badge.svg)](https://github.com/1am/spiderwire/actions/workflows/ci.yml)
38
+
39
+ Open Modbus RTU library and `gss-ctrl` CLI for the **SpiderFarmer GSS**
40
+ peripheral bus (inline fans, CO₂ sensor, sensor hub, light driver). The
41
+ OEM GSS hub is a Modbus RTU master over a proprietary RJ12 wire layout;
42
+ SpiderWire replaces that hub so you can drive the bus from any host -
43
+ locally, no cloud account.
44
+
45
+ > **Unofficial and experimental.** This is an independent project with no
46
+ > affiliation, endorsement, or relationship with SpiderFarmer. It works
47
+ > on my hardware, but the GSS ecosystem ships in many hardware and
48
+ > firmware revisions - yours may behave differently or not work at all.
49
+ > Expect rough edges and verify behavior on your own bus before relying
50
+ > on it.
51
+
52
+ Two surfaces, one library:
53
+
54
+ | Surface | Role | What ships with it |
55
+ | --------------------------- | ------------------------------------------ | ------------------------------------------------------------ |
56
+ | `spiderwire` Python package | Transport, register map, tiered bus master | `bus.py`, `protocol.py`, `registers.py`, `transport.py` |
57
+ | `gss-ctrl` CLI | Test / ops / manual control over USB-RS485 | `gss-ctrl scan / poll / read / write / fan / blower / light` |
58
+
59
+ A companion Home Assistant integration is under [spiderfarmer-ha](https://github.com/1am/spiderfarmer-ha).
60
+
61
+ ## Hardware
62
+
63
+ See [./docs/hw-notes.md](./docs/hw-notes.md) for more details on how to connect to the bus.
64
+
65
+ ## Usage
66
+
67
+ You can use it independently as a controller and a reader from the bus
68
+
69
+ [![asciicast](https://asciinema.org/a/1030214.svg)](https://asciinema.org/a/1030214)
70
+
71
+ It is also possible to just sniff the bus with GSS connected to see what is happening but as expecteed there will be quite a few CRC error with
72
+ 2 devices polling on the same bus.
73
+
74
+ [![asciicast](https://asciinema.org/a/pjMOnAvHK90CbKit.svg)](https://asciinema.org/a/pjMOnAvHK90CbKit)
75
+
76
+ ## Install
77
+
78
+ From PyPI (recommended):
79
+
80
+ ```bash
81
+ pip install spiderwire
82
+ ```
83
+
84
+ From source (for development):
85
+
86
+ ```bash
87
+ git clone https://github.com/1am/spiderwire.git
88
+ cd spiderwire
89
+ uv sync # installs runtime + dev tools (pytest, ruff, build, twine)
90
+ # or, with pip:
91
+ pip install -e .
92
+ pip install pytest ruff build twine
93
+ ```
94
+
95
+ Requires Python 3.10+ and a USB-RS485 adapter (`pyserial` is the only
96
+ runtime dependency).
97
+
98
+ ## CLI quickstart
99
+
100
+ ```bash
101
+ make scan PORT=/dev/ttyUSB0 # discover devices on the bus
102
+ make poll PORT=/dev/ttyUSB0 # master mode: tiered poll + heartbeat
103
+ make read PORT=... ADDR=0x0A QTY=28
104
+ make fan PORT=... ADDR=0x04 SPEED=15
105
+ make light PORT=... PCT=50
106
+ make blower PORT=... PCT=40
107
+ ```
108
+
109
+ Without the OEM GSS hub on the bus, **`gss-ctrl poll`** takes over
110
+ master duties: tiered polling (~1 s fast, ~2.5 s actuators, ~7 s scan)
111
+ plus the setpoint heartbeat broadcast (~3.5 s), matching the OEM
112
+ cadence peripherals expect.
113
+
114
+ Full CLI: `gss-ctrl --help`. Commands: `scan`, `poll`, `read`, `write`,
115
+ `fan`, `blower`, `light`.
116
+
117
+ ## Library use
118
+
119
+ ```python
120
+ from spiderwire import BusMaster, RS485Transport
121
+
122
+ with RS485Transport("/dev/ttyUSB0", baudrate=115200) as tx:
123
+ bus = BusMaster(tx)
124
+ bus.poll_loop(interval=1.0, callback=print)
125
+ ```
126
+
127
+ See `src/spiderwire/bus.py` for the tiered scheduler,
128
+ `src/spiderwire/registers.py` for the per-device register map, and
129
+ `src/spiderwire/transport.py` for the RS-485 framer.
130
+
131
+ ## Layout
132
+
133
+ ```
134
+ spiderwire/
135
+ ├── src/
136
+ │ ├── spiderwire/ Python package (lib)
137
+ │ │ ├── bus.py tiered master + heartbeat scheduler
138
+ │ │ ├── protocol.py Modbus RTU framing + CRC
139
+ │ │ ├── registers.py per-device register map + decoders
140
+ │ │ └── transport.py RS-485 framer over pyserial
141
+ │ └── gss_ctrl_pc/ gss-ctrl CLI (stand-in for the OEM master)
142
+ ├── tests/ pytest suite (no hardware required)
143
+ ├── docs/ protocol + device map reference
144
+ ├── pyproject.toml builds the `spiderwire` wheel + `gss-ctrl` script
145
+ └── Makefile dev shortcuts
146
+ ```
147
+
148
+ ## Docs
149
+
150
+ - [`docs/device-map.md`](docs/device-map.md) - per-device register map
151
+ for every address observed on the bus.
152
+ - [`docs/protocol-analysis.md`](docs/protocol-analysis.md) - protocol
153
+ reference: physical layer, function codes, tiered polling, heartbeat.
154
+ - [`docs/hw-notes.md`](docs/hw-notes.md) - OEM hub board notes and RJ12
155
+ pinout.
156
+
157
+ ## License
158
+
159
+ Copyright (c) 2026 [1AM](https://1am.pl)
160
+
161
+ Released under the [MIT License](LICENSE) - free to use, modify, and
162
+ distribute, including in commercial and closed-source products. The
163
+ only requirement is that the copyright notice and license text are
164
+ preserved in copies or substantial portions of the software.
165
+
166
+ ## Disclaimer
167
+
168
+ This software is provided "as is", without warranty of any kind, express
169
+ or implied, including but not limited to the warranties of
170
+ merchantability, fitness for a particular purpose, and non-infringement.
171
+
172
+ This project interacts with mains-powered grow equipment over an RS-485
173
+ bus. Incorrect wiring, miswired connectors, unsupported devices, or
174
+ misuse of the protocol can damage hardware, void manufacturer warranties,
175
+ cause fire, or result in personal injury. You are solely responsible for
176
+ verifying the correctness of your wiring, your device configuration, and
177
+ the commands you send.
178
+
179
+ In no event shall the author or contributors be liable for any direct,
180
+ indirect, incidental, special, exemplary, or consequential damages -
181
+ including but not limited to damage to equipment, crops, property, or
182
+ persons - arising from the use of, or inability to use, this software.
183
+
184
+ Use at your own risk.
@@ -0,0 +1,153 @@
1
+ # SpiderWire
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/spiderwire.svg)](https://pypi.org/project/spiderwire/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/spiderwire.svg)](https://pypi.org/project/spiderwire/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![CI](https://github.com/1am/spiderwire/actions/workflows/ci.yml/badge.svg)](https://github.com/1am/spiderwire/actions/workflows/ci.yml)
7
+
8
+ Open Modbus RTU library and `gss-ctrl` CLI for the **SpiderFarmer GSS**
9
+ peripheral bus (inline fans, CO₂ sensor, sensor hub, light driver). The
10
+ OEM GSS hub is a Modbus RTU master over a proprietary RJ12 wire layout;
11
+ SpiderWire replaces that hub so you can drive the bus from any host -
12
+ locally, no cloud account.
13
+
14
+ > **Unofficial and experimental.** This is an independent project with no
15
+ > affiliation, endorsement, or relationship with SpiderFarmer. It works
16
+ > on my hardware, but the GSS ecosystem ships in many hardware and
17
+ > firmware revisions - yours may behave differently or not work at all.
18
+ > Expect rough edges and verify behavior on your own bus before relying
19
+ > on it.
20
+
21
+ Two surfaces, one library:
22
+
23
+ | Surface | Role | What ships with it |
24
+ | --------------------------- | ------------------------------------------ | ------------------------------------------------------------ |
25
+ | `spiderwire` Python package | Transport, register map, tiered bus master | `bus.py`, `protocol.py`, `registers.py`, `transport.py` |
26
+ | `gss-ctrl` CLI | Test / ops / manual control over USB-RS485 | `gss-ctrl scan / poll / read / write / fan / blower / light` |
27
+
28
+ A companion Home Assistant integration is under [spiderfarmer-ha](https://github.com/1am/spiderfarmer-ha).
29
+
30
+ ## Hardware
31
+
32
+ See [./docs/hw-notes.md](./docs/hw-notes.md) for more details on how to connect to the bus.
33
+
34
+ ## Usage
35
+
36
+ You can use it independently as a controller and a reader from the bus
37
+
38
+ [![asciicast](https://asciinema.org/a/1030214.svg)](https://asciinema.org/a/1030214)
39
+
40
+ It is also possible to just sniff the bus with GSS connected to see what is happening but as expecteed there will be quite a few CRC error with
41
+ 2 devices polling on the same bus.
42
+
43
+ [![asciicast](https://asciinema.org/a/pjMOnAvHK90CbKit.svg)](https://asciinema.org/a/pjMOnAvHK90CbKit)
44
+
45
+ ## Install
46
+
47
+ From PyPI (recommended):
48
+
49
+ ```bash
50
+ pip install spiderwire
51
+ ```
52
+
53
+ From source (for development):
54
+
55
+ ```bash
56
+ git clone https://github.com/1am/spiderwire.git
57
+ cd spiderwire
58
+ uv sync # installs runtime + dev tools (pytest, ruff, build, twine)
59
+ # or, with pip:
60
+ pip install -e .
61
+ pip install pytest ruff build twine
62
+ ```
63
+
64
+ Requires Python 3.10+ and a USB-RS485 adapter (`pyserial` is the only
65
+ runtime dependency).
66
+
67
+ ## CLI quickstart
68
+
69
+ ```bash
70
+ make scan PORT=/dev/ttyUSB0 # discover devices on the bus
71
+ make poll PORT=/dev/ttyUSB0 # master mode: tiered poll + heartbeat
72
+ make read PORT=... ADDR=0x0A QTY=28
73
+ make fan PORT=... ADDR=0x04 SPEED=15
74
+ make light PORT=... PCT=50
75
+ make blower PORT=... PCT=40
76
+ ```
77
+
78
+ Without the OEM GSS hub on the bus, **`gss-ctrl poll`** takes over
79
+ master duties: tiered polling (~1 s fast, ~2.5 s actuators, ~7 s scan)
80
+ plus the setpoint heartbeat broadcast (~3.5 s), matching the OEM
81
+ cadence peripherals expect.
82
+
83
+ Full CLI: `gss-ctrl --help`. Commands: `scan`, `poll`, `read`, `write`,
84
+ `fan`, `blower`, `light`.
85
+
86
+ ## Library use
87
+
88
+ ```python
89
+ from spiderwire import BusMaster, RS485Transport
90
+
91
+ with RS485Transport("/dev/ttyUSB0", baudrate=115200) as tx:
92
+ bus = BusMaster(tx)
93
+ bus.poll_loop(interval=1.0, callback=print)
94
+ ```
95
+
96
+ See `src/spiderwire/bus.py` for the tiered scheduler,
97
+ `src/spiderwire/registers.py` for the per-device register map, and
98
+ `src/spiderwire/transport.py` for the RS-485 framer.
99
+
100
+ ## Layout
101
+
102
+ ```
103
+ spiderwire/
104
+ ├── src/
105
+ │ ├── spiderwire/ Python package (lib)
106
+ │ │ ├── bus.py tiered master + heartbeat scheduler
107
+ │ │ ├── protocol.py Modbus RTU framing + CRC
108
+ │ │ ├── registers.py per-device register map + decoders
109
+ │ │ └── transport.py RS-485 framer over pyserial
110
+ │ └── gss_ctrl_pc/ gss-ctrl CLI (stand-in for the OEM master)
111
+ ├── tests/ pytest suite (no hardware required)
112
+ ├── docs/ protocol + device map reference
113
+ ├── pyproject.toml builds the `spiderwire` wheel + `gss-ctrl` script
114
+ └── Makefile dev shortcuts
115
+ ```
116
+
117
+ ## Docs
118
+
119
+ - [`docs/device-map.md`](docs/device-map.md) - per-device register map
120
+ for every address observed on the bus.
121
+ - [`docs/protocol-analysis.md`](docs/protocol-analysis.md) - protocol
122
+ reference: physical layer, function codes, tiered polling, heartbeat.
123
+ - [`docs/hw-notes.md`](docs/hw-notes.md) - OEM hub board notes and RJ12
124
+ pinout.
125
+
126
+ ## License
127
+
128
+ Copyright (c) 2026 [1AM](https://1am.pl)
129
+
130
+ Released under the [MIT License](LICENSE) - free to use, modify, and
131
+ distribute, including in commercial and closed-source products. The
132
+ only requirement is that the copyright notice and license text are
133
+ preserved in copies or substantial portions of the software.
134
+
135
+ ## Disclaimer
136
+
137
+ This software is provided "as is", without warranty of any kind, express
138
+ or implied, including but not limited to the warranties of
139
+ merchantability, fitness for a particular purpose, and non-infringement.
140
+
141
+ This project interacts with mains-powered grow equipment over an RS-485
142
+ bus. Incorrect wiring, miswired connectors, unsupported devices, or
143
+ misuse of the protocol can damage hardware, void manufacturer warranties,
144
+ cause fire, or result in personal injury. You are solely responsible for
145
+ verifying the correctness of your wiring, your device configuration, and
146
+ the commands you send.
147
+
148
+ In no event shall the author or contributors be liable for any direct,
149
+ indirect, incidental, special, exemplary, or consequential damages -
150
+ including but not limited to damage to equipment, crops, property, or
151
+ persons - arising from the use of, or inability to use, this software.
152
+
153
+ Use at your own risk.
Binary file
@@ -0,0 +1,212 @@
1
+ # Device Map — Current Bus State
2
+
3
+ Snapshot of what actually lives on the RS-485 bus as captured by the
4
+ `gss-ctrl` CLI (`gss-ctrl <serial> scan` / `read`, or `make scan` /
5
+ `make read` from the repo root). Example port:
6
+ `/dev/cu.usbserial-130` (macOS).
7
+ See [`protocol-analysis.md`](./protocol-analysis.md) for the full
8
+ protocol write-up and historical observations; this file is a concise,
9
+ per-device reference grouped by role.
10
+
11
+ **Code:** parsing, tier lists, and `DEFAULT_QTY` live in
12
+ [`registers.py`](../src/spiderwire/registers.py);
13
+ orchestration in
14
+ [`bus.py`](../src/spiderwire/bus.py).
15
+ The Home Assistant custom component
16
+ [`custom_components/spiderfarmer/`](https://github.com/1am/spiderfarmer-ha/tree/main/custom_components/spiderfarmer/)
17
+ drives the same `BusMaster` + `RS485Transport` stack (see below).
18
+
19
+ ## Summary
20
+
21
+ | Addr | Role | Device | Header type (reg 6) | Model (reg 4) | FW (reg 2–3) | HW (reg 7) | Reliable register qty |
22
+ | ---- | --------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------- | ------------- | --------------------- | ---------- | ----------------------------------------------------------- |
23
+ | 0x03 | **Sensor** | CO₂ sensor | `0x0308` | `0x0330` | `"05.12"` (ASCII) | `0x0202` | 13 |
24
+ | 0x0A | **Sensor** + actuator | Sensor hub (air T/RH, soil T, PPFD); `reg 19` is the *reported* light value, not a setpoint | `0x0400` | `0x0330` | `"05.12"` (ASCII) | `0x0202` | 28 |
25
+ | 0x04 | **Actuator** | **Light dimmer (primary)** — accepts FC06 → reg 10 in **percent (0-100)**, never echoes writes | `0x0301` | `0x0426` | `0xF784C96D` (binary) | `0x0105` | 24 — silent to FC03 in current rig, but acts on FC06 writes |
26
+ | 0x06 | **Actuator** | **Blower / ventilation** (OEM UI calls it *"Light 2"*) — FC06 → **reg 14** in percent (0-100), *does* echo | `0x0204` | `0x043D` | `0xF784C90F` (binary) | `0x0102` | 16 |
27
+
28
+ `DeviceHeader.type_name` in `registers.py` (`LIGHT=0x02`, `FAN=0x03`,
29
+ `SENSOR_HUB=0x04`) maps **only the high byte** of register 6. That is
30
+ why the CLI prints `fan` for the CO₂ sensor at `0x03` (header type high
31
+ byte is `0x03`). The subtype in the low byte is what actually
32
+ distinguishes CO₂ (`0x08`) from the duct fan (`0x01`).
33
+
34
+ ## Tier placement (OEM orchestration)
35
+
36
+ `BusMaster.tick()` (see `bus.py`) interleaves the same four schedules the
37
+ OEM GSS hub uses (see `protocol-analysis.md` §"Bus Timing"). On each call,
38
+ actuators run **when their ~2.5 s deadline has elapsed** (before the fast
39
+ pair), the fast sensor pair **always** runs, then heartbeat / silent scan
40
+ fire on their own timers:
41
+
42
+ | Tier | Cadence | Addresses / action |
43
+ | ----------------------- | ------------------------------ | ------------------------------------------------------------------------ |
44
+ | B — actuators | ~2.5 s | `0x04`, `0x06` (FC03 read when due) |
45
+ | A — fast sensors | every tick (~1.0 s outer loop) | `0x03`, `0x0A` |
46
+ | D — heartbeat broadcast | ~3.5 s | FC 0x10 → 0x00, 26 regs @ 1001 |
47
+ | C — silent-slot scan | ~7.0 s | `0x10, 0x02, 0x01, 0x0C, 0x05, 0x07, 0x0B, 0x0D, 0x0E, 0x08, 0x0F, 0x13` |
48
+
49
+ Moving a device between tiers is editing `FAST_ADDRS`,
50
+ `ACTUATOR_ADDRS`, or `SCAN_ADDRS` in `spiderwire/registers.py` (or
51
+ mutating `bus.fast_addrs` / `actuator_addrs` / `scan_addrs` at runtime).
52
+
53
+ ### Home Assistant (`spiderfarmer`)
54
+
55
+ The integration's coordinator calls `BusMaster.tick()` once per HA
56
+ update (`DEFAULT_SCAN_INTERVAL` = 1 s in `custom_components/spiderfarmer/const.py`), so the Python
57
+ master tracks the OEM cadence internally.
58
+
59
+ | HA platform | What appears | Bus mapping |
60
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
61
+ | **Sensor** | Air temp, humidity, VPD, soil temp, PPFD on the hub; CO₂ ppm on `0x03` | Typed `SensorHubData` / `CO2SensorData` from coordinator data; unique ids `sf_0x0a_<key>` / `sf_0x03_co2` (hex lower case). |
62
+ | **Light** | One **Light 1** entity, attached to the **dimmer device (`0x04`)** so the user sees it under "Light Driver" rather than under the sensor hub | State from hub regs **18** (on/off) and **19** (reported %). `turn_on` writes hub reg 18 → 1, dimmer `0x04` reg **16** → 1 and reg **10** → % — **all FC06 blind**, neither the hub nor the dimmer echoes writes on this firmware. `turn_off` writes hub reg 18 → 0 and dimmer reg 10 → 0. Unique id stays `sf_0x0a_light1` (legacy, hub-scoped) so existing installs don't lose their entity. |
63
+ | **Fan** | **Blower** for each `BlowerData` (today: `0x06`) | HA fan entity with `translation_key` blower; unique id `sf_0x06_blower`. Writes FC06 → reg **14** (0–100 %), waits for echo. |
64
+ | **Number** | **Light brightness** (under "Light Driver") and **Blower speed** (under "Blower") — direct 0–100 % sliders visible on the dashboard / tile cards, complementing the on/off light + fan entities | Brightness writes the same dimmer reg 10 (with reg 16 = 1 latch) as the light entity but does **not** touch the hub gate. Blower speed writes blower reg 14, identical to the fan entity's `set_percentage`. Unique ids `sf_<dimmer>_brightness` / `sf_<blower>_blower_percent`. |
65
+
66
+ All four platforms use a **dynamic discovery** loop (`setup_dynamic_entities` in `entity.py`): entities are added both at first refresh **and** on subsequent coordinator updates as new addresses appear. This is what makes the CO₂ sensor at `0x03` show up reliably even when it misses the very first poll burst — without it, anything not on the bus during HA's startup tick would stay invisible until a full reload.
67
+
68
+ Device names in the HA UI are derived from the *parsed* device class
69
+ (`SensorHubData` → "Sensor Hub", `CO2SensorData` → "CO₂ Sensor",
70
+ `FanControllerData` → "Light Driver", `BlowerData` → "Blower" — see
71
+ `_ROLE_NAMES` in `entity.py`), **not** from `DeviceHeader.type_name`.
72
+ The OEM SKUs on this rig all mis-identify themselves in register 6
73
+ (0x03 CO₂ reports `fan`, 0x04 dimmer reports `fan`, 0x06 blower reports
74
+ `light`); using `type_name` for the device label is what produced the
75
+ "Blower device with a CO₂ sensor inside" misnaming on first install.
76
+
77
+ There is no separate switch platform: light enable is folded into the
78
+ light entity. Legacy `BusMaster` helpers (`set_fan_speed` /
79
+ `set_fan_enable` on reg 10 / 16) remain for a true fan-class actuator if
80
+ one is wired at `0x04` on another rig.
81
+
82
+ ## Common Header (all devices, regs 0–9)
83
+
84
+ | Reg | Meaning | Notes |
85
+ | --- | ------------------------------- | ------------------------------------------------------------------------------------ |
86
+ | 0 | Self-reported Modbus address | matches polling addr |
87
+ | 1 | `0xAA` magic byte << 8 \| addr | e.g. `0xAA0A` for `0x0A` |
88
+ | 2–3 | Firmware version | ASCII `"HH.LL"` on SF-made units, binary `0xF784xxxx` on OEM‑branded LED/fan drivers |
89
+ | 4 | Model / product code | |
90
+ | 5 | Serial number fragment | |
91
+ | 6 | Device type (hi) : subtype (lo) | |
92
+ | 7 | Hardware version | |
93
+ | 8–9 | Reserved | always `0x0000` |
94
+
95
+ ---
96
+
97
+ ## Sensors
98
+
99
+ ### `0x03` — CO₂ sensor *(13 registers)*
100
+
101
+ Example FC03 read (`qty` ≥ 13; values illustrative):
102
+
103
+ | Reg | Hex | Decimal | Interpretation |
104
+ | ------ | -------- | ------- | ---------------------------------- |
105
+ | 0 | `0x0003` | 3 | self-addr |
106
+ | 1 | `0xAA03` | 43523 | magic + addr |
107
+ | 2 | `0x3035` | — | FW hi = `"05"` |
108
+ | 3 | `0x3132` | — | FW lo = `"12"` |
109
+ | 4 | `0x0330` | 816 | model |
110
+ | 5 | `0x7518` | 29976 | serial frag |
111
+ | 6 | `0x0308` | 776 | type / subtype (sensor-class, CO₂) |
112
+ | 7 | `0x0202` | 514 | hw |
113
+ | 8–9 | `0x0000` | 0 | reserved |
114
+ | **10** | `0x01B5` | **437** | **CO₂ concentration [ppm]** |
115
+ | 11–12 | `0x0000` | 0 | reserved (outside the 13-reg spec) |
116
+
117
+ The scan uses `DEFAULT_QTY[0x03]=13`, the response parses to `CO2SensorData`, and `co2_ppm = reg[10]`. Observed range across recent runs: **430–466 ppm**, consistent with an occupied indoor room. The value is confirmed real sensor data (not stale, not from another device).
118
+
119
+ ### `0x0A` — Sensor hub (air T / RH / soil T / PPFD) + Light 1 enable *(28 registers)*
120
+
121
+ Full 28-register reads work reliably since `RS485Transport` switched
122
+ to deterministic, fixed-size frame reads (`spiderwire.transport`);
123
+ the old silence-timing heuristic truncated long frames. The
124
+ authoritative snapshot below comes from
125
+ `docs/capture-20260418-1135.sal` (Saleae, Session 9), where the OEM
126
+ GSS hub pulls the full 61-byte response with CRC OK.
127
+
128
+ | Reg | Hex | Decimal | Interpretation |
129
+ | ------ | ------------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
130
+ | 0–9 | — | — | header (self-addr `0x000A`, magic `0xAA0A`, FW `"05.12"`, model `0x0330`, type `0x0400`, hw `0x0202`) |
131
+ | **10** | `0x00F5` | **245** | **Air temperature × 10 → 24.5 °C** |
132
+ | **11** | `0x01B0` | **432** | **Air humidity × 10 → 43.2 %RH** |
133
+ | **12** | `0xFC18` | −1000 (signed) | **Soil temperature × 10** — `-1000` means probe not connected (`soil_temp_c = None`) |
134
+ | **13** | `0x0146` | **326** | **PPFD in µmol/m²/s** (matches in-app display 325–326) |
135
+ | 14 | `0x0163` | 355 | co-tracking light channel — peak / IR / DLI TBD |
136
+ | 15 | `0x23F8` | 9208 | constant — calibration / device config |
137
+ | 16–17 | `0x0000` | 0 | reserved |
138
+ | **18** | `0x0001` | 1 | **Light 1 enable flag** — written by master via **blind** FC 0x06 (the hub never echoes the write — `gss-ctrl light` and HA both timed out waiting for an echo before we switched to blind writes; reads of reg 18 still confirm the new value on the next poll) |
139
+ | **19** | `0x0064` | 100 | **Light 1 *reported* value** (read-only status echo of the dimmer at `0x04`; writing it does not change brightness — confirmed absent from all FC06 traffic in `capture-20260418-1152.sal`) |
140
+ | 20–21 | `0x0023`, `0x000A` | 35, 10 | zone / group (reg 21 matches device addr = 10) |
141
+ | 22–27 | `0x0000` | 0 | reserved / status flags |
142
+
143
+ `BusMaster.set_light_enable(addr=0x0A, enable)` writes reg 18. VPD is
144
+ computed client-side from reg 10 + reg 11 via the Tetens SVP formula in
145
+ `SensorHubData.vpd_kpa`. PPFD from reg 13 is exposed as a sensor entity
146
+ with translation key `ppfd` on the hub device in Home Assistant.
147
+
148
+ ---
149
+
150
+ ## Actuators
151
+
152
+ ### `0x04` — Light dimmer *(24 registers)*
153
+
154
+ Header type `0x0301` reads as "fan subtype" via `registers.py`, but the actual device wired at `0x04` in the current rig is the **main grow-light dimmer** — confirmed in `docs/capture-20260418-1152.sal`: every time the user moved the OEM app's brightness slider (0 → 53 → 30 → 53 → 0 %), the hub emitted FC06 writes to `0x04 reg 10` carrying the percent value directly. Whether this is a re-flashed fan MCU or the OEM assigning the fan product code to their light driver is unresolved; treat `reg 10` here as a 0-100 % brightness setpoint.
155
+
156
+ | Reg | Interpretation | Observed |
157
+ | ------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
158
+ | 0–9 | header | type `0x0301`, model `0x0426`, FW `0xF784C96D`, hw `0x0105` |
159
+ | **10** | **Brightness 0-100 %** (direct percent) | written by master via FC 0x06; OEM writes it blind (see below) |
160
+ | 11–15 | reserved | `0` |
161
+ | **16** | **Enable flag** — must be `1` for reg 10 writes to take effect | Throughout `capture-20260418-1152.sal` the FC03 responses show `reg[16] = 1`; the OEM sets it during an earlier session and the value sticks. If you start a fresh master without setting this bit, reg 10 writes land on the dimmer but produce no visible change. `gss-ctrl light` (and the HA light entity) writes `reg 16 ← 1` blind alongside the brightness. |
162
+ | 17–23 | reserved | `0` |
163
+
164
+ **No Modbus echo on FC06 writes.** The device acts on the command but never sends the 8-byte echo back — `gss-ctrl light` and the HA integration use `write_register(..., wait_for_response=False)` for this address so they don't stall 300 ms per write. FC03 reads also time out most of the time in the current rig.
165
+
166
+ The legacy fan helpers — `BusMaster.set_fan_speed(addr, 0..25)` / `set_fan_enable(addr, bool)` — still target `reg 10` / `reg 16`. If you genuinely have a duct fan wired here (different rig), speed is 0-25 and speeds below ~11 stall the motor; see `protocol-analysis.md` §"Fan Speed Control".
167
+
168
+ ### `0x06` — Blower / ventilation *(16 registers)*
169
+
170
+ The OEM app labels this "Light 2" but the 0x06 SKU is physically wired
171
+ to the blower / ventilation fan. Confirmed in
172
+ `docs/capture-20260418-1452.sal`: the user drove the OEM slider
173
+ **0 → 74 → 60 → 25 → OFF** and every change produced an FC06 write to
174
+ `0x06 reg 14` carrying the percent directly; reg 10 stays `0` the entire
175
+ capture. The device's own 16-register broadcast echoes the live
176
+ setpoint back in reg 14 and latches reg 12 to 1 whenever the blower is
177
+ running.
178
+
179
+ | Reg | Interpretation | Notes |
180
+ | ------ | -------------------- | ----------------------------------------------------------- |
181
+ | 0–9 | header | type `0x0204`, model `0x043D`, FW `0xF784C90F`, hw `0x0102` |
182
+ | 10 | unused | `0x0000` throughout every capture |
183
+ | 11 | paired flag | `0x0001` once the controller has linked, latches |
184
+ | **12** | **running flag** | `1` whenever the blower is actively driven, `0` when off |
185
+ | 13 | reserved | `0x0000` |
186
+ | **14** | **Blower % (0-100)** | master writes via FC 0x06; slave echoes |
187
+ | 15 | reserved | `0x0000` |
188
+
189
+ `BusMaster.set_blower(addr, percent)` issues `write_register` on reg 14
190
+ (`BLOWER_SETPOINT_REG` in `registers.py`) and waits for the standard FC06
191
+ response echo. The OEM UI enforces a 25 % floor above 0 (it refuses to
192
+ slide lower without going to OFF), but the device itself accepts any
193
+ 0–100 value — `set_blower` and the HA blower entity allow lower values
194
+ if desired.
195
+
196
+ ---
197
+
198
+ ## Addresses polled but silent
199
+
200
+ The OEM master polls `0x01, 0x02, 0x05, 0x07, 0x08, 0x0B–0x10, 0x13` every poll cycle. None of these respond in the current rig. The per-address register-count table in `registers.py` (`DEFAULT_QTY`) encodes the OEM's expected device class at each slot; nothing is physically installed there today.
201
+
202
+ > During `gss-ctrl poll`, the top-of-screen "N online" counter sometimes shows phantom devices at those addresses with data identical to `0x03`'s. Those are garbled frames that happened to pass CRC — not real devices. Only the clean `scan` output above should be trusted for device discovery.
203
+
204
+ ## Broadcast heartbeat (master → 0x00)
205
+
206
+ FC `0x10` @ register 1001, 26 words. `BusMaster.setpoints` holds the
207
+ live payload; `broadcast_setpoints()` ships it, and `tick()` fires one
208
+ every `heartbeat_interval` seconds (default 3.5 s, matching the OEM
209
+ hub). Peripherals enter a master-missing fail-safe if the heartbeat
210
+ stops. See `protocol-analysis.md` §"Broadcast Writes" for the
211
+ per-register layout; default non-zero values are `reg[1009]=7` and
212
+ `reg[1011]=1112` (both observed constant in every capture).