Skip to content

Commit 4f03af7

Browse files
authored
feat: add Cat Toilet (msp) accessory support (#551)
Add CatToiletAccessory for Tuya 'msp' category devices (smart cat toilets). Exposes: - Power, auto clean, manual clean, deodorization, UV switches - Mood light as Lightbulb service - OccupancySensor during active cleaning/UV/deodorization - FilterMaintenance when garbage box is full - StatusFault for motor/program/sensor faults All services are optional - gracefully skips unsupported DPs.
1 parent aca5712 commit 4f03af7

3 files changed

Lines changed: 154 additions & 1 deletion

File tree

SUPPORTED_DEVICES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Most category code is pinyin abbreviation of Chinese name.
8484
| Sofa | 沙发 | sf | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysf?id=Kaiuz2fp9uqtt) |
8585
| Electric Fireplace | 电壁炉 | dbl | | | [Documentation](https://developer.tuya.com/en/docs/iot/electric-fireplace?id=Kaiuz2hz4iyp6) |
8686
| Smart Milk Kettle | 智能调奶器 | tnq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorytnq?id=Kakf01agbfkfa) |
87-
| Cat Toilet | 猫砂盆 | msp | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymsp?id=Kakg2t7714ky7) |
87+
| Cat Toilet | 猫砂盆 | msp | Switch, Lightbulb, OccupancySensor, FilterMaintenance | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymsp?id=Kakg2t7714ky7) |
8888
| Towel Rack | 毛巾架 | mjj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymjj?id=Kakkmlm9k4cir) |
8989
| Smart Indoor Garden | 植物生长机 | sz | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0) |
9090

src/accessory/AccessoryFactory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import VibrationSensorAccessory from './VibrationSensorAccessory';
4343
import WeatherStationAccessory from './WeatherStationAccessory';
4444
import DoorbellAccessory from './DoorbellAccessory';
4545
import PetFeederAccessory from './PetFeederAccessory';
46+
import CatToiletAccessory from './CatToiletAccessory';
4647
import WhiteNoiseLightAccessory from './WhiteNoiseLightAccessory';
4748

4849

