homebridge-eliot-smart-control 0.5.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/LICENSE +21 -0
  3. package/README.md +172 -0
  4. package/config.schema.json +100 -0
  5. package/dist/accessory.d.ts +26 -0
  6. package/dist/accessory.js +282 -0
  7. package/dist/accessory.js.map +1 -0
  8. package/dist/config.d.ts +54 -0
  9. package/dist/config.js +24 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/eliot/desk.d.ts +177 -0
  12. package/dist/eliot/desk.js +562 -0
  13. package/dist/eliot/desk.js.map +1 -0
  14. package/dist/eliot/discover.d.ts +32 -0
  15. package/dist/eliot/discover.js +97 -0
  16. package/dist/eliot/discover.js.map +1 -0
  17. package/dist/eliot/link.d.ts +51 -0
  18. package/dist/eliot/link.js +244 -0
  19. package/dist/eliot/link.js.map +1 -0
  20. package/dist/eliot/move.d.ts +104 -0
  21. package/dist/eliot/move.js +122 -0
  22. package/dist/eliot/move.js.map +1 -0
  23. package/dist/eliot/protocol.d.ts +157 -0
  24. package/dist/eliot/protocol.js +212 -0
  25. package/dist/eliot/protocol.js.map +1 -0
  26. package/dist/eliot/queue.d.ts +24 -0
  27. package/dist/eliot/queue.js +51 -0
  28. package/dist/eliot/queue.js.map +1 -0
  29. package/dist/errors.d.ts +7 -0
  30. package/dist/errors.js +19 -0
  31. package/dist/errors.js.map +1 -0
  32. package/dist/index.d.ts +4 -0
  33. package/dist/index.js +7 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/platform.d.ts +12 -0
  36. package/dist/platform.js +73 -0
  37. package/dist/platform.js.map +1 -0
  38. package/dist/settings.d.ts +4 -0
  39. package/dist/settings.js +5 -0
  40. package/dist/settings.js.map +1 -0
  41. package/docs/PROTOCOL.md +330 -0
  42. package/homebridge-ui/public/index.html +168 -0
  43. package/homebridge-ui/server.js +41 -0
  44. package/package.json +63 -0
  45. package/tools/conflict-test.js +67 -0
  46. package/tools/desk-test.js +75 -0
  47. package/tools/eco-test.js +68 -0
  48. package/tools/eliot-probe.js +442 -0
  49. package/tools/find-desks.js +30 -0
  50. package/tools/goto-stop-test.js +127 -0
  51. package/tools/interrupt-test.js +120 -0
  52. package/tools/link-smoke.js +98 -0
  53. package/tools/lock-speed-test.js +117 -0
  54. package/tools/memory-test.js +84 -0
  55. package/tools/move-test.js +148 -0
  56. package/tools/velocity-test.js +65 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,125 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.5.0] — 2026-09-18
