zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
306 lines • 13.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.definitions = void 0;
const fz = __importStar(require("../converters/fromZigbee"));
const exposes = __importStar(require("../lib/exposes"));
const logger_1 = require("../lib/logger");
const m = __importStar(require("../lib/modernExtend"));
const tuya = __importStar(require("../lib/tuya"));
const e = exposes.presets;
// Constants
const MAX_ZONES = 8;
const DEFAULT_GROUP_ID_BASE = 100;
// Default zone to group mapping configuration
const defaultZoneGroupMapping = {
1: 101,
2: 102,
3: 103,
4: 104,
5: 105,
6: 106,
7: 107,
8: 108,
};
// Get zone group mapping from device options with fallback to defaults
function getZoneGroupMapping(options = {}) {
const mapping = { ...defaultZoneGroupMapping };
for (let i = 1; i <= MAX_ZONES; i++) {
const optionKey = `zone_${i}_group_id`;
const optionValue = options[optionKey];
if (optionValue !== undefined && typeof optionValue === "number" && optionValue > 0) {
mapping[i] = optionValue;
}
}
return mapping;
}
// Get zone suffix from group ID by checking device options
function getZoneSuffixFromGroupId(groupId, options = {}) {
if (!groupId)
return null;
// Get current zone group mapping from device options
const zoneGroupMapping = getZoneGroupMapping(options);
// Find the zone number for the given group ID
const zoneNumber = Object.keys(zoneGroupMapping).find((zone) => zoneGroupMapping[Number.parseInt(zone, 10)] === groupId);
// If no zone number is found for this group or if zone actions are disabled, add no suffix
if (!zoneNumber || options.zone_actions !== true) {
return "";
}
return `_zone_${zoneNumber}`;
}
// Create fromZigbee converter that adds zone suffix to action names
function createZoneAwareConverter(
// biome-ignore lint/suspicious/noExplicitAny: Fz.ConverterTypeStringOrArray is not exported
baseConverter) {
return {
cluster: baseConverter.cluster,
type: baseConverter.type,
convert: (model, msg, publish, options, meta) => {
const zoneSuffix = getZoneSuffixFromGroupId(msg.groupID, options);
if (zoneSuffix === null)
return undefined;
// Call the base converter
const baseResult = baseConverter.convert(model, msg, publish, options, meta);
if (!baseResult || typeof baseResult !== "object" || !("action" in baseResult))
return baseResult;
// Override the action with zone suffix
return {
...baseResult,
action: `${baseResult.action}${zoneSuffix}`,
};
},
};
}
// Create converters that extract action properties to numeric sensors
const actionPropertyConverters = {
hue_saturation: {
cluster: "lightingColorCtrl",
type: "commandMoveToHueAndSaturation",
convert: (model, msg, publish, options, meta) => {
if (!options?.expose_values)
return undefined;
return {
hue: msg.data?.hue,
saturation: msg.data?.saturation,
};
},
},
color_temp: {
cluster: "lightingColorCtrl",
type: "commandMoveToColorTemp",
convert: (model, msg, publish, options, meta) => {
if (!options?.expose_values)
return undefined;
return {
color_temperature: msg.data?.colortemp,
};
},
},
level: {
cluster: "genLevelCtrl",
type: ["commandMoveToLevel", "commandMoveToLevelWithOnOff"],
convert: (model, msg, publish, options, meta) => {
if (!options?.expose_values)
return undefined;
return {
level: msg.data?.level,
};
},
},
};
// ModernExtend function for FUT089Z remote control
function miboxerFut089zControls() {
// Create zone-aware converters using base converters (they will dynamically read device options)
const onConverter = createZoneAwareConverter(fz.command_on);
const offConverter = createZoneAwareConverter(fz.command_off);
const brightnessConverter = createZoneAwareConverter(fz.command_move_to_level);
const colorTempConverter = createZoneAwareConverter(fz.command_move_to_color_temp);
const colorConverter = createZoneAwareConverter(fz.command_move_to_hue_and_saturation);
// biome-ignore lint/suspicious/noExplicitAny: Fz.ConverterTypeStringOrArray is not exported
const fromZigbee = [
onConverter,
offConverter,
brightnessConverter,
colorTempConverter,
colorConverter,
fz.battery,
tuya.fz.switch_scene,
// Add converters for numeric sensors (always included, but they check options internally)
actionPropertyConverters.hue_saturation,
actionPropertyConverters.color_temp,
actionPropertyConverters.level,
];
// Device exposes (battery info and action)
const exposesList = [e.battery(), e.battery_voltage()];
// Device options for zone group mapping configuration and numeric sensors
const deviceOptions = [
// Generate zone group ID options dynamically
...Array.from({ length: MAX_ZONES }, (_, i) => {
const zoneNum = i + 1;
return exposes
.numeric(`zone_${zoneNum}_group_id`, exposes.access.SET)
.withDescription(`Group ID for zone ${zoneNum} (default: ${DEFAULT_GROUP_ID_BASE + zoneNum})`)
.withValueMin(1)
.withValueMax(65535);
}),
// Feature toggles
new exposes.Binary("expose_values", exposes.access.SET, true, false).withDescription("Expose additional numeric values for action properties (hue, saturation, level, etc.)"),
new exposes.Binary("zone_actions", exposes.access.SET, true, false).withDescription("Publish zone-specific actions with zone IDs (e.g., on_zone_1, off_zone_2)"),
];
// Generate action exposes for all possible zones
function zoneActions(enableZoneActions) {
const baseActions = ["on", "off", "brightness_move_to_level", "color_temperature_move", "move_to_hue_and_saturation"];
if (!enableZoneActions) {
return baseActions;
}
const actions = [];
for (let zoneNumber = 1; zoneNumber <= MAX_ZONES; zoneNumber++) {
baseActions.forEach((action) => {
actions.push(`${action}_zone_${zoneNumber}`);
});
}
return actions;
}
// Event handler to update device when zone options change
const eventHandlers = [
async (event) => {
if (event.type !== "deviceOptionsChanged")
return;
const { device, from, to } = event.data;
// Generate zone option keys dynamically
const zoneOptionKeys = Array.from({ length: MAX_ZONES }, (_, i) => `zone_${i + 1}_group_id`);
const hasZoneOptionChanged = zoneOptionKeys.some((key) => from[key] !== to[key]);
if (hasZoneOptionChanged) {
try {
const endpoint = device.getEndpoint(1);
if (!endpoint) {
throw new Error("Endpoint 1 not found on device");
}
// Get the updated zone group mapping from new options
const updatedMapping = getZoneGroupMapping(to);
// Build zone configuration array from the mapping
const zoneConfig = Object.entries(updatedMapping).map(([zoneNum, groupId]) => ({
zoneNum: Number.parseInt(zoneNum, 10),
groupId: groupId,
}));
// Send the updated zone configuration to the device
await endpoint.command("genGroups", "miboxerSetZones", { zones: zoneConfig });
}
catch (error) {
// Log error but don't throw to avoid breaking the update process
logger_1.logger.error(`Failed to update zone configuration: ${error}`, "zhc:miboxer");
}
}
event.data.deviceExposesChanged();
},
];
return {
fromZigbee,
exposes: [
...exposesList,
((device, options = {}) => {
const dynamicExposes = [];
// Add action expose with zone-aware actions
dynamicExposes.push(e.action(zoneActions(options.zone_actions === true)));
// Add numeric sensors if enabled in options
if (options.expose_values === true) {
dynamicExposes.push(exposes
.numeric("hue", exposes.access.STATE_GET)
.withDescription("Hue value from last action")
.withValueMin(0)
.withValueMax(254), exposes
.numeric("saturation", exposes.access.STATE_GET)
.withDescription("Saturation value from last action")
.withValueMin(0)
.withValueMax(254), exposes
.numeric("color_temperature", exposes.access.STATE_GET)
.withDescription("Color temperature value from last action")
.withUnit("mired")
.withValueMin(153)
.withValueMax(500), exposes
.numeric("level", exposes.access.STATE_GET)
.withDescription("Level value from last action")
.withValueMin(0)
.withValueMax(254));
}
return dynamicExposes;
}),
],
options: deviceOptions,
onEvent: eventHandlers,
isModernExtend: true,
configure: [
async (device, coordinatorEndpoint, definition) => {
const endpoint = device.getEndpoint(1);
if (!endpoint) {
throw new Error("Endpoint 1 not found on device");
}
await tuya.configureMagicPacket(device, coordinatorEndpoint);
// Use default zone group mapping for initial configuration
const currentMapping = getZoneGroupMapping();
// Build zone configuration array from the mapping
const zoneConfig = Object.entries(currentMapping).map(([zoneNum, groupId]) => ({
zoneNum: Number.parseInt(zoneNum, 10),
groupId: groupId,
}));
await endpoint.command("genGroups", "miboxerSetZones", { zones: zoneConfig });
await endpoint.command("genBasic", "tuyaSetup", {}, { disableDefaultResponse: true });
},
],
};
}
exports.definitions = [
{
fingerprint: tuya.fingerprint("TS0504B", ["_TZ3210_ttkgurpb"]),
model: "FUT038Z",
description: "RGBW LED controller",
vendor: "MiBoxer",
extend: [tuya.modernExtend.tuyaLight({ colorTemp: { range: [153, 500] }, color: true })],
},
{
fingerprint: tuya.fingerprint("TS1002", ["_TZ3000_xwh1e22x", "_TZ3000_zwszqdpy"]),
model: "FUT089Z",
vendor: "MiBoxer",
description: "RGB+CCT Remote",
whiteLabel: [tuya.whitelabel("Ledron", "YK-16", "RGB+CCT Remote", ["_TZ3000_zwszqdpy"])],
extend: [
tuya.clusters.addTuyaGenGroupsCluster(),
tuya.clusters.addTuyaGenOnOffCluster(),
miboxerFut089zControls(),
m.quirkCheckinInterval(21600), // Device observed to report every 4h, set to 6h (21600s) for safety margin
],
},
];
//# sourceMappingURL=miboxer.js.map