@@ -122,6 +123,9 @@ export default class AccessoryFactory {
122123
case 'cwwsq':
123124
handler = new PetFeederAccessory(platform, accessory);
124125
break;
126+
case 'msp':
127+
handler = new CatToiletAccessory(platform, accessory);
128+
break;
125129
case 'mc':
126130
handler = new WindowAccessory(platform, accessory);
127131
break;
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import BaseAccessory from './BaseAccessory';
2+
import { configureName } from './characteristic/Name';
3+
import { configureOn } from './characteristic/On';
4+
5+
const SCHEMA_CODE = {
6+
SWITCH: ['switch'],
7+
AUTO_CLEAN: ['auto_clean'],
8+
MANUAL_CLEAN: ['manual_clean'],
9+
DEODORIZATION: ['deodorization'],
10+
UV: ['uv'],
11+
LIGHT: ['light'],
12+
STATUS: ['status'],
13+
CAT_WEIGHT: ['cat_weight'],
14+
EXCRETION_TIMES: ['excretion_times_day'],
15+
EXCRETION_TIME: ['excretion_time_day'],
16+
NOTIFICATION: ['notification'],
17+
FAULT: ['fault'],
18+
};
19+
20+
export default class CatToiletAccessory extends BaseAccessory {
21+
22+
requiredSchema() {
23+
return [SCHEMA_CODE.SWITCH];
24+
}
25+
26+
configureServices() {
27+
// Main power switch
28+
configureOn(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SWITCH));
29+
configureName(this, this.mainService(), this.device.name);
30+
31+
// Additional switches
32+
this.configureSwitch(SCHEMA_CODE.AUTO_CLEAN, 'Auto Clean');
33+
this.configureSwitch(SCHEMA_CODE.MANUAL_CLEAN, 'Manual Clean');
34+
this.configureSwitch(SCHEMA_CODE.DEODORIZATION, 'Deodorization');
35+
this.configureSwitch(SCHEMA_CODE.UV, 'UV Sterilization');
36+
37+
// Mood light as Lightbulb
38+
this.configureLight();
39+
40+
// Occupancy sensor for active status
41+
this.configureOccupancySensor();
42+
43+
// Filter maintenance for garbage box full
44+
this.configureFilterMaintenance();
45+
46+
// Fault handling
47+
this.configureFault();
48+
}
49+
50+
mainService() {
51+
return this.accessory.getService(this.Service.Switch)
52+
|| this.accessory.addService(this.Service.Switch, this.device.name, 'switch');
53+
}
54+
55+
configureSwitch(schemaCodes: string[], name: string) {
56+
const schema = this.getSchema(...schemaCodes);
57+
if (!schema) {
58+
return;
59+
}
60+
61+
const service = this.accessory.getService(schema.code)
62+
|| this.accessory.addService(this.Service.Switch, name, schema.code);
63+
64+
configureName(this, service, name);
65+
configureOn(this, service, schema);
66+
}
67+
68+
configureLight() {
69+
const schema = this.getSchema(...SCHEMA_CODE.LIGHT);
70+
if (!schema) {
71+
return;
72+
}
73+
74+
const service = this.accessory.getService(schema.code)
75+
|| this.accessory.addService(this.Service.Lightbulb, 'Mood Light', schema.code);
76+
77+
configureName(this, service, 'Mood Light');
78+
service.getCharacteristic(this.Characteristic.On)
79+
.onGet(() => {
80+
this.checkOnlineStatus();
81+
const status = this.getStatus(schema.code)!;
82+
return status.value as boolean;
83+
})
84+
.onSet(async value => {
85+
await this.sendCommands([{
86+
code: schema.code,
87+
value: value as boolean,
88+
}], true);
89+
});
90+
}
91+
92+
configureOccupancySensor() {
93+
const schema = this.getSchema(...SCHEMA_CODE.STATUS);
94+
if (!schema) {
95+
return;
96+
}
97+
98+
const service = this.accessory.getService(this.Service.OccupancySensor)
99+
|| this.accessory.addService(this.Service.OccupancySensor, 'Status', 'status');
100+
101+
configureName(this, service, 'Status');
102+
103+
const { OCCUPANCY_DETECTED, OCCUPANCY_NOT_DETECTED } = this.Characteristic.OccupancyDetected;
104+
service.getCharacteristic(this.Characteristic.OccupancyDetected)
105+
.onGet(() => {
106+
const status = this.getStatus(schema.code)!;
107+
const activeStates = ['cleaning', 'uv', 'deodorization'];
108+
return activeStates.includes(status.value as string)
109+
? OCCUPANCY_DETECTED
110+
: OCCUPANCY_NOT_DETECTED;
111+
});
112+
}
113+
114+
configureFilterMaintenance() {
115+
const schema = this.getSchema(...SCHEMA_CODE.NOTIFICATION);
116+
if (!schema) {
117+
return;
118+
}
119+
120+
const service = this.accessory.getService(this.Service.FilterMaintenance)
121+
|| this.accessory.addService(this.Service.FilterMaintenance, 'Waste Box', 'notification');
122+
123+
configureName(this, service, 'Waste Box');
124+
125+
const { CHANGE_FILTER, FILTER_OK } = this.Characteristic.FilterChangeIndication;
126+
service.getCharacteristic(this.Characteristic.FilterChangeIndication)
127+
.onGet(() => {
128+
const status = this.getStatus(schema.code)!;
129+
// Bit 0 = garbage_box_full
130+
const value = status.value as number;
131+
return (value & 1) ? CHANGE_FILTER : FILTER_OK;
132+
});
133+
}
134+
135+
configureFault() {
136+
const schema = this.getSchema(...SCHEMA_CODE.FAULT);
137+
if (!schema) {
138+
return;
139+
}
140+
141+
// Add fault status to main service
142+
this.mainService().getCharacteristic(this.Characteristic.StatusFault)
143+
.onGet(() => {
144+
const status = this.getStatus(schema.code)!;
145+
const { GENERAL_FAULT, NO_FAULT } = this.Characteristic.StatusFault;
146+
return (status.value as number) > 0 ? GENERAL_FAULT : NO_FAULT;
147+
});
148+
}
149+
}

0 commit comments

Comments
 (0)