homebridge-neakasa 1.1.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/LICENSE +21 -0
- package/README.md +157 -0
- package/config.schema.json +113 -0
- package/dist/accessory.d.ts +38 -0
- package/dist/accessory.d.ts.map +1 -0
- package/dist/accessory.js +322 -0
- package/dist/accessory.js.map +1 -0
- package/dist/api.d.ts +42 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +439 -0
- package/dist/api.js.map +1 -0
- package/dist/client.d.ts +29 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +152 -0
- package/dist/client.js.map +1 -0
- package/dist/encryption.d.ts +17 -0
- package/dist/encryption.d.ts.map +1 -0
- package/dist/encryption.js +108 -0
- package/dist/encryption.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +20 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +158 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.d.ts +3 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +6 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Homebridge Neakasa Plugin Contributors
|
|
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,157 @@
|
|
|
1
|
+
# Homebridge Neakasa Plugin
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/homebridge-neakasa)
|
|
4
|
+
[](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
|
|
5
|
+
|
|
6
|
+
Homebridge plugin for Neakasa M1 Cat Litter Box. This plugin allows you to monitor and control your Neakasa litter box through Apple HomeKit.
|
|
7
|
+
|
|
8
|
+
This is a TypeScript/Node.js conversion of the [Home Assistant Neakasa Integration](https://github.com/timniklas/hass-neakasa) by [@timniklas](https://github.com/timniklas).
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
### Sensors
|
|
13
|
+
- **Litter Level** - Shows sand/litter level percentage with low indicator
|
|
14
|
+
- **Bin Full** - Binary sensor indicating when the waste bin is full
|
|
15
|
+
|
|
16
|
+
### Switches
|
|
17
|
+
- **Auto Clean** - Enable/disable automatic cleaning
|
|
18
|
+
- **Child Lock** - Enable/disable child safety lock
|
|
19
|
+
- **Auto Cover** - Enable/disable automatic litter coverage
|
|
20
|
+
- **Auto Leveling** - Enable/disable automatic sand leveling
|
|
21
|
+
- **Silent Mode** - Enable/disable quiet operation mode
|
|
22
|
+
- **Unstoppable Cycle** - Enable/disable uninterruptible cleaning cycles
|
|
23
|
+
|
|
24
|
+
### Buttons (Stateless Switches)
|
|
25
|
+
- **Clean Now** - Trigger an immediate cleaning cycle
|
|
26
|
+
- **Level Now** - Trigger an immediate sand leveling cycle
|
|
27
|
+
|
|
28
|
+
## Supported Devices
|
|
29
|
+
|
|
30
|
+
- Neakasa M1 Cat Litter Box
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
### Option 1: Homebridge UI (Recommended)
|
|
35
|
+
|
|
36
|
+
1. Search for "Neakasa" in the Homebridge Config UI X plugin search
|
|
37
|
+
2. Click Install
|
|
38
|
+
3. Configure your credentials in the plugin settings
|
|
39
|
+
4. Restart Homebridge
|
|
40
|
+
|
|
41
|
+
### Option 2: Manual Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g homebridge-neakasa
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Add the following to your Homebridge `config.json`:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"platforms": [
|
|
54
|
+
{
|
|
55
|
+
"platform": "Neakasa",
|
|
56
|
+
"name": "Neakasa",
|
|
57
|
+
"username": "your@email.com",
|
|
58
|
+
"password": "your_password",
|
|
59
|
+
"pollInterval": 60,
|
|
60
|
+
"debug": false
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Configuration Options
|
|
67
|
+
|
|
68
|
+
| Option | Required | Default | Description |
|
|
69
|
+
|--------|----------|---------|-------------|
|
|
70
|
+
| `platform` | Yes | `"Neakasa"` | Must be "Neakasa" |
|
|
71
|
+
| `name` | Yes | `"Neakasa"` | Name for the platform |
|
|
72
|
+
| `username` | Yes | - | Your Neakasa account email |
|
|
73
|
+
| `password` | Yes | - | Your Neakasa account password |
|
|
74
|
+
| `pollInterval` | No | `60` | How often to check for updates (in seconds) |
|
|
75
|
+
| `debug` | No | `false` | Enable debug logging |
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
Once configured, your Neakasa litter box will appear in the Home app as an accessory with multiple controls:
|
|
80
|
+
|
|
81
|
+
### Monitoring
|
|
82
|
+
- **Litter Level**: Check the current litter level percentage
|
|
83
|
+
- **Bin Status**: See if the waste bin needs to be emptied
|
|
84
|
+
|
|
85
|
+
### Controls
|
|
86
|
+
- Turn on/off various automation features through switches
|
|
87
|
+
- Trigger manual cleaning or leveling cycles
|
|
88
|
+
|
|
89
|
+
### Automation Ideas
|
|
90
|
+
- Get notified when the bin is full
|
|
91
|
+
- Get notified when litter is low
|
|
92
|
+
- Automatically turn on silent mode at night
|
|
93
|
+
- Create scenes that include litter box settings
|
|
94
|
+
|
|
95
|
+
## Troubleshooting
|
|
96
|
+
|
|
97
|
+
### "Failed to connect" errors
|
|
98
|
+
|
|
99
|
+
1. Verify your username and password are correct
|
|
100
|
+
2. Make sure you can log into the Neakasa mobile app with the same credentials
|
|
101
|
+
3. Check your internet connection
|
|
102
|
+
4. Enable debug logging to see detailed error messages
|
|
103
|
+
|
|
104
|
+
### Devices not appearing
|
|
105
|
+
|
|
106
|
+
1. Make sure your litter box is online in the Neakasa app
|
|
107
|
+
2. Restart Homebridge after adding credentials
|
|
108
|
+
3. Check the Homebridge logs for any errors
|
|
109
|
+
4. Try removing and re-adding the platform configuration
|
|
110
|
+
|
|
111
|
+
### State not updating
|
|
112
|
+
|
|
113
|
+
1. Check the `pollInterval` setting - increase it if you're seeing rate limit errors
|
|
114
|
+
2. Verify the device is online in the Neakasa mobile app
|
|
115
|
+
3. Restart Homebridge
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Clone the repository
|
|
121
|
+
git clone https://github.com/YOUR_USERNAME/homebridge-neakasa.git
|
|
122
|
+
cd homebridge-neakasa
|
|
123
|
+
|
|
124
|
+
# Install dependencies
|
|
125
|
+
npm install
|
|
126
|
+
|
|
127
|
+
# Build the plugin
|
|
128
|
+
npm run build
|
|
129
|
+
|
|
130
|
+
# Link for local testing
|
|
131
|
+
npm link
|
|
132
|
+
|
|
133
|
+
# Watch for changes
|
|
134
|
+
npm run watch
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Credits
|
|
138
|
+
|
|
139
|
+
- Original Home Assistant integration by [@timniklas](https://github.com/timniklas) - [hass-neakasa](https://github.com/timniklas/hass-neakasa)
|
|
140
|
+
- Converted to Homebridge plugin by Claude AI
|
|
141
|
+
|
|
142
|
+
## Disclaimer
|
|
143
|
+
|
|
144
|
+
This plugin is not officially endorsed or supported by Neakasa. Use at your own risk and ensure you comply with all relevant terms of service and privacy policies.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
149
|
+
|
|
150
|
+
## Support
|
|
151
|
+
|
|
152
|
+
If you find this plugin helpful, please consider:
|
|
153
|
+
- Starring the repository on GitHub
|
|
154
|
+
- Reporting issues and bugs
|
|
155
|
+
- Contributing improvements
|
|
156
|
+
|
|
157
|
+
For issues and feature requests, please use the [GitHub Issues](https://github.com/YOUR_USERNAME/homebridge-neakasa/issues) page.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "Neakasa",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"schema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"name": {
|
|
9
|
+
"title": "Name",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"required": true,
|
|
12
|
+
"default": "Neakasa"
|
|
13
|
+
},
|
|
14
|
+
"username": {
|
|
15
|
+
"title": "Email/Username",
|
|
16
|
+
"type": "string",
|
|
17
|
+
"required": true,
|
|
18
|
+
"placeholder": "your@email.com"
|
|
19
|
+
},
|
|
20
|
+
"password": {
|
|
21
|
+
"title": "Password",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"required": true
|
|
24
|
+
},
|
|
25
|
+
"pollInterval": {
|
|
26
|
+
"title": "Poll Interval (seconds)",
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"default": 60,
|
|
29
|
+
"minimum": 30,
|
|
30
|
+
"description": "How often to check for updates from the Neakasa cloud"
|
|
31
|
+
},
|
|
32
|
+
"deviceName": {
|
|
33
|
+
"title": "Device Name",
|
|
34
|
+
"type": "string",
|
|
35
|
+
"default": "Neakasa M1",
|
|
36
|
+
"description": "Display name for the device in HomeKit"
|
|
37
|
+
},
|
|
38
|
+
"debug": {
|
|
39
|
+
"title": "Enable Debug Logging",
|
|
40
|
+
"type": "boolean",
|
|
41
|
+
"default": false
|
|
42
|
+
},
|
|
43
|
+
"showStatusSensor": {
|
|
44
|
+
"title": "Show Status Sensor",
|
|
45
|
+
"type": "boolean",
|
|
46
|
+
"default": true,
|
|
47
|
+
"description": "Show a contact sensor indicating the current device status (Idle, Cleaning, etc.)"
|
|
48
|
+
},
|
|
49
|
+
"showBinStateSensor": {
|
|
50
|
+
"title": "Show Bin State Sensor",
|
|
51
|
+
"type": "boolean",
|
|
52
|
+
"default": true,
|
|
53
|
+
"description": "Show a leak sensor indicating waste bin state (Normal, Full, Missing)"
|
|
54
|
+
},
|
|
55
|
+
"showWifiSensor": {
|
|
56
|
+
"title": "Show WiFi Signal Sensor",
|
|
57
|
+
"type": "boolean",
|
|
58
|
+
"default": false,
|
|
59
|
+
"description": "Show a humidity sensor repurposed to display WiFi signal strength"
|
|
60
|
+
},
|
|
61
|
+
"showCatSensors": {
|
|
62
|
+
"title": "Show Cat Weight Sensors",
|
|
63
|
+
"type": "boolean",
|
|
64
|
+
"default": true,
|
|
65
|
+
"description": "Show per-cat weight sensors (one per registered cat)"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"layout": [
|
|
70
|
+
{
|
|
71
|
+
"type": "flex",
|
|
72
|
+
"flex-flow": "row wrap",
|
|
73
|
+
"items": [
|
|
74
|
+
{
|
|
75
|
+
"key": "username",
|
|
76
|
+
"flex": "1 1 50%"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"key": "password",
|
|
80
|
+
"flex": "1 1 50%"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"type": "flex",
|
|
86
|
+
"flex-flow": "row wrap",
|
|
87
|
+
"items": [
|
|
88
|
+
{
|
|
89
|
+
"key": "deviceName",
|
|
90
|
+
"flex": "1 1 50%"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"key": "pollInterval",
|
|
94
|
+
"flex": "1 1 50%"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "fieldset",
|
|
100
|
+
"title": "Optional Sensors",
|
|
101
|
+
"expandable": true,
|
|
102
|
+
"items": [
|
|
103
|
+
"showStatusSensor",
|
|
104
|
+
"showBinStateSensor",
|
|
105
|
+
"showWifiSensor",
|
|
106
|
+
"showCatSensors"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"key": "debug"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PlatformAccessory, CharacteristicValue } from 'homebridge';
|
|
2
|
+
import { NeakasaPlatform } from './platform';
|
|
3
|
+
import { DeviceData, NeakasaPlatformConfig } from './types';
|
|
4
|
+
export declare class NeakasaAccessory {
|
|
5
|
+
private readonly platform;
|
|
6
|
+
private readonly accessory;
|
|
7
|
+
private readonly iotId;
|
|
8
|
+
private readonly deviceName;
|
|
9
|
+
private services;
|
|
10
|
+
private deviceData?;
|
|
11
|
+
private readonly config;
|
|
12
|
+
constructor(platform: NeakasaPlatform, accessory: PlatformAccessory, iotId: string, deviceName: string, config: NeakasaPlatformConfig);
|
|
13
|
+
private setupServices;
|
|
14
|
+
private addSwitch;
|
|
15
|
+
private removeServiceIfExists;
|
|
16
|
+
private rssiToPercent;
|
|
17
|
+
updateData(data: DeviceData): Promise<void>;
|
|
18
|
+
private updateCatSensors;
|
|
19
|
+
setAutoClean(value: CharacteristicValue): Promise<void>;
|
|
20
|
+
getAutoClean(): Promise<CharacteristicValue>;
|
|
21
|
+
setChildLock(value: CharacteristicValue): Promise<void>;
|
|
22
|
+
getChildLock(): Promise<CharacteristicValue>;
|
|
23
|
+
setAutoBury(value: CharacteristicValue): Promise<void>;
|
|
24
|
+
getAutoBury(): Promise<CharacteristicValue>;
|
|
25
|
+
setAutoLevel(value: CharacteristicValue): Promise<void>;
|
|
26
|
+
getAutoLevel(): Promise<CharacteristicValue>;
|
|
27
|
+
setSilentMode(value: CharacteristicValue): Promise<void>;
|
|
28
|
+
getSilentMode(): Promise<CharacteristicValue>;
|
|
29
|
+
setUnstoppableCycle(value: CharacteristicValue): Promise<void>;
|
|
30
|
+
getUnstoppableCycle(): Promise<CharacteristicValue>;
|
|
31
|
+
setAutoRecovery(value: CharacteristicValue): Promise<void>;
|
|
32
|
+
getAutoRecovery(): Promise<CharacteristicValue>;
|
|
33
|
+
setYoungCatMode(value: CharacteristicValue): Promise<void>;
|
|
34
|
+
getYoungCatMode(): Promise<CharacteristicValue>;
|
|
35
|
+
cleanNow(value: CharacteristicValue): Promise<void>;
|
|
36
|
+
levelNow(value: CharacteristicValue): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=accessory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessory.d.ts","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAqC,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAE/F,qBAAa,gBAAgB;IAMzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAR7B,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;gBAG5B,QAAQ,EAAE,eAAe,EACzB,SAAS,EAAE,iBAAiB,EAC5B,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EACnC,MAAM,EAAE,qBAAqB;IAa/B,OAAO,CAAC,aAAa;IAsFrB,OAAO,CAAC,SAAS;IAgBjB,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,aAAa;IAWf,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA8EjD,OAAO,CAAC,gBAAgB;IAqClB,YAAY,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAavD,YAAY,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAI5C,YAAY,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD,YAAY,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAI5C,WAAW,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWtD,WAAW,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAI3C,YAAY,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD,YAAY,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAI5C,aAAa,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWxD,aAAa,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAI7C,mBAAmB,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAW9D,mBAAmB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAInD,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1D,eAAe,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAI/C,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1D,eAAe,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAI/C,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAenD,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;CAc1D"}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NeakasaAccessory = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
class NeakasaAccessory {
|
|
6
|
+
constructor(platform, accessory, iotId, deviceName, config) {
|
|
7
|
+
this.platform = platform;
|
|
8
|
+
this.accessory = accessory;
|
|
9
|
+
this.iotId = iotId;
|
|
10
|
+
this.deviceName = deviceName;
|
|
11
|
+
this.services = new Map();
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.accessory.getService(this.platform.Service.AccessoryInformation)
|
|
14
|
+
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Neakasa')
|
|
15
|
+
.setCharacteristic(this.platform.Characteristic.Model, 'M1')
|
|
16
|
+
.setCharacteristic(this.platform.Characteristic.SerialNumber, iotId);
|
|
17
|
+
this.setupServices();
|
|
18
|
+
}
|
|
19
|
+
setupServices() {
|
|
20
|
+
const filterService = this.accessory.getService(this.platform.Service.FilterMaintenance) ||
|
|
21
|
+
this.accessory.addService(this.platform.Service.FilterMaintenance, 'Litter Level', 'sand-level');
|
|
22
|
+
filterService.setCharacteristic(this.platform.Characteristic.Name, 'Litter Level');
|
|
23
|
+
this.services.set('filter', filterService);
|
|
24
|
+
const binSensor = this.accessory.getService('bin-full') ||
|
|
25
|
+
this.accessory.addService(this.platform.Service.OccupancySensor, 'Waste Bin Full', 'bin-full');
|
|
26
|
+
binSensor.setCharacteristic(this.platform.Characteristic.Name, 'Waste Bin Full');
|
|
27
|
+
this.services.set('binFull', binSensor);
|
|
28
|
+
this.addSwitch('autoClean', 'Auto Clean', 'auto-clean', this.setAutoClean, this.getAutoClean);
|
|
29
|
+
this.addSwitch('childLock', 'Child Lock', 'child-lock', this.setChildLock, this.getChildLock);
|
|
30
|
+
this.addSwitch('autoBury', 'Auto Bury', 'auto-bury', this.setAutoBury, this.getAutoBury);
|
|
31
|
+
this.addSwitch('autoLevel', 'Auto Level', 'auto-level', this.setAutoLevel, this.getAutoLevel);
|
|
32
|
+
this.addSwitch('silentMode', 'Silent Mode', 'silent-mode', this.setSilentMode, this.getSilentMode);
|
|
33
|
+
this.addSwitch('unstoppable', 'Unstoppable Cycle', 'unstoppable-cycle', this.setUnstoppableCycle, this.getUnstoppableCycle);
|
|
34
|
+
this.addSwitch('autoRecovery', 'Auto Recovery', 'auto-recovery', this.setAutoRecovery, this.getAutoRecovery);
|
|
35
|
+
this.addSwitch('youngCatMode', 'Young Cat Mode', 'young-cat-mode', this.setYoungCatMode, this.getYoungCatMode);
|
|
36
|
+
const cleanSwitch = this.accessory.getService('clean-now') ||
|
|
37
|
+
this.accessory.addService(this.platform.Service.Switch, 'Clean Now', 'clean-now');
|
|
38
|
+
cleanSwitch.setCharacteristic(this.platform.Characteristic.Name, 'Clean Now');
|
|
39
|
+
cleanSwitch.getCharacteristic(this.platform.Characteristic.On)
|
|
40
|
+
.onSet(this.cleanNow.bind(this))
|
|
41
|
+
.onGet(() => false);
|
|
42
|
+
this.services.set('clean', cleanSwitch);
|
|
43
|
+
const levelSwitch = this.accessory.getService('level-now') ||
|
|
44
|
+
this.accessory.addService(this.platform.Service.Switch, 'Level Now', 'level-now');
|
|
45
|
+
levelSwitch.setCharacteristic(this.platform.Characteristic.Name, 'Level Now');
|
|
46
|
+
levelSwitch.getCharacteristic(this.platform.Characteristic.On)
|
|
47
|
+
.onSet(this.levelNow.bind(this))
|
|
48
|
+
.onGet(() => false);
|
|
49
|
+
this.services.set('level', levelSwitch);
|
|
50
|
+
if (this.config.showStatusSensor !== false) {
|
|
51
|
+
const statusSensor = this.accessory.getService('device-status') ||
|
|
52
|
+
this.accessory.addService(this.platform.Service.ContactSensor, 'Status', 'device-status');
|
|
53
|
+
statusSensor.setCharacteristic(this.platform.Characteristic.Name, 'Status');
|
|
54
|
+
this.services.set('status', statusSensor);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.removeServiceIfExists('device-status');
|
|
58
|
+
}
|
|
59
|
+
if (this.config.showBinStateSensor !== false) {
|
|
60
|
+
const binStateSensor = this.accessory.getService('bin-state') ||
|
|
61
|
+
this.accessory.addService(this.platform.Service.LeakSensor, 'Bin State', 'bin-state');
|
|
62
|
+
binStateSensor.setCharacteristic(this.platform.Characteristic.Name, 'Bin State');
|
|
63
|
+
this.services.set('binState', binStateSensor);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.removeServiceIfExists('bin-state');
|
|
67
|
+
}
|
|
68
|
+
if (this.config.showWifiSensor === true) {
|
|
69
|
+
const wifiSensor = this.accessory.getService('wifi-signal') ||
|
|
70
|
+
this.accessory.addService(this.platform.Service.HumiditySensor, 'WiFi Signal', 'wifi-signal');
|
|
71
|
+
wifiSensor.setCharacteristic(this.platform.Characteristic.Name, 'WiFi Signal');
|
|
72
|
+
this.services.set('wifi', wifiSensor);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
this.removeServiceIfExists('wifi-signal');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
addSwitch(key, name, subType, setter, getter) {
|
|
79
|
+
const service = this.accessory.getService(subType) ||
|
|
80
|
+
this.accessory.addService(this.platform.Service.Switch, name, subType);
|
|
81
|
+
service.setCharacteristic(this.platform.Characteristic.Name, name);
|
|
82
|
+
service.getCharacteristic(this.platform.Characteristic.On)
|
|
83
|
+
.onSet(setter.bind(this))
|
|
84
|
+
.onGet(getter.bind(this));
|
|
85
|
+
this.services.set(key, service);
|
|
86
|
+
}
|
|
87
|
+
removeServiceIfExists(subType) {
|
|
88
|
+
const existing = this.accessory.getService(subType);
|
|
89
|
+
if (existing) {
|
|
90
|
+
this.accessory.removeService(existing);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
rssiToPercent(rssi) {
|
|
94
|
+
if (rssi >= -50) {
|
|
95
|
+
return 100;
|
|
96
|
+
}
|
|
97
|
+
if (rssi <= -100) {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
return 2 * (rssi + 100);
|
|
101
|
+
}
|
|
102
|
+
async updateData(data) {
|
|
103
|
+
this.deviceData = data;
|
|
104
|
+
const filterService = this.services.get('filter');
|
|
105
|
+
const changeIndication = data.sandLevelState === types_1.SandLevel.INSUFFICIENT ?
|
|
106
|
+
this.platform.Characteristic.FilterChangeIndication.CHANGE_FILTER :
|
|
107
|
+
this.platform.Characteristic.FilterChangeIndication.FILTER_OK;
|
|
108
|
+
filterService.updateCharacteristic(this.platform.Characteristic.FilterChangeIndication, changeIndication);
|
|
109
|
+
filterService.updateCharacteristic(this.platform.Characteristic.FilterLifeLevel, data.sandLevelPercent);
|
|
110
|
+
const binSensor = this.services.get('binFull');
|
|
111
|
+
binSensor.updateCharacteristic(this.platform.Characteristic.OccupancyDetected, data.binFullWaitReset ?
|
|
112
|
+
this.platform.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED :
|
|
113
|
+
this.platform.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED);
|
|
114
|
+
this.services.get('autoClean').updateCharacteristic(this.platform.Characteristic.On, data.cleanCfg?.active === 1);
|
|
115
|
+
this.services.get('childLock').updateCharacteristic(this.platform.Characteristic.On, data.childLockOnOff);
|
|
116
|
+
this.services.get('autoBury').updateCharacteristic(this.platform.Characteristic.On, data.autoBury);
|
|
117
|
+
this.services.get('autoLevel').updateCharacteristic(this.platform.Characteristic.On, data.autoLevel);
|
|
118
|
+
this.services.get('silentMode').updateCharacteristic(this.platform.Characteristic.On, data.silentMode);
|
|
119
|
+
this.services.get('unstoppable').updateCharacteristic(this.platform.Characteristic.On, data.bIntrptRangeDet);
|
|
120
|
+
this.services.get('autoRecovery').updateCharacteristic(this.platform.Characteristic.On, data.autoForceInit);
|
|
121
|
+
this.services.get('youngCatMode').updateCharacteristic(this.platform.Characteristic.On, data.youngCatMode);
|
|
122
|
+
const statusSensor = this.services.get('status');
|
|
123
|
+
if (statusSensor) {
|
|
124
|
+
const isActive = data.bucketStatus !== 0;
|
|
125
|
+
statusSensor.updateCharacteristic(this.platform.Characteristic.ContactSensorState, isActive ?
|
|
126
|
+
this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED :
|
|
127
|
+
this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED);
|
|
128
|
+
const statusName = types_1.BucketStatus[data.bucketStatus] || `Unknown (${data.bucketStatus})`;
|
|
129
|
+
statusSensor.updateCharacteristic(this.platform.Characteristic.Name, statusName);
|
|
130
|
+
}
|
|
131
|
+
const binStateSensor = this.services.get('binState');
|
|
132
|
+
if (binStateSensor) {
|
|
133
|
+
const leakDetected = data.room_of_bin !== 0;
|
|
134
|
+
binStateSensor.updateCharacteristic(this.platform.Characteristic.LeakDetected, leakDetected ?
|
|
135
|
+
this.platform.Characteristic.LeakDetected.LEAK_DETECTED :
|
|
136
|
+
this.platform.Characteristic.LeakDetected.LEAK_NOT_DETECTED);
|
|
137
|
+
const binStateName = types_1.BinState[data.room_of_bin] || `Unknown (${data.room_of_bin})`;
|
|
138
|
+
binStateSensor.updateCharacteristic(this.platform.Characteristic.Name, binStateName);
|
|
139
|
+
}
|
|
140
|
+
const wifiSensor = this.services.get('wifi');
|
|
141
|
+
if (wifiSensor) {
|
|
142
|
+
const signalPercent = this.rssiToPercent(data.wifiRssi);
|
|
143
|
+
wifiSensor.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, signalPercent);
|
|
144
|
+
}
|
|
145
|
+
if (this.config.showCatSensors !== false && data.cat_list && data.cat_list.length > 0) {
|
|
146
|
+
this.updateCatSensors(data);
|
|
147
|
+
}
|
|
148
|
+
this.platform.log.debug(`Updated ${this.deviceName}: Status=${types_1.BucketStatus[data.bucketStatus] || data.bucketStatus}, ` +
|
|
149
|
+
`Sand=${data.sandLevelPercent}%, Bin=${types_1.BinState[data.room_of_bin] || data.room_of_bin}`);
|
|
150
|
+
}
|
|
151
|
+
updateCatSensors(data) {
|
|
152
|
+
for (const cat of data.cat_list) {
|
|
153
|
+
const subType = `cat-${cat.id}`;
|
|
154
|
+
let catSensor = this.services.get(subType);
|
|
155
|
+
if (!catSensor) {
|
|
156
|
+
const existingService = this.accessory.getService(subType);
|
|
157
|
+
if (existingService) {
|
|
158
|
+
catSensor = existingService;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
catSensor = this.accessory.addService(this.platform.Service.HumiditySensor, cat.name, subType);
|
|
162
|
+
}
|
|
163
|
+
catSensor.setCharacteristic(this.platform.Characteristic.Name, cat.name);
|
|
164
|
+
this.services.set(subType, catSensor);
|
|
165
|
+
}
|
|
166
|
+
const catRecords = data.record_list
|
|
167
|
+
.filter(r => r.cat_id === cat.id)
|
|
168
|
+
.sort((a, b) => b.end_time - a.end_time);
|
|
169
|
+
if (catRecords.length > 0) {
|
|
170
|
+
const latestRecord = catRecords[0];
|
|
171
|
+
const weight = Math.min(100, Math.max(0, latestRecord.weight));
|
|
172
|
+
catSensor.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, weight);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async setAutoClean(value) {
|
|
177
|
+
const newValue = value;
|
|
178
|
+
try {
|
|
179
|
+
const cleanCfg = this.deviceData?.cleanCfg || { active: 0 };
|
|
180
|
+
cleanCfg.active = newValue ? 1 : 0;
|
|
181
|
+
await this.platform.neakasaApi.setDeviceProperties(this.iotId, { cleanCfg });
|
|
182
|
+
this.platform.log.info(`Set Auto Clean to ${newValue}`);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
this.platform.log.error(`Failed to set Auto Clean: ${error}`);
|
|
186
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async getAutoClean() {
|
|
190
|
+
return this.deviceData?.cleanCfg?.active === 1;
|
|
191
|
+
}
|
|
192
|
+
async setChildLock(value) {
|
|
193
|
+
const newValue = value;
|
|
194
|
+
try {
|
|
195
|
+
await this.platform.neakasaApi.setDeviceProperties(this.iotId, { childLockOnOff: newValue ? 1 : 0 });
|
|
196
|
+
this.platform.log.info(`Set Child Lock to ${newValue}`);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
this.platform.log.error(`Failed to set Child Lock: ${error}`);
|
|
200
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async getChildLock() {
|
|
204
|
+
return this.deviceData?.childLockOnOff || false;
|
|
205
|
+
}
|
|
206
|
+
async setAutoBury(value) {
|
|
207
|
+
const newValue = value;
|
|
208
|
+
try {
|
|
209
|
+
await this.platform.neakasaApi.setDeviceProperties(this.iotId, { autoBury: newValue ? 1 : 0 });
|
|
210
|
+
this.platform.log.info(`Set Auto Bury to ${newValue}`);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
this.platform.log.error(`Failed to set Auto Bury: ${error}`);
|
|
214
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async getAutoBury() {
|
|
218
|
+
return this.deviceData?.autoBury || false;
|
|
219
|
+
}
|
|
220
|
+
async setAutoLevel(value) {
|
|
221
|
+
const newValue = value;
|
|
222
|
+
try {
|
|
223
|
+
await this.platform.neakasaApi.setDeviceProperties(this.iotId, { autoLevel: newValue ? 1 : 0 });
|
|
224
|
+
this.platform.log.info(`Set Auto Level to ${newValue}`);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
this.platform.log.error(`Failed to set Auto Level: ${error}`);
|
|
228
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async getAutoLevel() {
|
|
232
|
+
return this.deviceData?.autoLevel || false;
|
|
233
|
+
}
|
|
234
|
+
async setSilentMode(value) {
|
|
235
|
+
const newValue = value;
|
|
236
|
+
try {
|
|
237
|
+
await this.platform.neakasaApi.setDeviceProperties(this.iotId, { silentMode: newValue ? 1 : 0 });
|
|
238
|
+
this.platform.log.info(`Set Silent Mode to ${newValue}`);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
this.platform.log.error(`Failed to set Silent Mode: ${error}`);
|
|
242
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async getSilentMode() {
|
|
246
|
+
return this.deviceData?.silentMode || false;
|
|
247
|
+
}
|
|
248
|
+
async setUnstoppableCycle(value) {
|
|
249
|
+
const newValue = value;
|
|
250
|
+
try {
|
|
251
|
+
await this.platform.neakasaApi.setDeviceProperties(this.iotId, { bIntrptRangeDet: newValue ? 1 : 0 });
|
|
252
|
+
this.platform.log.info(`Set Unstoppable Cycle to ${newValue}`);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
this.platform.log.error(`Failed to set Unstoppable Cycle: ${error}`);
|
|
256
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async getUnstoppableCycle() {
|
|
260
|
+
return this.deviceData?.bIntrptRangeDet || false;
|
|
261
|
+
}
|
|
262
|
+
async setAutoRecovery(value) {
|
|
263
|
+
const newValue = value;
|
|
264
|
+
try {
|
|
265
|
+
await this.platform.neakasaApi.setDeviceProperties(this.iotId, { autoForceInit: newValue ? 1 : 0 });
|
|
266
|
+
this.platform.log.info(`Set Auto Recovery to ${newValue}`);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
this.platform.log.error(`Failed to set Auto Recovery: ${error}`);
|
|
270
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async getAutoRecovery() {
|
|
274
|
+
return this.deviceData?.autoForceInit || false;
|
|
275
|
+
}
|
|
276
|
+
async setYoungCatMode(value) {
|
|
277
|
+
const newValue = value;
|
|
278
|
+
try {
|
|
279
|
+
await this.platform.neakasaApi.setDeviceProperties(this.iotId, { youngCatMode: newValue ? 1 : 0 });
|
|
280
|
+
this.platform.log.info(`Set Young Cat Mode to ${newValue}`);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
this.platform.log.error(`Failed to set Young Cat Mode: ${error}`);
|
|
284
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async getYoungCatMode() {
|
|
288
|
+
return this.deviceData?.youngCatMode || false;
|
|
289
|
+
}
|
|
290
|
+
async cleanNow(value) {
|
|
291
|
+
if (value) {
|
|
292
|
+
try {
|
|
293
|
+
await this.platform.neakasaApi.cleanNow(this.iotId);
|
|
294
|
+
this.platform.log.info(`Triggered clean for ${this.deviceName}`);
|
|
295
|
+
setTimeout(() => {
|
|
296
|
+
this.services.get('clean').updateCharacteristic(this.platform.Characteristic.On, false);
|
|
297
|
+
}, 1000);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
this.platform.log.error(`Failed to trigger clean: ${error}`);
|
|
301
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async levelNow(value) {
|
|
306
|
+
if (value) {
|
|
307
|
+
try {
|
|
308
|
+
await this.platform.neakasaApi.sandLeveling(this.iotId);
|
|
309
|
+
this.platform.log.info(`Triggered leveling for ${this.deviceName}`);
|
|
310
|
+
setTimeout(() => {
|
|
311
|
+
this.services.get('level').updateCharacteristic(this.platform.Characteristic.On, false);
|
|
312
|
+
}, 1000);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
this.platform.log.error(`Failed to trigger leveling: ${error}`);
|
|
316
|
+
throw new this.platform.api.hap.HapStatusError(-70402);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
exports.NeakasaAccessory = NeakasaAccessory;
|
|
322
|
+
//# sourceMappingURL=accessory.js.map
|