10
+
11
+ ### Added
12
+
13
+ - **The desk's child lock as a switch.** It shows the real state: unlike every
14
+ other setting the app can change, this one the control box reads back.
15
+
16
+ ### Changed
17
+
18
+ - **The desk is now given a height and left to drive there itself.** Its
19
+ control box has a *go to height* command and a *stop* command; neither
20
+ appears in the published write-ups of this protocol, and both were found in
21
+ the Eliot Android app and verified on hardware. Moves land exactly on target
22
+ instead of within about 1%, on the control box's own ramp, and stopping part
23
+ way coasts 13 mm rather than 18.
24
+ - Step commands remain as a fallback for a control box that does not know
25
+ *go to height*: if the desk has not moved shortly after being told where to
26
+ go, the old loop takes over. Untested against such a box, since none was to
27
+ hand.
28
+
29
+ ### Added
30
+
31
+ - `docs/PROTOCOL.md` records the twelve commands the published write-ups are
32
+ missing, and what the app's *Automatischer Reset* actually does — it is not
33
+ a command at all, but a drive to the physical bottom so the control box can
34
+ find its zero. It also records the two commands that do nothing on this
35
+ control box, measured rather than assumed, which is why no switches were
36
+ built for them.
37
+
38
+ ## [0.3.1] — 2026-09-18
39
+
40
+ ### Fixed
41
+
42
+ - **Memory switches did nothing after a restart.** Homebridge restores an
43
+ accessory's services from its cache but not their handlers, and the switches
44
+ were adopted without being wired up again — present in the Home app,
45
+ unresponsive, and silent about it. They worked only in the session that
46
+ created them.
47
+
48
+ ### Changed
49
+
50
+ - The accessory accepts an injected transport, so its behaviour can be tested
51
+ without a desk in the room. The fix above was untestable before.
52
+
53
+ ## [0.3.0] — 2026-09-18
54
+
55
+ ### Added
56
+
57
+ - **Find your desk** in the plugin's settings page. The dongle advertises as
58
+ `Schreibtisch` with no manufacturer name, a random-static address and often
59
+ nothing printed on the sticker, so a scan list gives nothing to go on.
60
+ Candidates are confirmed by the serial service rather than by name, and the
61
+ page reports the two states that otherwise look like failure: a dongle
62
+ already connected to something else, and a signal weak enough to predict
63
+ dropouts.
64
+ - `tools/find-desks.js`, the same discovery from the command line.
65
+
66
+ ## [0.2.1] — 2026-09-18
67
+
68
+ ### Fixed
69
+
70
+ - **A memory move can be stopped after all.** The previous release documented
71
+ the opposite. There is no stop command, but any step command cancels a move
72
+ the control box is driving — verified on hardware. `HoldPosition` works
73
+ during a preset, and a second move now cancels the first rather than racing
74
+ it.
75
+
76
+ ## [0.2.0] — 2026-09-18
77
+
78
+ ### Added
79
+
80
+ - **The desk's memory positions as momentary switches.** The control box drives
81
+ to them itself on its own ramp, easing into the target within about 2 mm,
82
+ which is better than this plugin manages with step commands. Only memories
83
+ the desk actually has appear.
84
+
85
+ ### Fixed
86
+
87
+ - Services set `ConfiguredName` as well as `Name`; without it the Home app
88
+ shows every switch under the accessory's own name.
89
+
90
+ ## [0.1.1] — 2026-09-18
91
+
92
+ ### Fixed
93
+
94
+ - **Measurement noise read as somebody using the handset.** The control box's
95
+ reported height wanders a few millimetres between reports, and the threshold
96
+ sat inside that noise, so the target was reset constantly. Movement is now
97
+ judged against the height the desk was last settled at, which also catches a
98
+ slow handset move that consecutive readings would miss entirely.
99
+
100
+ ## [0.1.0] — 2026-09-18
101
+
102
+ First release.
103
+
104
+ ### Added
105
+
106
+ - Control an Eliot sit-stand desk from the Home app over Bluetooth, as a window
107
+ covering. 0% is the lowest height the desk is configured to allow, 100% the
108
+ highest, both read from the desk rather than configured here.
109
+ - Position, direction of travel and a stop control; the Home app follows the
110
+ handset as well as the other way round.
111
+ - An unreachable desk reports a communication failure rather than the height it
112
+ had an hour ago.
113
+ - `docs/PROTOCOL.md`: the protocol, reverse-engineered for this desk and
114
+ verified against hardware, including the measured travel speed and stopping
115
+ distance.
116
+
117
+ [Unreleased]: https://github.com/rummeyer/homebridge-eliot-smart-control/compare/v0.5.0...HEAD
118
+ [0.5.0]: https://github.com/rummeyer/homebridge-eliot-smart-control/compare/v0.4.0...v0.5.0
119
+ [0.4.0]: https://github.com/rummeyer/homebridge-eliot-smart-control/compare/v0.3.1...v0.4.0
120
+ [0.3.1]: https://github.com/rummeyer/homebridge-eliot-smart-control/compare/v0.3.0...v0.3.1
121
+ [0.3.0]: https://github.com/rummeyer/homebridge-eliot-smart-control/compare/v0.2.1...v0.3.0
122
+ [0.2.1]: https://github.com/rummeyer/homebridge-eliot-smart-control/compare/v0.2.0...v0.2.1
123
+ [0.2.0]: https://github.com/rummeyer/homebridge-eliot-smart-control/compare/v0.1.1...v0.2.0
124
+ [0.1.1]: https://github.com/rummeyer/homebridge-eliot-smart-control/compare/v0.1.0...v0.1.1
125
+ [0.1.0]: https://github.com/rummeyer/homebridge-eliot-smart-control/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oliver Rummeyer
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.
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ <h1 align="center">homebridge-eliot-smart-control</h1>
2
+
3
+ <p align="center">
4
+ Your <b>Eliot sit-stand desk</b> in the Apple Home app &mdash; over Bluetooth, with no account and no cloud.
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/homebridge-eliot-smart-control"><img src="https://img.shields.io/npm/v/homebridge-eliot-smart-control?label=npm" alt="npm"></a>
9
+ <a href="https://github.com/rummeyer/homebridge-eliot-smart-control/actions/workflows/build.yml"><img src="https://github.com/rummeyer/homebridge-eliot-smart-control/actions/workflows/build.yml/badge.svg" alt="CI"></a>
10
+ <a href="https://github.com/rummeyer/homebridge-eliot-smart-control/blob/main/LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue" alt="Licence"></a>
11
+ <img src="https://img.shields.io/badge/homebridge-%E2%89%A5%202.0.0-purple" alt="Homebridge 2.0.0+">
12
+ <img src="https://img.shields.io/badge/node-22%20%7C%2024%20%7C%2026-green" alt="Node 22, 24 or 26">
13
+ </p>
14
+
15
+ ---
16
+
17
+ ## What it looks like in the Home app
18
+
19
+ **The desk itself**, as a slider from 0 to 100%.
20
+
21
+ 0% is the lowest height your desk is set to allow and 100% the highest. Those
22
+ come from the desk, not from this plugin, so if you change the limits on the
23
+ handset the ends of the slider follow.
24
+
25
+ *"Hey Siri, set the desk to 100%."* Or put it in an automation: up for the
26
+ morning meeting, down at the end of the day.
27
+
28
+ **Your memory positions**, as buttons &mdash; the same ones as on the handset.
29
+ Press one and the desk goes there; the button springs back, the way a scene
30
+ does. Only positions you have actually stored appear.
31
+
32
+ **The child lock**, as a switch. It shows the desk's real state, so if someone
33
+ locks it at the handset the Home app knows.
34
+
35
+ Everything follows the desk, not just the other way round. Use the handset and
36
+ the Home app keeps up. A desk out of range says *No Response* rather than
37
+ showing you the height it had an hour ago.
38
+
39
+ ## What you need
40
+
41
+ | | |
42
+ |---|---|
43
+ | **Desk** | An Eliot with the Smart Dongle, or the Procontroller |
44
+ | **Homebridge** | 2.0.0 or newer, **on Linux** |
45
+ | **Node.js** | 22.18, 24 or 26 |
46
+ | **Range** | Homebridge has to stay within Bluetooth range of the desk |
47
+
48
+ **Linux only.** The plugin talks to BlueZ over D-Bus, and macOS and Windows
49
+ have no equivalent. A Raspberry Pi running Homebridge is the usual home for it.
50
+ In a container, mount the host's `/var/run/dbus/system_bus_socket`.
51
+
52
+ **One connection at a time.** The dongle accepts a single Bluetooth connection.
53
+ While Homebridge has it, the Eliot app on your phone cannot connect — and while
54
+ the app has it, this plugin cannot.
55
+
56
+ ## Setting it up
57
+
58
+ **1. Install it.** In the Homebridge UI, go to **Plugins**, search for
59
+ `homebridge-eliot-smart-control` and choose **Install**.
60
+
61
+ **2. Give it a child bridge.** In the plugin's **⋮** menu, choose **Bridge
62
+ Settings** and turn the child bridge on. Bluetooth links drop and reconnect;
63
+ in a child bridge that churn stays in its own process instead of unsettling
64
+ your other accessories.
65
+
66
+ **3. Find your desk.** Open the plugin's **Settings** and press **Scan for
67
+ desks**.
68
+
69
+ This is the part worth having help with. The dongle does not announce itself:
70
+ it appears as **`Schreibtisch`**, with no manufacturer name, and many units
71
+ have nothing printed on them — in a scan list it sits among forty anonymous
72
+ phones with nothing to tell them apart. The page looks for the Bluetooth
73
+ service the desk actually speaks, so a result marked *confirmed* is the right
74
+ device and not a good guess.
75
+
76
+ Press **Use this** next to yours, name it, and save.
77
+
78
+ *Nothing found?* Something else probably has it. Close the Eliot app and turn
79
+ Bluetooth off on any phone that has been paired with the desk, then unplug the
80
+ dongle for five seconds.
81
+
82
+ **4. Add it to the Home app.** The child bridge has its own QR code under
83
+ **Bridge Settings**. Scan it the same way you paired Homebridge itself.
84
+
85
+ ## Settings
86
+
87
+ | | | |
88
+ |---|---|---|
89
+ | **Name** | required | What the desk is called in the Home app |
90
+ | **Dongle address** | required | Filled in for you by the scan |
91
+ | **Idle refresh** | 30 s | How often to ask a standing desk for its height. It reports by itself while moving, so this only catches the handset being used. 0 turns it off |
92
+ | **Memory switches** | on | Offer your stored positions as buttons |
93
+ | **Memory names** | — | What to call them. Defaults to *Memory 1*…*Memory 4* |
94
+ | **Child lock switch** | on | Offer the desk's child lock |
95
+
96
+ Or by hand, in `config.json`:
97
+
98
+ ```json
99
+ {
100
+ "platform": "EliotSmartControl",
101
+ "desks": [
102
+ {
103
+ "name": "Schreibtisch",
104
+ "mac": "E5:11:22:33:44:55",
105
+ "memoryNames": ["Sitzen", "Stehen", "Besprechung"]
106
+ }
107
+ ]
108
+ }
109
+ ```
110
+
111
+ ## Things worth knowing
112
+
113
+ **Positions land exactly.** The desk is handed a height and drives there on its
114
+ own ramp, easing in and stopping within a couple of millimetres — the same
115
+ mechanism as the memory buttons on the handset.
116
+
117
+ **Stopping works, and coasts about 13 mm.** There is no instant brake on a desk.
118
+
119
+ **Using the handset during a move is safe.** The control box gives the handset
120
+ priority and stops; it never drives against you. A brief press pauses the move
121
+ and it carries on, holding the button ends it, and the Home app settles on
122
+ wherever the desk actually is rather than trying again.
123
+
124
+ **An unset memory gets no button.** If you store a new position on the handset,
125
+ its button appears the next time the plugin connects.
126
+
127
+ **The desk's own safety features are untouched.** Anti-collision and the soft
128
+ limits live in the control box. The plugin adds limits of its own on top: it
129
+ gives up if the desk stops making progress, if a move takes longer than the
130
+ distance can account for, or if the desk stops saying where it is.
131
+
132
+ ## When something is wrong
133
+
134
+ **The scan finds nothing.** Close the Eliot app and turn Bluetooth off on any
135
+ phone that has been paired with the desk — closing the app does not always
136
+ release the connection, and the dongle will not advertise while something holds
137
+ it. Then unplug it for five seconds; it advertises immediately on power-up.
138
+
139
+ **It connects, then drops.** Check the signal. The settings page shows it beside
140
+ each result and warns when it is weak; anything near -90 dBm comes and goes.
141
+ Move Homebridge closer, or put a second one near the desk.
142
+
143
+ **A dongle that stays silent while plugged in and powered is broken.** Obvious
144
+ written down, indistinguishable from a range problem in practice, and worth
145
+ ruling out early by trying another one. It cost a day here.
146
+
147
+ **"No D-Bus system bus"** means BlueZ is not reachable: you are not on Linux, or
148
+ you are in a container without `/var/run/dbus/system_bus_socket` mounted.
149
+
150
+ **Everything looks right but nothing moves.** Check the dongle is in the port
151
+ marked **F** on the control box, not **HS** — that one is for the handset.
152
+
153
+ ## How it works
154
+
155
+ The Smart Dongle is a Lierda `LSD4BT-E95ALSP001` presenting a transparent
156
+ Bluetooth-to-serial bridge; behind it the control box speaks the Jiecang
157
+ handset protocol. None of that was written down anywhere for this desk, so
158
+ [`docs/PROTOCOL.md`](docs/PROTOCOL.md) writes it down now: the framing, the
159
+ command set, the measured travel speed and stopping distance, what the app's
160
+ *Automatischer Reset* really does, and which parts are verified against
161
+ hardware rather than assumed. Including the commands that turned out to do
162
+ nothing, which is worth knowing too.
163
+
164
+ Credit where it is due: the serial protocol was reverse-engineered for other
165
+ desks by [phord/Jarvis](https://github.com/phord/Jarvis) and
166
+ [Rocka84/jiecang_desk_controller](https://github.com/Rocka84/jiecang_desk_controller).
167
+ Working out that an Eliot speaks it, what the Bluetooth side looks like, and
168
+ the several commands those write-ups are missing is what this project added.
169
+
170
+ ## Licence
171
+
172
+ MIT
@@ -0,0 +1,100 @@
1
+ {
2
+ "pluginAlias": "EliotSmartControl",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Control an Eliot sit-stand desk with the Smart Dongle from the Home app, over Bluetooth. Requires Linux — the plugin talks to BlueZ.",
6
+ "footerDisplay": "The dongle advertises as **Schreibtisch** and has no vendor name, so it can be hard to spot. Run `node tools/eliot-probe.js scan` on the Homebridge host to find its address.\n\nThe desk appears as a window covering: 0% is the lowest height it is set to allow, 100% the highest. Those come from the desk's own limits, so changing them on the handset changes what 0% and 100% mean.",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "name": {
11
+ "title": "Name",
12
+ "type": "string",
13
+ "default": "Eliot Smart Control",
14
+ "description": "Only the log prefix. Each desk is named separately below."
15
+ },
16
+ "desks": {
17
+ "title": "Desks",
18
+ "type": "array",
19
+ "items": {
20
+ "type": "object",
21
+ "properties": {
22
+ "name": {
23
+ "title": "Name",
24
+ "type": "string",
25
+ "required": true,
26
+ "default": "Schreibtisch",
27
+ "placeholder": "Schreibtisch",
28
+ "description": "What the desk is called in the Home app."
29
+ },
30
+ "mac": {
31
+ "title": "Dongle address",
32
+ "type": "string",
33
+ "required": true,
34
+ "placeholder": "E5:11:22:33:44:55",
35
+ "pattern": "^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$",
36
+ "description": "Bluetooth address of the Smart Dongle."
37
+ },
38
+ "idlePollSeconds": {
39
+ "title": "Idle refresh (seconds)",
40
+ "type": "integer",
41
+ "default": 30,
42
+ "minimum": 0,
43
+ "description": "How often to ask a standing desk for its height. The desk reports by itself while moving, so this only catches the handset being used. 0 turns it off."
44
+ },
45
+ "memorySwitches": {
46
+ "title": "Memory-Schalter",
47
+ "type": "boolean",
48
+ "default": true,
49
+ "description": "Die Speicherplätze des Tisches als Schalter anbieten. Sie springen nach dem Drücken zurück, wie eine Szene. Nur belegte Plätze erscheinen."
50
+ },
51
+ "memoryNames": {
52
+ "title": "Namen der Memory-Schalter",
53
+ "type": "array",
54
+ "items": {
55
+ "type": "string",
56
+ "title": "Name"
57
+ },
58
+ "description": "Optionale Namen für Memory 1 bis 4, in dieser Reihenfolge. Leer lassen für \"Memory 1\", \"Memory 2\" und so weiter."
59
+ },
60
+ "childLockSwitch": {
61
+ "title": "Kindersicherung als Schalter",
62
+ "type": "boolean",
63
+ "default": true,
64
+ "description": "Die Kindersicherung des Tisches in der Home-App anbieten. Sie ist die einzige Einstellung, die die Controlbox zurückmeldet, also die einzige, die ehrlich angezeigt werden kann."
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ },
71
+ "layout": [
72
+ {
73
+ "key": "name"
74
+ },
75
+ {
76
+ "key": "desks",
77
+ "type": "array",
78
+ "orderable": false,
79
+ "buttonText": "Add desk",
80
+ "items": [
81
+ "desks[].name",
82
+ "desks[].mac",
83
+ "desks[].idlePollSeconds",
84
+ "desks[].childLockSwitch",
85
+ "desks[].memorySwitches",
86
+ {
87
+ "key": "desks[].memoryNames",
88
+ "type": "array",
89
+ "orderable": false,
90
+ "buttonText": "Namen hinzufügen",
91
+ "items": [
92
+ "desks[].memoryNames[]"
93
+ ]
94
+ }
95
+ ]
96
+ }
97
+ ],
98
+ "customUi": true,
99
+ "customUiPath": "./homebridge-ui"
100
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * One desk as a HomeKit window covering.
3
+ *
4
+ * A covering rather than a light, because a desk is a positional actuator that
5
+ * takes time to get there: `PositionState` says which way it is going and
6
+ * `HoldPosition` stops it, neither of which a dimmer can express. It also
7
+ * keeps the desk out of "turn off all the lights", which would otherwise drive
8
+ * it to its lowest setting at bedtime.
9
+ */
10
+ import type { PlatformAccessory } from 'homebridge';
11
+ import type { DeskConfig } from './config.ts';
12
+ import type { Transport } from './eliot/desk.ts';
13
+ import type { EliotPlatform } from './platform.ts';
14
+ export declare class EliotAccessory {
15
+ #private;
16
+ /**
17
+ * @param transport Stand-in for the Bluetooth link. Only tests pass one;
18
+ * in normal use the accessory builds its own. Without this seam there is
19
+ * no way to exercise the accessory without a desk in the room, and the
20
+ * handler wiring is exactly the part that needs exercising.
21
+ */
22
+ constructor(platform: EliotPlatform, accessory: PlatformAccessory, config: DeskConfig, transport?: Transport);
23
+ get name(): string;
24
+ start(): Promise<void>;
25
+ stop(): Promise<void>;
26
+ }