homebridge-tesy-heater-mqtt 1.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.
- package/CHANGELOG.md +84 -0
- package/LICENSE +22 -0
- package/README.md +244 -0
- package/config.schema.json +113 -0
- package/index.js +537 -0
- package/package.json +58 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2025-12-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Platform plugin architecture** - complete rewrite from Accessory to Platform
|
|
14
|
+
- **Automatic device discovery** - finds all Tesy heaters in your account automatically
|
|
15
|
+
- **Multi-device support** - manages multiple heaters with single configuration
|
|
16
|
+
- Shared MQTT connection for all devices
|
|
17
|
+
- Device name discovery from Tesy Cloud
|
|
18
|
+
- Persistent device caching across restarts
|
|
19
|
+
- Automatic device addition/removal based on account
|
|
20
|
+
- GitHub Actions CI/CD workflows
|
|
21
|
+
- Automated testing on push and PR (Node.js 18, 20, 22, 24)
|
|
22
|
+
- Automated NPM publishing on version tags
|
|
23
|
+
- `.npmignore` file to exclude dev files from package
|
|
24
|
+
- Debug logging for device discovery
|
|
25
|
+
- Support for Node.js v24
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- **BREAKING**: Configuration format changed from `accessories` to `platforms`
|
|
29
|
+
- **BREAKING**: No longer requires `device_id` parameter (automatic discovery)
|
|
30
|
+
- Simplified configuration - only credentials needed
|
|
31
|
+
- Updated README with Platform configuration
|
|
32
|
+
- Added setup guide for GitHub Actions
|
|
33
|
+
- Improved logging with per-device context
|
|
34
|
+
- Default pullInterval changed from 10000ms to 60000ms (1 minute)
|
|
35
|
+
- Updated config.schema.json for Platform type with helpful UI
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- Device name retrieval from correct API field (`deviceData.deviceName`)
|
|
39
|
+
- MQTT connection now shared across all devices
|
|
40
|
+
- Better error handling for missing credentials
|
|
41
|
+
|
|
42
|
+
### Technical
|
|
43
|
+
- Single MQTT connection for all devices (better resource usage)
|
|
44
|
+
- Devices cached by Homebridge (faster startup)
|
|
45
|
+
- Platform meets Homebridge Verified Plugin requirements
|
|
46
|
+
- Cleaner codebase with better separation of concerns
|
|
47
|
+
|
|
48
|
+
## [0.0.1] - 2025-12-15
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
- Initial release based on homebridge-tesy-heater
|
|
52
|
+
- Support for Tesy API v4
|
|
53
|
+
- MQTT control protocol implementation
|
|
54
|
+
- Real-time device control via MQTT WebSocket
|
|
55
|
+
- Temperature setter and getter
|
|
56
|
+
- On/Off control
|
|
57
|
+
- Current temperature reporting
|
|
58
|
+
- Target temperature configuration
|
|
59
|
+
- Unit tests with ~35% coverage
|
|
60
|
+
- Test scripts for MQTT debugging
|
|
61
|
+
- Support for Homebridge v2.x
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- Migrated from Tesy API v3 to v4
|
|
65
|
+
- Replaced REST API control with MQTT
|
|
66
|
+
- Updated configuration for Homebridge v2 compatibility
|
|
67
|
+
- Modernized Node.js compatibility (18.x, 20.x, 22.x)
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
- Missing getter for HeatingThresholdTemperature characteristic
|
|
71
|
+
- Device not responding to REST API commands
|
|
72
|
+
- Compatibility issues with latest Homebridge versions
|
|
73
|
+
|
|
74
|
+
### Technical Details
|
|
75
|
+
- MQTT broker: wss://mqtt.tesy.com:8083/mqtt
|
|
76
|
+
- Command topics: v1/{MAC}/request/{MODEL}/{TOKEN}/{COMMAND}
|
|
77
|
+
- Response topics: v1/{MAC}/response/{MODEL}/{TOKEN}/{COMMAND}
|
|
78
|
+
- Supported commands: onOff, setTemp, setMode
|
|
79
|
+
|
|
80
|
+
### Tested Devices
|
|
81
|
+
- Tesy CN 06 100 EА CLOUD AS W
|
|
82
|
+
|
|
83
|
+
[Unreleased]: https://github.com/svjakrm/homebridge-tesy-heater-mqtt/compare/v0.0.1...HEAD
|
|
84
|
+
[0.0.1]: https://github.com/svjakrm/homebridge-tesy-heater-mqtt/releases/tag/v0.0.1
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Original work Copyright (c) 2021 Dobriyan Benov
|
|
4
|
+
Modified work Copyright (c) 2025 Aleksey Molchanov
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# homebridge-tesy-heater-mqtt
|
|
2
|
+
|
|
3
|
+
[](https://github.com/svjakrm/homebridge-tesy-heater-mqtt/actions/workflows/test.yml)
|
|
4
|
+
[](https://badge.fury.io/js/homebridge-tesy-heater-mqtt)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Homebridge plugin for Tesy heaters using MQTT control (Tesy API v4).
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This plugin allows you to control your Tesy smart heaters through Apple HomeKit using Homebridge. It uses the latest Tesy API v4 with MQTT for real-time device control.
|
|
12
|
+
|
|
13
|
+
**Features:**
|
|
14
|
+
- **Automatic device discovery** - finds all Tesy heaters in your account
|
|
15
|
+
- **Multi-device support** - manages multiple heaters with one configuration
|
|
16
|
+
- Turn heater on/off
|
|
17
|
+
- Set target temperature
|
|
18
|
+
- View current temperature
|
|
19
|
+
- View heating status
|
|
20
|
+
- Temperature range configuration
|
|
21
|
+
- Real-time MQTT control
|
|
22
|
+
- Persistent device caching
|
|
23
|
+
|
|
24
|
+
## Compatibility
|
|
25
|
+
|
|
26
|
+
This plugin has been tested and confirmed working with:
|
|
27
|
+
- **Tesy CN 06 100 EА CLOUD AS W**
|
|
28
|
+
|
|
29
|
+
It will most likely work with other **Tesy CN06 series** devices and possibly with other [Tesy FinEco Cloud](https://tesy.bg/produkti/otoplenie-i-grija-za-vyzduha/elektricheski-konvektori/view_group/1) convectors that support the Tesy Cloud v4 API.
|
|
30
|
+
|
|
31
|
+
If you successfully use this plugin with other Tesy models, please let us know!
|
|
32
|
+
|
|
33
|
+
## Credits
|
|
34
|
+
|
|
35
|
+
This project is based on [homebridge-tesy-heater](https://github.com/benov84/homebridge-tesy-heater) by [Dobriyan Benov](https://github.com/benov84). The original plugin was updated to work with Tesy API v4 and MQTT control protocol.
|
|
36
|
+
|
|
37
|
+
**Major changes from original:**
|
|
38
|
+
- Migrated from Tesy API v3 to API v4
|
|
39
|
+
- Implemented MQTT control for device commands
|
|
40
|
+
- Added `mqtt` dependency for real-time communication
|
|
41
|
+
- Removed obsolete authentication methods
|
|
42
|
+
- Added getter for `HeatingThresholdTemperature` characteristic
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
### Option 1: Install from npm (when published)
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g homebridge-tesy-heater-mqtt
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Option 2: Install from GitHub
|
|
52
|
+
```bash
|
|
53
|
+
npm install -g https://github.com/svjakrm/homebridge-tesy-heater-mqtt.git
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Option 3: Manual installation
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/svjakrm/homebridge-tesy-heater-mqtt.git
|
|
59
|
+
cd homebridge-tesy-heater-mqtt
|
|
60
|
+
npm install -g .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
Add this platform to your Homebridge `config.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"platforms": [
|
|
70
|
+
{
|
|
71
|
+
"platform": "TesyHeater",
|
|
72
|
+
"name": "TesyHeater",
|
|
73
|
+
"userid": "YOUR_USER_ID",
|
|
74
|
+
"username": "your.email@example.com",
|
|
75
|
+
"password": "your_password",
|
|
76
|
+
"maxTemp": 30,
|
|
77
|
+
"minTemp": 10,
|
|
78
|
+
"pullInterval": 60000
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Note:** The plugin will **automatically discover** all Tesy heaters linked to your account. No need to specify device IDs!
|
|
85
|
+
|
|
86
|
+
### Configuration Parameters
|
|
87
|
+
|
|
88
|
+
| Parameter | Required | Description | Default |
|
|
89
|
+
|-----------|----------|-------------|---------|
|
|
90
|
+
| `platform` | **Yes** | Must be `TesyHeater` | - |
|
|
91
|
+
| `name` | **Yes** | Platform name | `TesyHeater` |
|
|
92
|
+
| `userid` | **Yes** | Your Tesy account user ID | - |
|
|
93
|
+
| `username` | **Yes** | Your Tesy account email | - |
|
|
94
|
+
| `password` | **Yes** | Your Tesy account password | - |
|
|
95
|
+
| `maxTemp` | No | Maximum temperature (°C) | `30` |
|
|
96
|
+
| `minTemp` | No | Minimum temperature (°C) | `10` |
|
|
97
|
+
| `pullInterval` | No | Status update interval (ms) | `60000` |
|
|
98
|
+
|
|
99
|
+
### How to Get Configuration Values
|
|
100
|
+
|
|
101
|
+
**Get your credentials** (`userid`, `username`, `password`):
|
|
102
|
+
- These are your Tesy Cloud account credentials
|
|
103
|
+
- `userid` can be found in the Tesy Cloud web interface or via API
|
|
104
|
+
- `username` is your email used to log in to Tesy Cloud
|
|
105
|
+
- `password` is your Tesy Cloud password
|
|
106
|
+
|
|
107
|
+
**To find your User ID**, run this command:
|
|
108
|
+
```bash
|
|
109
|
+
curl -s 'https://ad.mytesy.com/rest/get-my-devices?userEmail=YOUR_EMAIL&userPass=YOUR_PASSWORD&lang=en' | python3 -m json.tool
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The response will show your `userid` and all your devices. Example:
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"AA:BB:CC:DD:EE:FF": {
|
|
116
|
+
"deviceName": "Living Room Heater",
|
|
117
|
+
"token": "abc1234",
|
|
118
|
+
"state": {
|
|
119
|
+
"id": 123456,
|
|
120
|
+
"mac": "AA:BB:CC:DD:EE:FF",
|
|
121
|
+
"status": "on",
|
|
122
|
+
"temp": 20,
|
|
123
|
+
...
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**That's it!** The plugin will automatically discover all devices in your account.
|
|
130
|
+
|
|
131
|
+
## Technical Details
|
|
132
|
+
|
|
133
|
+
### How it Works
|
|
134
|
+
|
|
135
|
+
1. **Device Discovery**: Fetches all devices from Tesy API v4 (`/rest/get-my-devices`) on startup
|
|
136
|
+
2. **Status Updates**: Polls device status periodically (default: 60 seconds)
|
|
137
|
+
3. **Device Control**: Single MQTT connection (`wss://mqtt.tesy.com:8083`) shared by all devices
|
|
138
|
+
4. **MQTT Topics**: `v1/{MAC}/request/{MODEL}/{TOKEN}/{COMMAND}`
|
|
139
|
+
5. **Commands**: `onOff` (power), `setTemp` (temperature), `setMode` (heating mode)
|
|
140
|
+
6. **Caching**: Devices persist across Homebridge restarts
|
|
141
|
+
|
|
142
|
+
### MQTT Protocol
|
|
143
|
+
|
|
144
|
+
The plugin automatically:
|
|
145
|
+
- Connects to Tesy MQTT broker using shared credentials
|
|
146
|
+
- Retrieves device-specific token and model from API
|
|
147
|
+
- Publishes commands to device-specific MQTT topics
|
|
148
|
+
- Subscribes to response topics for acknowledgments
|
|
149
|
+
|
|
150
|
+
## Troubleshooting
|
|
151
|
+
|
|
152
|
+
### Device not responding to commands
|
|
153
|
+
- Check that your device is online in Tesy Cloud app
|
|
154
|
+
- Verify your credentials are correct
|
|
155
|
+
- Check Homebridge logs for MQTT connection errors
|
|
156
|
+
|
|
157
|
+
### Temperature slider not showing in Home app
|
|
158
|
+
- Make sure you're running the latest version of this plugin
|
|
159
|
+
- Restart Homebridge after updating configuration
|
|
160
|
+
- Remove and re-add the accessory in Home app if needed
|
|
161
|
+
|
|
162
|
+
### HomeKit shows "No Response"
|
|
163
|
+
- Check your internet connection
|
|
164
|
+
- Verify Homebridge is running
|
|
165
|
+
- Check if device is online in Tesy Cloud
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
### Testing MQTT Commands
|
|
170
|
+
|
|
171
|
+
Test scripts are included in the repository:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Test connection and monitor messages
|
|
175
|
+
node test-mqtt.js
|
|
176
|
+
|
|
177
|
+
# Test on/off control
|
|
178
|
+
node test-mqtt-control.js
|
|
179
|
+
|
|
180
|
+
# Test temperature setting
|
|
181
|
+
node test-mqtt-temp.js
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Unit Tests
|
|
185
|
+
|
|
186
|
+
The plugin includes unit tests for core functionality:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Run tests
|
|
190
|
+
npm test
|
|
191
|
+
|
|
192
|
+
# Run tests with coverage report
|
|
193
|
+
npm run test:coverage
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Tests cover:
|
|
197
|
+
- MQTT connection and initialization
|
|
198
|
+
- Command formatting and sending
|
|
199
|
+
- Device control (on/off, temperature)
|
|
200
|
+
- Error handling
|
|
201
|
+
- Configuration validation
|
|
202
|
+
|
|
203
|
+
## Support
|
|
204
|
+
|
|
205
|
+
If you find this plugin useful, consider supporting:
|
|
206
|
+
|
|
207
|
+
[](https://revolut.me/molchaoxez)
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT License
|
|
212
|
+
|
|
213
|
+
Original work Copyright (c) 2021 Dobriyan Benov
|
|
214
|
+
Modified work Copyright (c) 2025 Aleksey Molchanov
|
|
215
|
+
|
|
216
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
217
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
218
|
+
in the Software without restriction, including without limitation the rights
|
|
219
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
220
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
221
|
+
furnished to do so, subject to the following conditions:
|
|
222
|
+
|
|
223
|
+
The above copyright notice and this permission notice shall be included in all
|
|
224
|
+
copies or substantial portions of the Software.
|
|
225
|
+
|
|
226
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
227
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
228
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
229
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
230
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
231
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
232
|
+
SOFTWARE.
|
|
233
|
+
|
|
234
|
+
## Contributing
|
|
235
|
+
|
|
236
|
+
Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change.
|
|
237
|
+
|
|
238
|
+
## Links
|
|
239
|
+
|
|
240
|
+
- [GitHub Repository](https://github.com/svjakrm/homebridge-tesy-heater-mqtt)
|
|
241
|
+
- [Original Plugin](https://github.com/benov84/homebridge-tesy-heater)
|
|
242
|
+
- [Homebridge](https://github.com/homebridge/homebridge)
|
|
243
|
+
- [Tesy Cloud](https://v4.mytesy.com)
|
|
244
|
+
- [Tesy FinEco Cloud Convectors](https://tesy.bg/produkti/otoplenie-i-grija-za-vyzduha/elektricheski-konvektori/view_group/1)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "TesyHeater",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"schema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"name": {
|
|
9
|
+
"title": "Platform Name",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"default": "TesyHeater",
|
|
12
|
+
"required": true
|
|
13
|
+
},
|
|
14
|
+
"userid": {
|
|
15
|
+
"title": "Tesy Cloud User ID",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": true,
|
|
18
|
+
"description": "Your Tesy Cloud account user ID (numeric)"
|
|
19
|
+
},
|
|
20
|
+
"username": {
|
|
21
|
+
"title": "Tesy Cloud Email",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"required": true,
|
|
24
|
+
"format": "email",
|
|
25
|
+
"description": "Your Tesy Cloud account email address"
|
|
26
|
+
},
|
|
27
|
+
"password": {
|
|
28
|
+
"title": "Tesy Cloud Password",
|
|
29
|
+
"type": "string",
|
|
30
|
+
"required": true,
|
|
31
|
+
"description": "Your Tesy Cloud account password"
|
|
32
|
+
},
|
|
33
|
+
"maxTemp": {
|
|
34
|
+
"title": "Maximum Temperature (°C)",
|
|
35
|
+
"type": "integer",
|
|
36
|
+
"default": 30,
|
|
37
|
+
"minimum": 10,
|
|
38
|
+
"maximum": 40,
|
|
39
|
+
"description": "Maximum temperature limit for all heaters"
|
|
40
|
+
},
|
|
41
|
+
"minTemp": {
|
|
42
|
+
"title": "Minimum Temperature (°C)",
|
|
43
|
+
"type": "integer",
|
|
44
|
+
"default": 10,
|
|
45
|
+
"minimum": 5,
|
|
46
|
+
"maximum": 25,
|
|
47
|
+
"description": "Minimum temperature limit for all heaters"
|
|
48
|
+
},
|
|
49
|
+
"pullInterval": {
|
|
50
|
+
"title": "Status Update Interval (ms)",
|
|
51
|
+
"type": "integer",
|
|
52
|
+
"default": 60000,
|
|
53
|
+
"minimum": 30000,
|
|
54
|
+
"maximum": 300000,
|
|
55
|
+
"description": "How often to poll device status (in milliseconds). Default: 60000 (1 minute)"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"layout": [
|
|
60
|
+
{
|
|
61
|
+
"type": "help",
|
|
62
|
+
"helpvalue": "<h5>Tesy Heater Platform</h5><p>This plugin will <strong>automatically discover</strong> all Tesy heaters linked to your Tesy Cloud account. Simply enter your credentials below and restart Homebridge.</p>"
|
|
63
|
+
},
|
|
64
|
+
"name",
|
|
65
|
+
{
|
|
66
|
+
"type": "section",
|
|
67
|
+
"title": "Tesy Cloud Credentials",
|
|
68
|
+
"expandable": true,
|
|
69
|
+
"expanded": true,
|
|
70
|
+
"items": [
|
|
71
|
+
{
|
|
72
|
+
"key": "userid",
|
|
73
|
+
"type": "string",
|
|
74
|
+
"placeholder": "27356"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"key": "username",
|
|
78
|
+
"type": "string",
|
|
79
|
+
"placeholder": "your.email@example.com"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"key": "password",
|
|
83
|
+
"type": "string",
|
|
84
|
+
"placeholder": "Your password"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"type": "section",
|
|
90
|
+
"title": "Advanced Settings",
|
|
91
|
+
"expandable": true,
|
|
92
|
+
"expanded": false,
|
|
93
|
+
"items": [
|
|
94
|
+
{
|
|
95
|
+
"key": "maxTemp",
|
|
96
|
+
"type": "number"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"key": "minTemp",
|
|
100
|
+
"type": "number"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"key": "pullInterval",
|
|
104
|
+
"type": "number"
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"type": "help",
|
|
110
|
+
"helpvalue": "<p><strong>How to find your User ID:</strong></p><ol><li>Log in to <a href='https://v4.mytesy.com' target='_blank'>Tesy Cloud</a></li><li>Check the browser URL or use the API to retrieve your user ID</li><li>Or run: <code>curl 'https://ad.mytesy.com/rest/get-my-devices?userEmail=YOUR_EMAIL&userPass=YOUR_PASSWORD&lang=en'</code></li></ol>"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
var Service, Characteristic, API;
|
|
2
|
+
const mqtt = require('mqtt');
|
|
3
|
+
const request = require('request');
|
|
4
|
+
const querystring = require('querystring');
|
|
5
|
+
|
|
6
|
+
module.exports = function(homebridge) {
|
|
7
|
+
Service = homebridge.hap.Service;
|
|
8
|
+
Characteristic = homebridge.hap.Characteristic;
|
|
9
|
+
API = homebridge;
|
|
10
|
+
|
|
11
|
+
homebridge.registerPlatform("homebridge-tesy-heater-mqtt", "TesyHeater", TesyHeaterPlatform, true);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
class TesyHeaterPlatform {
|
|
15
|
+
constructor(log, config, api) {
|
|
16
|
+
this.log = log;
|
|
17
|
+
this.config = config || {};
|
|
18
|
+
this.api = api;
|
|
19
|
+
|
|
20
|
+
this.accessories = [];
|
|
21
|
+
this.devices = {};
|
|
22
|
+
this.mqttClient = null;
|
|
23
|
+
|
|
24
|
+
// Configuration
|
|
25
|
+
this.userid = this.config.userid;
|
|
26
|
+
this.username = this.config.username;
|
|
27
|
+
this.password = this.config.password;
|
|
28
|
+
this.pullInterval = this.config.pullInterval || 60000;
|
|
29
|
+
this.maxTemp = this.config.maxTemp || 30;
|
|
30
|
+
this.minTemp = this.config.minTemp || 10;
|
|
31
|
+
|
|
32
|
+
if (!this.userid || !this.username || !this.password) {
|
|
33
|
+
this.log.error("Missing required credentials (userid, username, password) in config!");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.log.info("TesyHeater Platform Plugin Loaded");
|
|
38
|
+
|
|
39
|
+
if (api) {
|
|
40
|
+
this.api.on('didFinishLaunching', () => {
|
|
41
|
+
this.log.info("Homebridge finished launching, discovering devices...");
|
|
42
|
+
this.discoverDevices();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
configureAccessory(accessory) {
|
|
48
|
+
this.log.info("Configuring cached accessory:", accessory.displayName);
|
|
49
|
+
|
|
50
|
+
accessory.reachable = true;
|
|
51
|
+
this.accessories.push(accessory);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
discoverDevices() {
|
|
55
|
+
this.log.info("Fetching devices from Tesy Cloud...");
|
|
56
|
+
|
|
57
|
+
const queryParams = querystring.stringify({
|
|
58
|
+
'userID': parseInt(this.userid),
|
|
59
|
+
'userEmail': this.username,
|
|
60
|
+
'userPass': this.password,
|
|
61
|
+
'lang': 'en'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const options = {
|
|
65
|
+
'method': 'GET',
|
|
66
|
+
'url': 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
request(options, (error, response) => {
|
|
70
|
+
if (error) {
|
|
71
|
+
this.log.error("API Error:", error);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(response.body);
|
|
77
|
+
|
|
78
|
+
if (!data || Object.keys(data).length === 0) {
|
|
79
|
+
this.log.warn("No devices found in your Tesy Cloud account");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.log.info("Found %d device(s) in your account", Object.keys(data).length);
|
|
84
|
+
|
|
85
|
+
// Initialize MQTT connection once before adding devices
|
|
86
|
+
this.initMQTT();
|
|
87
|
+
|
|
88
|
+
// Add each device
|
|
89
|
+
for (const mac in data) {
|
|
90
|
+
const deviceData = data[mac];
|
|
91
|
+
const state = deviceData.state;
|
|
92
|
+
|
|
93
|
+
if (!state || !state.id) {
|
|
94
|
+
this.log.warn("Skipping device with MAC %s - missing state data", mac);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Try to get device name from various possible fields
|
|
99
|
+
const deviceName = state.deviceName ||
|
|
100
|
+
state.name ||
|
|
101
|
+
deviceData.deviceName ||
|
|
102
|
+
deviceData.name ||
|
|
103
|
+
`Tesy Heater ${state.id}`;
|
|
104
|
+
|
|
105
|
+
this.log.debug("Device data for %s:", mac, JSON.stringify({
|
|
106
|
+
'state.deviceName': state.deviceName,
|
|
107
|
+
'state.name': state.name,
|
|
108
|
+
'deviceData.deviceName': deviceData.deviceName,
|
|
109
|
+
'deviceData.name': deviceData.name,
|
|
110
|
+
'using': deviceName
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
this.addDevice({
|
|
114
|
+
id: state.id.toString(),
|
|
115
|
+
mac: mac,
|
|
116
|
+
token: deviceData.token,
|
|
117
|
+
model: deviceData.model || 'cn05uv',
|
|
118
|
+
name: deviceName,
|
|
119
|
+
state: state
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Remove devices that are no longer in account
|
|
124
|
+
this.removeOldDevices(data);
|
|
125
|
+
|
|
126
|
+
// Start status polling
|
|
127
|
+
this.startPolling();
|
|
128
|
+
|
|
129
|
+
} catch(e) {
|
|
130
|
+
this.log.error("Error parsing device data:", e);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
addDevice(deviceInfo) {
|
|
136
|
+
const uuid = this.api.hap.uuid.generate('tesy-' + deviceInfo.id);
|
|
137
|
+
let accessory = this.accessories.find(acc => acc.UUID === uuid);
|
|
138
|
+
|
|
139
|
+
if (accessory) {
|
|
140
|
+
this.log.info("Restoring existing accessory:", deviceInfo.name);
|
|
141
|
+
|
|
142
|
+
// Always update name to ensure HomeKit has the latest value
|
|
143
|
+
if (accessory.displayName !== deviceInfo.name) {
|
|
144
|
+
this.log.info("Updating accessory name from '%s' to '%s'", accessory.displayName, deviceInfo.name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Update displayName
|
|
148
|
+
accessory.displayName = deviceInfo.name;
|
|
149
|
+
|
|
150
|
+
// Update context
|
|
151
|
+
accessory.context.deviceInfo = deviceInfo;
|
|
152
|
+
|
|
153
|
+
// Update AccessoryInformation service name
|
|
154
|
+
const infoService = accessory.getService(Service.AccessoryInformation);
|
|
155
|
+
if (infoService) {
|
|
156
|
+
infoService.setCharacteristic(Characteristic.Name, deviceInfo.name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update HeaterCooler service name
|
|
160
|
+
const service = accessory.getService(Service.HeaterCooler);
|
|
161
|
+
if (service) {
|
|
162
|
+
service.setCharacteristic(Characteristic.Name, deviceInfo.name);
|
|
163
|
+
service.displayName = deviceInfo.name;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Also update the internal HAP accessory displayName
|
|
167
|
+
if (accessory._associatedHAPAccessory) {
|
|
168
|
+
accessory._associatedHAPAccessory.displayName = deviceInfo.name;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.api.updatePlatformAccessories([accessory]);
|
|
172
|
+
} else {
|
|
173
|
+
this.log.info("Adding new accessory:", deviceInfo.name);
|
|
174
|
+
accessory = new this.api.platformAccessory(deviceInfo.name, uuid);
|
|
175
|
+
accessory.context.deviceInfo = deviceInfo;
|
|
176
|
+
|
|
177
|
+
this.accessories.push(accessory);
|
|
178
|
+
this.api.registerPlatformAccessories("homebridge-tesy-heater-mqtt", "TesyHeater", [accessory]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Setup accessory
|
|
182
|
+
this.setupAccessory(accessory);
|
|
183
|
+
|
|
184
|
+
// Store device reference
|
|
185
|
+
this.devices[deviceInfo.id] = {
|
|
186
|
+
accessory: accessory,
|
|
187
|
+
info: deviceInfo
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
removeOldDevices(currentData) {
|
|
192
|
+
const currentDeviceIds = Object.values(currentData)
|
|
193
|
+
.filter(d => d.state && d.state.id)
|
|
194
|
+
.map(d => d.state.id.toString());
|
|
195
|
+
|
|
196
|
+
const accessoriesToRemove = this.accessories.filter(acc => {
|
|
197
|
+
if (!acc.context.deviceInfo) return false;
|
|
198
|
+
return !currentDeviceIds.includes(acc.context.deviceInfo.id);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (accessoriesToRemove.length > 0) {
|
|
202
|
+
this.log.info("Removing %d device(s) that are no longer in account", accessoriesToRemove.length);
|
|
203
|
+
this.api.unregisterPlatformAccessories("homebridge-tesy-heater-mqtt", "TesyHeater", accessoriesToRemove);
|
|
204
|
+
|
|
205
|
+
accessoriesToRemove.forEach(acc => {
|
|
206
|
+
const index = this.accessories.indexOf(acc);
|
|
207
|
+
if (index > -1) {
|
|
208
|
+
this.accessories.splice(index, 1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (acc.context.deviceInfo) {
|
|
212
|
+
delete this.devices[acc.context.deviceInfo.id];
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
setupAccessory(accessory) {
|
|
219
|
+
const deviceInfo = accessory.context.deviceInfo;
|
|
220
|
+
|
|
221
|
+
// Information Service
|
|
222
|
+
const informationService = accessory.getService(Service.AccessoryInformation) ||
|
|
223
|
+
accessory.addService(Service.AccessoryInformation);
|
|
224
|
+
|
|
225
|
+
informationService
|
|
226
|
+
.setCharacteristic(Characteristic.Manufacturer, 'Tesy')
|
|
227
|
+
.setCharacteristic(Characteristic.Model, deviceInfo.model || 'Convector')
|
|
228
|
+
.setCharacteristic(Characteristic.SerialNumber, deviceInfo.id);
|
|
229
|
+
|
|
230
|
+
// HeaterCooler Service
|
|
231
|
+
let service = accessory.getService(Service.HeaterCooler);
|
|
232
|
+
if (!service) {
|
|
233
|
+
service = accessory.addService(Service.HeaterCooler, deviceInfo.name);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Configure characteristics
|
|
237
|
+
service.getCharacteristic(Characteristic.Active)
|
|
238
|
+
.on('get', this.getActive.bind(this, deviceInfo))
|
|
239
|
+
.on('set', this.setActive.bind(this, deviceInfo));
|
|
240
|
+
|
|
241
|
+
service.getCharacteristic(Characteristic.CurrentHeaterCoolerState)
|
|
242
|
+
.updateValue(Characteristic.CurrentHeaterCoolerState.INACTIVE);
|
|
243
|
+
|
|
244
|
+
service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
|
|
245
|
+
.on('get', callback => callback(null, Characteristic.TargetHeaterCoolerState.HEAT))
|
|
246
|
+
.setProps({
|
|
247
|
+
validValues: [Characteristic.TargetHeaterCoolerState.HEAT]
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
service.getCharacteristic(Characteristic.CurrentTemperature)
|
|
251
|
+
.on('get', this.getCurrentTemperature.bind(this, deviceInfo))
|
|
252
|
+
.setProps({ minStep: 0.1 });
|
|
253
|
+
|
|
254
|
+
service.getCharacteristic(Characteristic.HeatingThresholdTemperature)
|
|
255
|
+
.on('get', this.getHeatingThresholdTemperature.bind(this, deviceInfo))
|
|
256
|
+
.on('set', this.setHeatingThresholdTemperature.bind(this, deviceInfo))
|
|
257
|
+
.setProps({
|
|
258
|
+
minValue: this.minTemp,
|
|
259
|
+
maxValue: this.maxTemp,
|
|
260
|
+
minStep: 0.5
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Cooling threshold (for HomeKit UI)
|
|
264
|
+
service.getCharacteristic(Characteristic.CoolingThresholdTemperature)
|
|
265
|
+
.setProps({
|
|
266
|
+
minValue: this.minTemp,
|
|
267
|
+
maxValue: this.maxTemp,
|
|
268
|
+
minStep: 0.5
|
|
269
|
+
})
|
|
270
|
+
.updateValue(this.minTemp);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
initMQTT() {
|
|
274
|
+
if (this.mqttClient) {
|
|
275
|
+
this.log.debug("MQTT client already initialized");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.log.info("Initializing MQTT connection...");
|
|
280
|
+
|
|
281
|
+
this.mqttClient = mqtt.connect('wss://mqtt.tesy.com:8083/mqtt', {
|
|
282
|
+
username: 'client1',
|
|
283
|
+
password: '123',
|
|
284
|
+
clientId: 'mqttjs_' + Math.random().toString(16).substr(2, 8),
|
|
285
|
+
protocol: 'wss',
|
|
286
|
+
reconnectPeriod: 5000,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
this.mqttClient.on('connect', () => {
|
|
290
|
+
this.log.info("✓ Connected to MQTT broker");
|
|
291
|
+
|
|
292
|
+
// Subscribe to all device response topics
|
|
293
|
+
Object.values(this.devices).forEach(device => {
|
|
294
|
+
const info = device.info;
|
|
295
|
+
const responseTopic = `v1/${info.mac}/response/${info.model}/${info.token}/#`;
|
|
296
|
+
this.mqttClient.subscribe(responseTopic, (err) => {
|
|
297
|
+
if (err) {
|
|
298
|
+
this.log.error("MQTT subscribe error for %s:", info.name, err);
|
|
299
|
+
} else {
|
|
300
|
+
this.log.debug("✓ Subscribed to MQTT topic for %s", info.name);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
this.mqttClient.on('error', (error) => {
|
|
307
|
+
this.log.error("MQTT Error:", error);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
this.mqttClient.on('close', () => {
|
|
311
|
+
this.log.warn("MQTT connection closed");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
this.mqttClient.on('offline', () => {
|
|
315
|
+
this.log.warn("MQTT client offline");
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
sendMQTTCommand(deviceInfo, command, payload, callback) {
|
|
320
|
+
if (!this.mqttClient || !this.mqttClient.connected) {
|
|
321
|
+
this.log.error("MQTT not connected");
|
|
322
|
+
return callback(new Error("MQTT not connected"));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const topic = `v1/${deviceInfo.mac}/request/${deviceInfo.model}/${deviceInfo.token}/${command}`;
|
|
326
|
+
const message = {
|
|
327
|
+
app_id: 'hb' + Math.random().toString(16).substr(2, 7),
|
|
328
|
+
...payload
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
this.log.debug("Sending MQTT command to %s: %s", deviceInfo.name, command);
|
|
332
|
+
|
|
333
|
+
this.mqttClient.publish(topic, JSON.stringify(message), (err) => {
|
|
334
|
+
if (err) {
|
|
335
|
+
this.log.error("MQTT publish error:", err);
|
|
336
|
+
callback(err);
|
|
337
|
+
} else {
|
|
338
|
+
callback(null);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
fetchDeviceStatus(deviceInfo, callback) {
|
|
344
|
+
const queryParams = querystring.stringify({
|
|
345
|
+
'userID': parseInt(this.userid),
|
|
346
|
+
'userEmail': this.username,
|
|
347
|
+
'userPass': this.password,
|
|
348
|
+
'lang': 'en'
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const options = {
|
|
352
|
+
'method': 'GET',
|
|
353
|
+
'url': 'https://ad.mytesy.com/rest/get-my-devices?' + queryParams
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
request(options, (error, response) => {
|
|
357
|
+
if (error) {
|
|
358
|
+
return callback(error, null);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const data = JSON.parse(response.body);
|
|
363
|
+
const deviceData = data[deviceInfo.mac];
|
|
364
|
+
|
|
365
|
+
if (!deviceData || !deviceData.state) {
|
|
366
|
+
return callback(new Error("Device not found"), null);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
callback(null, deviceData.state);
|
|
370
|
+
} catch(e) {
|
|
371
|
+
callback(e, null);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
startPolling() {
|
|
377
|
+
if (this.pollingInterval) {
|
|
378
|
+
clearInterval(this.pollingInterval);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.log.info("Starting status polling every %d ms", this.pullInterval);
|
|
382
|
+
|
|
383
|
+
this.pollingInterval = setInterval(() => {
|
|
384
|
+
this.updateAllDevices();
|
|
385
|
+
}, this.pullInterval);
|
|
386
|
+
|
|
387
|
+
// Initial update
|
|
388
|
+
this.updateAllDevices();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
updateAllDevices() {
|
|
392
|
+
Object.values(this.devices).forEach(device => {
|
|
393
|
+
this.fetchDeviceStatus(device.info, (error, status) => {
|
|
394
|
+
if (error) {
|
|
395
|
+
this.log.error("Error fetching status for %s:", device.info.name, error);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this.updateAccessoryStatus(device.accessory, status);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
updateAccessoryStatus(accessory, status) {
|
|
405
|
+
const service = accessory.getService(Service.HeaterCooler);
|
|
406
|
+
if (!service) return;
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
// Update current temperature
|
|
410
|
+
const currentTemp = parseFloat(status.current_temp);
|
|
411
|
+
if (!isNaN(currentTemp) && currentTemp >= this.minTemp && currentTemp <= this.maxTemp) {
|
|
412
|
+
const oldTemp = service.getCharacteristic(Characteristic.CurrentTemperature).value;
|
|
413
|
+
if (currentTemp !== oldTemp) {
|
|
414
|
+
service.getCharacteristic(Characteristic.CurrentTemperature).updateValue(currentTemp);
|
|
415
|
+
this.log.debug("%s: CurrentTemperature %s -> %s", accessory.displayName, oldTemp, currentTemp);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Update target temperature
|
|
420
|
+
const targetTemp = parseFloat(status.temp);
|
|
421
|
+
if (!isNaN(targetTemp) && targetTemp >= this.minTemp && targetTemp <= this.maxTemp) {
|
|
422
|
+
const oldTarget = service.getCharacteristic(Characteristic.HeatingThresholdTemperature).value;
|
|
423
|
+
if (targetTemp !== oldTarget) {
|
|
424
|
+
service.getCharacteristic(Characteristic.HeatingThresholdTemperature).updateValue(targetTemp);
|
|
425
|
+
this.log.debug("%s: HeatingThresholdTemperature %s -> %s", accessory.displayName, oldTarget, targetTemp);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Update active status
|
|
430
|
+
const isActive = status.status.toLowerCase() === 'on' ?
|
|
431
|
+
Characteristic.Active.ACTIVE :
|
|
432
|
+
Characteristic.Active.INACTIVE;
|
|
433
|
+
const oldActive = service.getCharacteristic(Characteristic.Active).value;
|
|
434
|
+
if (isActive !== oldActive) {
|
|
435
|
+
service.getCharacteristic(Characteristic.Active).updateValue(isActive);
|
|
436
|
+
this.log.info("%s: Active %s -> %s", accessory.displayName, oldActive ? 'ON' : 'OFF', isActive ? 'ON' : 'OFF');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Update heating state
|
|
440
|
+
const isHeating = status.heating === 'on';
|
|
441
|
+
const heatingState = isHeating ?
|
|
442
|
+
Characteristic.CurrentHeaterCoolerState.HEATING :
|
|
443
|
+
Characteristic.CurrentHeaterCoolerState.IDLE;
|
|
444
|
+
const oldState = service.getCharacteristic(Characteristic.CurrentHeaterCoolerState).value;
|
|
445
|
+
if (heatingState !== oldState) {
|
|
446
|
+
service.getCharacteristic(Characteristic.CurrentHeaterCoolerState).updateValue(heatingState);
|
|
447
|
+
this.log.debug("%s: Heating state %s -> %s", accessory.displayName, oldState, heatingState);
|
|
448
|
+
}
|
|
449
|
+
} catch(e) {
|
|
450
|
+
this.log.error("Error updating %s status:", accessory.displayName, e);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Characteristic Handlers
|
|
455
|
+
|
|
456
|
+
getActive(deviceInfo, callback) {
|
|
457
|
+
this.fetchDeviceStatus(deviceInfo, (error, status) => {
|
|
458
|
+
if (error) {
|
|
459
|
+
return callback(error);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const isActive = status.status.toLowerCase() === 'on' ?
|
|
463
|
+
Characteristic.Active.ACTIVE :
|
|
464
|
+
Characteristic.Active.INACTIVE;
|
|
465
|
+
callback(null, isActive);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
setActive(deviceInfo, value, callback) {
|
|
470
|
+
const newValue = value === 0 ? 'off' : 'on';
|
|
471
|
+
this.log.info("%s: Setting active to %s", deviceInfo.name, newValue);
|
|
472
|
+
|
|
473
|
+
this.sendMQTTCommand(deviceInfo, 'onOff', { status: newValue }, (error) => {
|
|
474
|
+
if (error) {
|
|
475
|
+
this.log.error("%s: Error setting active status:", deviceInfo.name, error);
|
|
476
|
+
return callback(error);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.log.info("%s: ✓ Active status changed to %s", deviceInfo.name, newValue);
|
|
480
|
+
callback(null);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
getCurrentTemperature(deviceInfo, callback) {
|
|
485
|
+
this.fetchDeviceStatus(deviceInfo, (error, status) => {
|
|
486
|
+
if (error) {
|
|
487
|
+
return callback(error);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const currentTemp = parseFloat(status.current_temp);
|
|
491
|
+
callback(null, currentTemp);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
getHeatingThresholdTemperature(deviceInfo, callback) {
|
|
496
|
+
this.fetchDeviceStatus(deviceInfo, (error, status) => {
|
|
497
|
+
if (error) {
|
|
498
|
+
return callback(error);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const targetTemp = parseFloat(status.temp);
|
|
502
|
+
callback(null, targetTemp);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
setHeatingThresholdTemperature(deviceInfo, value, callback) {
|
|
507
|
+
// Clamp value
|
|
508
|
+
if (value < this.minTemp) value = this.minTemp;
|
|
509
|
+
if (value > this.maxTemp) value = this.maxTemp;
|
|
510
|
+
|
|
511
|
+
this.log.info("%s: Setting target temperature to %s°C", deviceInfo.name, value);
|
|
512
|
+
|
|
513
|
+
// First set mode to manual
|
|
514
|
+
this.sendMQTTCommand(deviceInfo, 'setMode', { mode: 'manual' }, (error) => {
|
|
515
|
+
if (error) {
|
|
516
|
+
this.log.error("%s: Error setting mode to manual:", deviceInfo.name, error);
|
|
517
|
+
return callback(error);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
this.log.debug("%s: ✓ Mode set to manual", deviceInfo.name);
|
|
521
|
+
|
|
522
|
+
// Then set temperature
|
|
523
|
+
this.sendMQTTCommand(deviceInfo, 'setTemp', { temp: value }, (error) => {
|
|
524
|
+
if (error) {
|
|
525
|
+
this.log.error("%s: Error setting temperature:", deviceInfo.name, error);
|
|
526
|
+
return callback(error);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
this.log.info("%s: ✓ Temperature set to %s°C", deviceInfo.name, value);
|
|
530
|
+
callback(null);
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Export for testing
|
|
537
|
+
module.exports.TesyHeaterPlatform = TesyHeaterPlatform;
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homebridge-tesy-heater-mqtt",
|
|
3
|
+
"displayName": "Homebridge Tesy Heater MQTT",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Tesy heater plugin for Homebridge using MQTT control (API v4). Supports on/off control and temperature adjustment.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"homebridge-plugin",
|
|
9
|
+
"tesy",
|
|
10
|
+
"heater",
|
|
11
|
+
"convector",
|
|
12
|
+
"mqtt",
|
|
13
|
+
"smart-home",
|
|
14
|
+
"homekit"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": "^18.20.4 || ^20.15.1 || ^22 || ^24",
|
|
18
|
+
"homebridge": "^1.6.0 || ^2.0.0"
|
|
19
|
+
},
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Aleksey Molchanov",
|
|
22
|
+
"email": "molchanov.alex@gmail.com"
|
|
23
|
+
},
|
|
24
|
+
"contributors": [
|
|
25
|
+
{
|
|
26
|
+
"name": "Dobriyan Benov",
|
|
27
|
+
"url": "https://github.com/benov84/homebridge-tesy-heater"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/svjakrm/homebridge-tesy-heater-mqtt.git"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"homebridge-http-base": "2.1.6",
|
|
36
|
+
"mqtt": "^5.14.1",
|
|
37
|
+
"request": "^2.65.0"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/svjakrm/homebridge-tesy-heater-mqtt/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/svjakrm/homebridge-tesy-heater-mqtt#readme",
|
|
43
|
+
"main": "index.js",
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "jest",
|
|
46
|
+
"test:coverage": "jest --coverage"
|
|
47
|
+
},
|
|
48
|
+
"funding": [
|
|
49
|
+
{
|
|
50
|
+
"type": "revolut",
|
|
51
|
+
"url": "https://revolut.me/molchaoxez"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/jest": "^30.0.0",
|
|
56
|
+
"jest": "^30.2.0"
|
|
57
|
+
}
|
|
58
|
+
}
|