atrea_control 3.2.1 → 4.0.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +11 -0
- data/.rubocop_todo.yml +7 -1
- data/.ruby-version +1 -1
- data/AGENTS.md +51 -0
- data/CHANGELOG.md +63 -1
- data/Gemfile +4 -4
- data/Gemfile.lock +49 -47
- data/README.md +215 -62
- data/atrea_control.gemspec +4 -3
- data/docs/rd5-cloud-protocol.md +253 -0
- data/docs/superpowers/plans/2026-10-24-rd5-reliability.md +238 -0
- data/lib/atrea_control/duplex/login.rb +94 -109
- data/lib/atrea_control/duplex/login_discovery.rb +82 -0
- data/lib/atrea_control/duplex/login_session.rb +53 -0
- data/lib/atrea_control/duplex/rd5_command.rb +72 -0
- data/lib/atrea_control/duplex/rd5_configuration.rb +104 -0
- data/lib/atrea_control/duplex/request.rb +34 -6
- data/lib/atrea_control/duplex/unit.rb +58 -52
- data/lib/atrea_control/duplex/unit_commands.rb +59 -0
- data/lib/atrea_control/duplex/user_ctrl.rb +75 -64
- data/lib/atrea_control/duplex/write_verification.rb +75 -0
- data/lib/atrea_control/duplex.rb +2 -0
- data/lib/atrea_control/logger.rb +3 -1
- data/lib/atrea_control/sensor_parser.rb +94 -30
- data/lib/atrea_control/version.rb +2 -1
- data/lib/atrea_control.rb +7 -0
- metadata +31 -8
data/README.md
CHANGED
|
@@ -1,89 +1,242 @@
|
|
|
1
1
|
# atrea_control
|
|
2
|
-
Ventilation systems by https://www.atrea.eu are build with web UI portal - but this portal did not provide any API interface...
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
Ruby client for ATREA DUPLEX ventilation units with **RD5 regulation**, using
|
|
4
|
+
the [RD5 cloud portal](https://control.atrea.eu/). RD5-only support is intentional:
|
|
5
|
+
this unofficial gem uses cloud HTTP/XML, not aMotion Cloud or Modbus TCP.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* fan power
|
|
12
|
-
* power mode
|
|
13
|
-
* allow change
|
|
14
|
-
* power
|
|
15
|
-
* mode
|
|
7
|
+
**Login and end-to-end reads were verified against a live RD5 unit on 2026-10-25:**
|
|
8
|
+
configuration discovery, fresh snapshots, getters/JSON and repeated refreshes
|
|
9
|
+
succeeded. A missing `Sync=1` parameter in the unit-login request was corrected
|
|
10
|
+
after comparison with the current portal. Live writes and requested-value
|
|
11
|
+
read-back semantics remain unverified. See [protocol evidence and limitations](docs/rd5-cloud-protocol.md).
|
|
16
12
|
|
|
17
13
|
## Installation
|
|
18
14
|
|
|
19
|
-
Add this line to your application's Gemfile:
|
|
20
|
-
|
|
21
15
|
```ruby
|
|
22
16
|
gem "atrea_control"
|
|
23
17
|
```
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
bundle install
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Or
|
|
31
|
-
```bash
|
|
32
|
-
bundle add atrea_control
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Or install it yourself as:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
gem install atrea_control
|
|
39
|
-
```
|
|
19
|
+
Run `bundle install`, or install directly with `gem install atrea_control`.
|
|
40
20
|
|
|
41
|
-
##
|
|
21
|
+
## Login and snapshots
|
|
42
22
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
* `user_id` is atrea internal ID of user account
|
|
46
|
-
* `unit_id` is atrea `ident` - identification number of airflow unit (ventilation system) - 10 digits
|
|
47
|
-
* `sid` is session ID - auth token, valid for logged session. Its validity is unknown
|
|
23
|
+
Supply credentials through your application's environment. The library does not
|
|
24
|
+
load `.env` files automatically. Never log credentials or returned tokens.
|
|
48
25
|
|
|
49
26
|
```ruby
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
require "atrea_control"
|
|
28
|
+
|
|
29
|
+
tokens = AtreaControl::Duplex::Login.user_tokens(
|
|
30
|
+
login: ENV.fetch("ATREA_USERNAME"),
|
|
31
|
+
password: ENV.fetch("ATREA_PASSWORD"),
|
|
32
|
+
unit_id: ENV.fetch("ATREA_UNIT_IDENT"),
|
|
33
|
+
timeout: 300,
|
|
34
|
+
poll_interval: 1,
|
|
35
|
+
open_timeout: 10,
|
|
36
|
+
read_timeout: 60
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
control = AtreaControl::Duplex::Unit.new(**tokens, time_zone: "Europe/Prague")
|
|
40
|
+
values = control.values
|
|
41
|
+
values[:current_power] # Numeric configured power value, not universally percent.
|
|
42
|
+
values[:current_mode] # Translated label; may describe a user input such as CO2.
|
|
43
|
+
control.refresh! # Explicitly fetch and validate a new snapshot.
|
|
52
44
|
```
|
|
53
|
-
I recommend to store then somewhere...
|
|
54
|
-
Then you can call Unit for data...
|
|
55
45
|
|
|
56
|
-
|
|
46
|
+
`Login.new(...).call` accepts the same keywords. `unit_id:` selects the unit's
|
|
47
|
+
`ident`, **not** its portal row ID. Omit it to select the first listed unit.
|
|
48
|
+
Returned tokens contain `user_id`, `unit_id` and secret `sid`.
|
|
49
|
+
|
|
50
|
+
Account authentication and unit-session acquisition are separate stages. Defaults
|
|
51
|
+
are `timeout: 300`, `poll_interval: 1`, `open_timeout: 10`, `read_timeout: 60`, in
|
|
52
|
+
seconds. The entire login, including authentication, discovery and polling, must
|
|
53
|
+
finish within five minutes; shorter `timeout:` values are allowed, larger ones
|
|
54
|
+
are rejected. Each HTTP operation has a separate 60-second total limit covering
|
|
55
|
+
connection, writing and reading, also bounded by the outer login limit. Both use
|
|
56
|
+
Faraday's Net::HTTP adapter and standard-library `Timeout`, without a custom
|
|
57
|
+
transport. Server ETA is advisory, not a loop count. Timeout does not establish
|
|
58
|
+
that credentials were rejected or the unit is offline.
|
|
59
|
+
|
|
60
|
+
Ordinary unit requests also have a 60-second total limit, not just a per-read
|
|
61
|
+
timeout. `Duplex::Request.new(..., timeout: ...)` accepts shorter total limits;
|
|
62
|
+
`open_timeout:` and `read_timeout:` can independently shorten connection/read
|
|
63
|
+
waits. The adapter never automatically retries requests.
|
|
64
|
+
|
|
65
|
+
### Snapshot lifetime and temperature semantics
|
|
66
|
+
|
|
67
|
+
* First reading fetches one complete snapshot; later getters reuse it without
|
|
68
|
+
implicit network refresh. `refresh!` explicitly replaces it.
|
|
69
|
+
* Every current-value getter and serializer checks freshness: timestamp must be
|
|
70
|
+
newer than `now - 900` seconds and no later than `now + 60` seconds. Exactly
|
|
71
|
+
15 minutes old is expired. `valid?` inspects cached state without fetching.
|
|
72
|
+
* Failed refresh invalidates current data and preserves the failure until another
|
|
73
|
+
`refresh!`. A write attempt also discards the pre-write snapshot, even when its
|
|
74
|
+
response fails; next reading fetches again. No stale fallback is exposed.
|
|
75
|
+
* `values`/`as_json` use symbol keys; `parsed` uses string keys. Readers include
|
|
76
|
+
`power`/`current_power`, `mode`/`current_mode`, `outdoor_temperature`,
|
|
77
|
+
`required_temperature`, `preheat_temperature`, `input_temperature`, `preheating`
|
|
78
|
+
/ `preheating?`, and `timestamp`. Temperatures are degrees Celsius.
|
|
79
|
+
* `required_temperature` is the requested temperature. Historical
|
|
80
|
+
`preheat_temperature` reader and hash key remain aliases in meaning; both keys
|
|
81
|
+
are present. This is **not** a measured preheater temperature.
|
|
82
|
+
* Optional `input_temperature` is `nil` when unavailable or its discovered logic
|
|
83
|
+
is unsupported. It is not fabricated as zero.
|
|
84
|
+
* Legacy `preheating` is an aggregate of heating-related signals, not a verified
|
|
85
|
+
dedicated preheater state.
|
|
86
|
+
* Optional `time_zone:` accepts an IANA identifier (also on `SensorParser.new`).
|
|
87
|
+
`nil` preserves process-local timestamp interpretation. Set the actual unit's
|
|
88
|
+
zone explicitly to remove deployment-zone ambiguity. Invalid zones and
|
|
89
|
+
ambiguous/nonexistent DST-local timestamps raise `ProtocolError`.
|
|
90
|
+
* Device timestamps may omit leading zeroes (for example, `17:7:32`). These are
|
|
91
|
+
normalized while invalid calendar dates and times are still rejected.
|
|
92
|
+
|
|
93
|
+
Loading the gem registers translations and sets global `I18n.default_locale` to
|
|
94
|
+
`:cs`. Sensor/mode maps use string keys. `user_modes` keys are numeric strings,
|
|
95
|
+
not input names such as `"D1"`.
|
|
96
|
+
|
|
97
|
+
### Configuration discovery
|
|
98
|
+
|
|
99
|
+
`Unit.new` accepts `user_ctrl:` to reuse a `Duplex::UserCtrl` for the **same unit**.
|
|
100
|
+
It caches successfully validated layout/text configuration per instance; recreate
|
|
101
|
+
it after configuration changes. `UserCtrl.data` exports JSON-safe configuration,
|
|
102
|
+
including `input_temperature_source`: `"rd5_standard"` selects the recognized
|
|
103
|
+
device source-selection rule; `"unsupported"` keeps inlet temperature unavailable.
|
|
104
|
+
Persist this field alongside the sensor/mode maps and expose it when reconstructing
|
|
105
|
+
a custom `user_ctrl:` object, such as an ActiveRecord model. Without it, only the
|
|
106
|
+
legacy direct sensor mapping can be used. Missing/nil metadata retains that legacy
|
|
107
|
+
behavior; explicit unknown metadata returns `nil`, without guessing a sensor.
|
|
108
|
+
|
|
109
|
+
Custom objects supply `name`, `sensors`, `modes`, `user_modes` and the persisted
|
|
110
|
+
`input_temperature_source`. Sensor interpretation stays inside the gem, not the
|
|
111
|
+
application UI. Command mappings must include discovered `power_input`, `power_switch`,
|
|
112
|
+
`mode_input` and `mode_switch` fields. Derive mappings from the unit's `UserCtrl`,
|
|
113
|
+
not fixture addresses. `control.configuration` returns the fresh snapshot's
|
|
114
|
+
`Duplex::RD5Configuration` policy (`[]`, `fetch`, `to_h`).
|
|
115
|
+
|
|
116
|
+
## Power and mode commands
|
|
117
|
+
|
|
118
|
+
Setter values must be nonnegative integers, finite integral Floats, or ASCII
|
|
119
|
+
decimal-digit strings: `88`, `88.0`, `"88"` normalize to 88. `nil`, booleans,
|
|
120
|
+
negative/fractional numbers, NaN/infinity, whitespace, signs and `"88.0"` raise
|
|
121
|
+
`ArgumentError`. Values are never silently truncated or clamped. Commands also
|
|
122
|
+
require a supported configuration and discovered register mapping.
|
|
123
|
+
|
|
124
|
+
Power semantics follow RD5 `H10510` in the vendor's
|
|
125
|
+
[Communication MODBUS — RD5 and aMotion, version 04](https://atrea.eu/_files/documents/modbus-comm-amot-rd5-cz-v04_834.pdf),
|
|
126
|
+
section 3.1, page 9:
|
|
127
|
+
|
|
128
|
+
| `H10510` | Configuration | Accepted power values |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| 0 | Direct speed | 0 or integers 12–100 |
|
|
131
|
+
| 1 | Constant flow | Integers 0–100 (configured flow scale) |
|
|
132
|
+
| 2 | Constant pressure | 0, 1, 2 (levels) |
|
|
133
|
+
| 3 | External input | No power writes; `UnsupportedConfiguration` |
|
|
134
|
+
| 4 | Multiport | 0, 10–12, 20–22, 30–38 (enumerated settings) |
|
|
135
|
+
| Missing/unknown | Unsupported | `UnsupportedConfiguration` |
|
|
136
|
+
|
|
137
|
+
These are not universal fan-speed percentages or measured airflow. Modes must be
|
|
138
|
+
discovered numeric IDs 0–7 and satisfy equipment prerequisites: 0/2 unrestricted;
|
|
139
|
+
1/6/7 require `C10508=0`; 3 additionally requires `C10509=1`; 4 additionally
|
|
140
|
+
requires `H11700=1`; 5 requires `I11401=1`. Missing prerequisites fail explicitly.
|
|
141
|
+
Translated current-mode labels are not command values.
|
|
142
|
+
|
|
143
|
+
Schedule policy uses the discovered control register: manual 0 sends only the
|
|
144
|
+
setpoint; scheduled 1 or temporary 2 sends the setpoint plus temporary control 2,
|
|
145
|
+
including for power zero. It does not permanently disable schedules. A temporary
|
|
146
|
+
request lasts until the next scheduled interval under the documented RD5 policy.
|
|
147
|
+
|
|
148
|
+
### Unconfirmed writes
|
|
149
|
+
|
|
57
150
|
```ruby
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
control.power
|
|
151
|
+
response = control.set_power(88, verify: false)
|
|
152
|
+
response = control.set_mode(2, verify: false)
|
|
153
|
+
control.power = 88
|
|
154
|
+
control.mode = 2
|
|
61
155
|
```
|
|
62
156
|
|
|
63
|
-
|
|
64
|
-
`
|
|
157
|
+
`verify: false` returns the HTTP response, without application/read-back
|
|
158
|
+
confirmation. `power=` and `mode=` delegate with `verify: false`. Ruby assignment
|
|
159
|
+
expressions evaluate to their right-hand value, **not** confirmation. HTTP success
|
|
160
|
+
alone does not establish that the unit accepted or applied the request. Writes
|
|
161
|
+
are never automatically retried.
|
|
65
162
|
|
|
66
|
-
|
|
67
|
-
`sensors` (Hash) = Map of sensors, for example: `{ outdoor_temperature: "HI10208", current_power: "H10704" }`
|
|
68
|
-
`modes` (Hash) = Is a map of "changable" modes - in unit its something like "builtin?" modes. They are translated by unit lang - `{ "0" => "Vypnuto", "1" => "Automat" }`
|
|
69
|
-
`user_modes` (Hash) = Is a map user specific modes, based on home switches / devices (D1, D2, D3, IN1, IN2 ...). They are translated by user texts - `{ "D1" => "Koupelna", "D2" => "CO2", "IN1" => "ovladač" }`
|
|
163
|
+
### Verified writes require an independently proven mapping
|
|
70
164
|
|
|
71
|
-
|
|
165
|
+
`set_power(value, verify: true, timeout: 30, poll_interval: 5)` and `set_mode`
|
|
166
|
+
default to verification. **The default configuration has no proven requested-value
|
|
167
|
+
read-back profile:** verified calls raise `UnsupportedConfiguration` before writing.
|
|
168
|
+
Neither the PDF's current-value fields nor old fixture `idw` fields are selected
|
|
169
|
+
automatically.
|
|
72
170
|
|
|
73
|
-
|
|
171
|
+
Only supply `verification_registers:` after independently verifying requested-value
|
|
172
|
+
semantics for that exact unit/profile. It accepts a nonempty Hash with symbol keys
|
|
173
|
+
`:power` and/or `:mode`, and String values matching `H` plus exactly five digits.
|
|
174
|
+
Syntax validation does not establish register semantics. This example deliberately
|
|
175
|
+
supplies no literal register address:
|
|
74
176
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
177
|
+
```ruby
|
|
178
|
+
# These environment values must come from your independently verified profile.
|
|
179
|
+
verified_control = AtreaControl::Duplex::Unit.new(
|
|
180
|
+
**tokens,
|
|
181
|
+
time_zone: "Europe/Prague",
|
|
182
|
+
verification_registers: {
|
|
183
|
+
power: ENV.fetch("ATREA_VERIFIED_POWER_REQUEST_REGISTER"),
|
|
184
|
+
mode: ENV.fetch("ATREA_VERIFIED_MODE_REQUEST_REGISTER")
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
confirmed_power = verified_control.set_power(88, timeout: 30, poll_interval: 5)
|
|
188
|
+
confirmed_mode = verified_control.set_mode(2, timeout: 30, poll_interval: 5)
|
|
189
|
+
```
|
|
80
190
|
|
|
81
|
-
|
|
191
|
+
Verification fetches a fresh baseline, writes once, then polls for the normalized
|
|
192
|
+
request in a snapshot **newer than the baseline**. One absolute budget includes
|
|
193
|
+
baseline, lazy discovery, dispatch, read-back and sleeps. It confirms a requested
|
|
194
|
+
setpoint, not physical airflow, effective mode or override of safety functions.
|
|
195
|
+
`WriteNotConfirmed` means outcome unknown after dispatch; do not blindly retry.
|
|
196
|
+
|
|
197
|
+
## Errors and migration
|
|
198
|
+
|
|
199
|
+
All library errors inherit `AtreaControl::Error`; invalid caller command inputs
|
|
200
|
+
raise standard `ArgumentError`.
|
|
201
|
+
|
|
202
|
+
| Error | Meaning |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `AuthenticationError` | Portal rejected authentication. |
|
|
205
|
+
| `SessionExpired` | Unit HTTP 401/403, or missing/stale/future snapshot timestamp. Freshness failure alone does not prove authentication expired. |
|
|
206
|
+
| `ProtocolError` | Malformed/unexpected XML, invalid register/timestamp or DST ambiguity. |
|
|
207
|
+
| `TransportError` | Failed HTTP response or connection. |
|
|
208
|
+
| `UnitConnectionTimeout` | Transport timeout or exhausted login/verification-baseline budget; subclass of `TransportError`. |
|
|
209
|
+
| `UnsupportedConfiguration` | Missing/unsupported configuration, command profile or proven verification mapping. |
|
|
210
|
+
| `WriteNotConfirmed` | Verification budget exhausted after dispatch; outcome unknown. |
|
|
211
|
+
|
|
212
|
+
Migration from 3.2.x: `nil` formerly could become zero; it now raises
|
|
213
|
+
`ArgumentError`. Other malformed inputs no longer silently coerce. All cached
|
|
214
|
+
getters/serializers can now raise freshness or refresh errors. Handle optional
|
|
215
|
+
inlet temperature `nil`, the additional `required_temperature` hash key, strict
|
|
216
|
+
configuration/mode validation, and preserved manual schedule control. Explicit
|
|
217
|
+
`set_power`/`set_mode` default to verification; existing assignment setters remain
|
|
218
|
+
unconfirmed. These changes require a **major-version upgrade** from 3.2.x.
|
|
219
|
+
|
|
220
|
+
## Protocol reference and development
|
|
221
|
+
|
|
222
|
+
See [RD5 cloud protocol evidence](docs/rd5-cloud-protocol.md) for supported
|
|
223
|
+
declarative discovery forms, fixture/PDF differences and current integration blocker.
|
|
224
|
+
The PDF describes **Modbus TCP**, not cloud framing, acknowledgements or rate
|
|
225
|
+
limits. Port 502, enabling Modbus and its five-second batch interval are not
|
|
226
|
+
prerequisites or established HTTP constraints for this gem.
|
|
227
|
+
|
|
228
|
+
Run `bin/setup` to install dependencies. `bundle exec rake` runs specs then
|
|
229
|
+
RuboCop. CI retains the Ruby spec matrix and runs a separate RuboCop job. Focused
|
|
230
|
+
test: `bundle exec rspec spec/atrea_control/duplex/unit_spec.rb`, optionally with
|
|
231
|
+
`:LINE`. WebMock/local fixtures need no portal credentials or physical unit.
|
|
232
|
+
Use `bin/console` for an interactive prompt.
|
|
233
|
+
|
|
234
|
+
Gem contents come from `git ls-files`; new runtime files must be tracked before
|
|
235
|
+
packaging. `bundle exec rake build` builds locally. Publishing requires a separate
|
|
236
|
+
release decision: `bundle exec rake release` pushes commits/tags and publishes to
|
|
237
|
+
RubyGems; it is not a verification command.
|
|
82
238
|
|
|
83
239
|
## Contributing
|
|
84
240
|
|
|
85
|
-
Bug reports and pull requests are welcome
|
|
86
|
-
|
|
87
|
-
## Code of Conduct
|
|
88
|
-
|
|
89
|
-
Everyone interacting in the AtreaControl project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/luk4s/atrea_control/blob/master/CODE_OF_CONDUCT.md).
|
|
241
|
+
Bug reports and pull requests are welcome at https://github.com/luk4s/atrea_control.
|
|
242
|
+
Participants must follow the [code of conduct](CODE_OF_CONDUCT.md).
|
data/atrea_control.gemspec
CHANGED
|
@@ -35,8 +35,9 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
# For more information and examples about making a new gem, checkout our
|
|
36
36
|
# guide at: https://bundler.io/guides/creating_gem.html
|
|
37
37
|
|
|
38
|
-
spec.add_dependency "faraday", "~> 2.
|
|
38
|
+
spec.add_dependency "faraday", "~> 2.14"
|
|
39
39
|
spec.add_dependency "faraday-cookie_jar", "~> 0.0"
|
|
40
|
-
spec.add_dependency "i18n", "~> 1.
|
|
41
|
-
spec.add_dependency "nokogiri", "~> 1.
|
|
40
|
+
spec.add_dependency "i18n", "~> 1.15"
|
|
41
|
+
spec.add_dependency "nokogiri", "~> 1.16"
|
|
42
|
+
spec.add_dependency "tzinfo", "~> 2.0"
|
|
42
43
|
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# RD5 cloud protocol: evidence and supported profiles
|
|
2
|
+
|
|
3
|
+
This client intentionally targets RD5 cloud HTTP/XML only. It is neither aMotion
|
|
4
|
+
Cloud nor a Modbus TCP implementation. Local fixture coverage establishes parser
|
|
5
|
+
and policy behavior, not current firmware compatibility.
|
|
6
|
+
|
|
7
|
+
## Evidence ledger
|
|
8
|
+
|
|
9
|
+
1. **Older portal fixtures:** `spec/fixtures/files/userCtrl.xml` and `unit.xml`,
|
|
10
|
+
plus login response fixtures. These support declarative layout discovery and
|
|
11
|
+
register parsing. The unit fixture timestamp is historical, not a live sample.
|
|
12
|
+
2. **Vendor PDF:** *Communication MODBUS — RD5 and aMotion*, version 04, cover
|
|
13
|
+
issue `2025_04_25`, sections 2.3.1 and 3.1 (especially pages 9–13),
|
|
14
|
+
[public source](https://atrea.eu/_files/documents/modbus-comm-amot-rd5-cz-v04_834.pdf).
|
|
15
|
+
The contents page has a different issue date; use version and cover issue as
|
|
16
|
+
identity. It establishes RD5 register semantics, not cloud framing, login,
|
|
17
|
+
acknowledgements or HTTP rate limits. Modbus's five-second batch interval is
|
|
18
|
+
not evidence of a cloud HTTP limit.
|
|
19
|
+
3. **Initial read-only audits:** account authentication and unit-record discovery
|
|
20
|
+
succeeded. Advertised package
|
|
21
|
+
`3001022`, normalized with leading zeroes, matches `CONTROL_VERSION`
|
|
22
|
+
`003001022`. SID remained zero. The full 360-second budget ended exactly with
|
|
23
|
+
`AtreaControl::UnitConnectionTimeout`, `stage=session_query`.
|
|
24
|
+
|
|
25
|
+
4. **Successful live audit on 2026-10-25, after production client shutdown:** the
|
|
26
|
+
unmodified handshake still timed out. Unit metadata reported a server contact
|
|
27
|
+
about 101 seconds earlier, so zero SID alone did not establish disconnection.
|
|
28
|
+
Inspection of current portal source identified a missing `Sync=1` on the
|
|
29
|
+
unit-login POST. Adding only this parameter obtained a SID in about 127 seconds
|
|
30
|
+
and allowed valid reads. After applying the fix, a separate public-API run
|
|
31
|
+
logged in in 5.86 seconds, read values/configuration, serialized getters/JSON,
|
|
32
|
+
and completed three refreshes with advancing timestamps and data roughly five
|
|
33
|
+
seconds old. Another run checked the unit-name reader, constructed power/mode
|
|
34
|
+
commands from the actual configuration without sending them, rejected an aged
|
|
35
|
+
live snapshot under a process-local controlled clock, and recovered via refresh.
|
|
36
|
+
The rebuilt `pkg/atrea_control-3.2.1.gem` was then unpacked outside the workspace
|
|
37
|
+
and used for another real login (5.9 seconds), valid read and successful refresh;
|
|
38
|
+
this checked the packaged runtime, not only the working-tree source.
|
|
39
|
+
5. **Faraday-only follow-up:** after removing the extra transport, live login
|
|
40
|
+
completed in 1.45 seconds. Both Login and Request used `Faraday::Adapter::NetHttp`;
|
|
41
|
+
HTTPX was not loaded. Values and refresh passed, with refreshed data roughly
|
|
42
|
+
five seconds old. This audit also exposed unpadded timestamp components
|
|
43
|
+
(`2026-10-25 17:7:32 `); the parser now accepts those valid fields after
|
|
44
|
+
normalization. No control writes were dispatched.
|
|
45
|
+
|
|
46
|
+
**Remaining integration boundary:** current login, configuration discovery and
|
|
47
|
+
reads are proven for this unit/package. Cloud acknowledgement shape, actual
|
|
48
|
+
writes and requested-value read-back semantics are not. No live control writes
|
|
49
|
+
were run; those need separate agreement. No credentials, account names, unit
|
|
50
|
+
identifiers or SID values are retained here.
|
|
51
|
+
|
|
52
|
+
## Cloud sequence implemented
|
|
53
|
+
|
|
54
|
+
`Login` posts credentials to the portal (`?action=login`), accepts its expected
|
|
55
|
+
302, requires the session cookie, obtains a client ID from `core/core.php`, lists
|
|
56
|
+
`userUnits`, and resolves the `units` record to its `ident`. Explicit `unit_id:`
|
|
57
|
+
matches that ident through the account list; otherwise the first unit is selected.
|
|
58
|
+
|
|
59
|
+
`apps/rd5Control/handle.php` with `action=unitQuery`, `query=loged` returns a
|
|
60
|
+
`/root/login` SID. A zero SID triggers `Sync=1&action=unitLogin` with the unit's
|
|
61
|
+
account row reference and `comm=config/login.cgi?magic=`, requiring `/root/sended`, then
|
|
62
|
+
polling the session query. Server wait estimates do not shorten or extend the
|
|
63
|
+
absolute budget. A nonempty, nonzero SID is required for success. No browser
|
|
64
|
+
application-launch request is needed in the verified library sequence.
|
|
65
|
+
|
|
66
|
+
The current portal's `core/start.js` shared `sendSync` implementation prepends
|
|
67
|
+
`Sync=1` to its requests. The RD5 app's `setUnit` function, returned by
|
|
68
|
+
`core/core.php?action=run&object=app`, invokes that synchronous helper for
|
|
69
|
+
`unitLogin`. This source comparison, followed by live success, supports the
|
|
70
|
+
parameter fix without executing downloaded application code in the gem. A
|
|
71
|
+
`/root/sended` response alone had not proved successful session establishment.
|
|
72
|
+
|
|
73
|
+
`Request` sends unit requests through `comm/sw/unit.php` with authentication and
|
|
74
|
+
version parameters. These parameters are secret-bearing: do not retain full URLs.
|
|
75
|
+
Targets include:
|
|
76
|
+
|
|
77
|
+
| Target | Purpose |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `lang/userCtrl.xml`, `_async=1` | Layout and declarative discovery |
|
|
80
|
+
| `config/texts.xml` | User-defined labels |
|
|
81
|
+
| `config/xml.xml` | Register snapshot |
|
|
82
|
+
| `config/xml.cgi`, `_w=1` | Command request |
|
|
83
|
+
|
|
84
|
+
No cloud application ACK shape has been proven. A successful HTTP response is
|
|
85
|
+
transport success only, including on an unconfirmed write. The client never
|
|
86
|
+
automatically retries a write whose outcome may already be unknown.
|
|
87
|
+
|
|
88
|
+
Login's default and maximum total limit is 300 seconds; polling is one second.
|
|
89
|
+
Every HTTP operation has a separate 60-second total limit, including connection,
|
|
90
|
+
write and response read. The outer login timer can stop a request earlier.
|
|
91
|
+
Default connect/read waits are 10/60 seconds; shorter overrides are supported.
|
|
92
|
+
Faraday's stock Net::HTTP adapter is the sole transport and disables retries.
|
|
93
|
+
Standard-library `Timeout.timeout` wrappers use explicit `StandardError`-derived
|
|
94
|
+
exception classes to preserve Net::HTTP's TLS-connect cleanup. No custom socket,
|
|
95
|
+
resolver or transport plugin is needed. Verification keeps its own aggregate
|
|
96
|
+
30-second default limit, including lazy configuration discovery and read-back.
|
|
97
|
+
The earlier 360-second audit timings above predate this simplification.
|
|
98
|
+
|
|
99
|
+
## Snapshot and raw register interpretation
|
|
100
|
+
|
|
101
|
+
Snapshots require strict XML with `/RD5WEB/RD5`, no DTD and a valid root `t`
|
|
102
|
+
timestamp: a four-digit year and one- or two-digit month/day/hour/minute/second
|
|
103
|
+
fields (`YYYY-M-D H:M:S` or zero-padded equivalents), allowing surrounding
|
|
104
|
+
whitespace. Fields are normalized before strict calendar/timezone validation.
|
|
105
|
+
Registers are `/RD5WEB/RD5/*/O` elements with
|
|
106
|
+
`I` identifiers and decimal `V` values in 0–65535. Last occurrence wins for a
|
|
107
|
+
duplicate identifier. Missing required mapped readings, malformed numbers and
|
|
108
|
+
unexpected XML fail explicitly; no partial snapshot is published.
|
|
109
|
+
|
|
110
|
+
`SensorParser#raw_values` preserves unsigned integers. Temperature projection
|
|
111
|
+
alone applies signed 16-bit conversion then tenths-of-degree scaling: 65535 is
|
|
112
|
+
−0.1 °C, 65036 is −50.0 °C. A high configuration value stays unsigned.
|
|
113
|
+
`RD5Configuration` consumes the same raw snapshot without changing service
|
|
114
|
+
configuration. `Unit` checks freshness at every getter/serialization, publishes
|
|
115
|
+
atomically and invalidates on failed refresh or any attempted write.
|
|
116
|
+
|
|
117
|
+
## Declarative discovery: fixture profile confirmed on the live unit
|
|
118
|
+
|
|
119
|
+
`UserCtrl` looks up `/root/layout/body/content/i` by title, rather than treating
|
|
120
|
+
fixture IDs as universal:
|
|
121
|
+
|
|
122
|
+
| Title | Discovery |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `$outdoorTemp` | `id` for outdoor temperature |
|
|
125
|
+
| `$requiredTemp` | `id` for requested temperature, both public alias names |
|
|
126
|
+
| `$inletTemp` | Optional `id` and recognized `displayval` logic |
|
|
127
|
+
| `$currentPower` | `id` for current power; `onchange` for input/control fields |
|
|
128
|
+
| `$currentMode` | `id` for current mode; `displayval` reference for user-input selector; `onchange` for input/control fields |
|
|
129
|
+
|
|
130
|
+
`Mode` and `ModeText` option lists define numeric-string-keyed labels; translated
|
|
131
|
+
labels never replace numeric command IDs. Successful layout/text discovery is
|
|
132
|
+
cached per `UserCtrl` instance. Unknown command scripts raise
|
|
133
|
+
`UnsupportedConfiguration`; downloaded JavaScript is never evaluated.
|
|
134
|
+
|
|
135
|
+
`RD5Command.discover` accepts this **complete** function shape, with whitespace
|
|
136
|
+
between tokens and register IDs matching `H` plus five decimal digits:
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
function(val) {
|
|
140
|
+
saved=true;
|
|
141
|
+
send2Unit(getUrlPar('INPUT',val)+(values.SWITCH==1?'&'+getUrlPar('SWITCH',2):''));
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`INPUT` and `SWITCH` above are notation, not literal accepted IDs. Both `SWITCH`
|
|
146
|
+
references must be identical. Actual older fixture forms are:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
function(val) {
|
|
150
|
+
saved=true;
|
|
151
|
+
send2Unit(getUrlPar('H10708',val)+(values.H10700==1?'&'+getUrlPar('H10700',2):''));
|
|
152
|
+
if(val>0 && values.H10715==0) {
|
|
153
|
+
flyDown=false;
|
|
154
|
+
this.nextSibling.nextSibling.onmousedown();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function(val) {
|
|
158
|
+
saved=true;
|
|
159
|
+
send2Unit(getUrlPar('H10709',val)+(values.H10701==1?'&'+getUrlPar('H10701',2):''));
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The power-only UI tail is optional, but if present must match that specific
|
|
164
|
+
recognized form; arbitrary extra statements are unsupported. Input/control IDs
|
|
165
|
+
can differ when the complete supported form matches. The resulting string-keyed
|
|
166
|
+
maps contain `power_input`, `power_switch`, `mode_input`, `mode_switch`.
|
|
167
|
+
|
|
168
|
+
The successful live run discovered outdoor `I10208`, requested temperature
|
|
169
|
+
`H10706`, current power/mode `H10704`/`H10705`, selector `H10712`, power
|
|
170
|
+
input/switch `H10708`/`H10700`, and mode input/switch `H10709`/`H10701`.
|
|
171
|
+
Observed configuration included direct-speed control `H10510=0`, orientation
|
|
172
|
+
`H10501=1`, inlet profile `H11300=0`, `C10508=0`, `I11401=0`, and scheduled
|
|
173
|
+
power/mode control states of 1. These observations establish this unit's profile,
|
|
174
|
+
not universal addresses or behavior for every RD5 firmware.
|
|
175
|
+
|
|
176
|
+
Commands concatenate a discovered holding-register ID and exactly five decimal
|
|
177
|
+
digits (e.g. the older fixture's `H10708` plus 88 becomes `H1070800088`). One
|
|
178
|
+
write request carries the setpoint and, when needed, its control switch. Unlike
|
|
179
|
+
the UI's conditional, library policy treats **both** scheduled 1 and temporary 2
|
|
180
|
+
as temporary-control writes; manual 0 stays manual. Zero power follows the same
|
|
181
|
+
policy. Unknown/missing control state fails before dispatch. Permanent schedule
|
|
182
|
+
disable is outside these setters.
|
|
183
|
+
|
|
184
|
+
## Temperature profile and heating limitations
|
|
185
|
+
|
|
186
|
+
Supported inlet logic is `RD5Configuration::INLET_SOURCE_RULE`, matched as a complete
|
|
187
|
+
declarative form after whitespace removal:
|
|
188
|
+
|
|
189
|
+
* `H11300=0`, `H10501=1`: use `I10200` only when `H10200>0` and temperature is
|
|
190
|
+
not 126.0 °C (unsigned raw sentinel 1260).
|
|
191
|
+
* `H11300=0`, `H10501=2`: use `I10201` only when `H10201>0` and not sentinel.
|
|
192
|
+
* `H11300=1`: use `I10204` if present.
|
|
193
|
+
* Missing required optional sensor, stopped fan, other configuration or unknown
|
|
194
|
+
inlet script: return `nil`, without inventing a reading.
|
|
195
|
+
|
|
196
|
+
`UserCtrl.data` exports `input_temperature_source` as a JSON-safe string:
|
|
197
|
+
`"rd5_standard"` for the recognized rule, `"unsupported"` otherwise. Applications
|
|
198
|
+
that persist this data must also expose that field on reconstructed `user_ctrl:`
|
|
199
|
+
objects. The field selects device interpretation, not UI presentation; M1/M2/direct
|
|
200
|
+
selection continues to use the current raw snapshot inside the gem.
|
|
201
|
+
|
|
202
|
+
Legacy custom objects without `input_temperature_source` (or with nil metadata)
|
|
203
|
+
can supply a direct inlet mapping; callers own its correctness. Explicit unknown
|
|
204
|
+
metadata keeps the optional reading unavailable. Refresh previously cached metadata
|
|
205
|
+
on upgrade rather than interpreting the new map as a legacy direct source.
|
|
206
|
+
Outdoor mapping always comes
|
|
207
|
+
from `UserCtrl`: old fixture uses `I10208`, whereas PDF page 11 calls `I10211`
|
|
208
|
+
T-ODA. No blanket substitution is justified.
|
|
209
|
+
|
|
210
|
+
PDF `H10706` means requested temperature, not measured preheater temperature.
|
|
211
|
+
`required_temperature` and retained `preheat_temperature` reader/hash key expose
|
|
212
|
+
that same reading. Legacy `preheating` ORs `C10200`, `C10202`, `C10215`, `C10217`
|
|
213
|
+
when equal to 1. PDF descriptions include water-heater pump/heat-pump signals;
|
|
214
|
+
this aggregate does not establish dedicated preheater operation. No replacement
|
|
215
|
+
mapping has been fabricated.
|
|
216
|
+
|
|
217
|
+
## Requested versus effective values and verification
|
|
218
|
+
|
|
219
|
+
The PDF lists power/mode read fields `H10704`/`H10705` and write inputs
|
|
220
|
+
`H10708`/`H10709`. Older layout fixtures separately label `idw=H10714`/`H10715`
|
|
221
|
+
as requested power/mode while `id` fields are current readings. This difference
|
|
222
|
+
is unresolved for current firmware. Neither pair is automatically chosen as a
|
|
223
|
+
proven cloud requested-value read-back profile.
|
|
224
|
+
|
|
225
|
+
Verified `Unit#set_power` / `set_mode` default to `verify: true`, timeout 30
|
|
226
|
+
seconds and polling every five seconds. Without an explicit proven mapping they
|
|
227
|
+
raise `UnsupportedConfiguration` **before writing**. Trusted callers may provide
|
|
228
|
+
`Unit.new(..., verification_registers: mapping)` after independently establishing
|
|
229
|
+
semantics for the exact unit/profile. Mapping is a nonempty Hash with only symbol
|
|
230
|
+
keys `:power` and/or `:mode`, each mapped to a String matching `\AH[0-9]{5}\z`.
|
|
231
|
+
Validation checks syntax, not truth of the caller's profile.
|
|
232
|
+
|
|
233
|
+
Verification loads a fresh baseline containing the mapped raw register, builds
|
|
234
|
+
and sends one command, and polls fresh snapshots until the timestamp is strictly
|
|
235
|
+
newer than baseline and the raw requested value equals the normalized command.
|
|
236
|
+
A stale matching value cannot confirm. Budget includes all baseline/discovery,
|
|
237
|
+
HTTP, dispatch and sleep work. Baseline deadline failure raises
|
|
238
|
+
`UnitConnectionTimeout`; verification timeout after dispatch raises
|
|
239
|
+
`WriteNotConfirmed` with unknown outcome. Confirmation would establish a requested
|
|
240
|
+
setpoint only, not effective operation or physical airflow. No current live ACK
|
|
241
|
+
or read-back profile has been captured.
|
|
242
|
+
|
|
243
|
+
## Remaining integration acceptance
|
|
244
|
+
|
|
245
|
+
* Establish requested-value mappings and any application ACK from actual evidence.
|
|
246
|
+
* Only with separate agreement, test one ordinary write and restore both original
|
|
247
|
+
setpoint and original manual/scheduled/temporary control state. Do not alter
|
|
248
|
+
service configuration.
|
|
249
|
+
|
|
250
|
+
These remain hardware/protocol acceptance items, independent of fixture-suite
|
|
251
|
+
success. Temperature/zone setters, alarms, flow telemetry, external temperature
|
|
252
|
+
injection and Modbus transport remain deferred. External temperature injection's
|
|
253
|
+
90-second refresh rule must not be applied to ordinary power/mode requests.
|