diff --git a/devices/generic/constants.json b/devices/generic/constants.json index 94a1ee16bb..7063384da8 100644 --- a/devices/generic/constants.json +++ b/devices/generic/constants.json @@ -2,6 +2,7 @@ "schema": "constants1.schema.json", "manufacturers" : { + "$MF_AQARA": "Aqara", "$MF_BOSCH": "Bosch", "$MF_IKEA": "IKEA of Sweden", "$MF_LUMI": "LUMI", diff --git a/devices/generic/items/attr_swversion_bis_item.json b/devices/generic/items/attr_swversion_bis_item.json new file mode 100644 index 0000000000..ab448efa12 --- /dev/null +++ b/devices/generic/items/attr_swversion_bis_item.json @@ -0,0 +1,9 @@ +{ + "schema": "resourceitem1.schema.json", + "id": "attr/swversion_bis", + "datatype": "String", + "access": "R", + "public": true, + "implicit" : true, + "description": "Firmware version of the device." +} diff --git a/devices/generic/items/config_color_gradient_pixel_count_item.json b/devices/generic/items/config_color_gradient_pixel_count_item.json new file mode 100644 index 0000000000..5c1f70e9f7 --- /dev/null +++ b/devices/generic/items/config_color_gradient_pixel_count_item.json @@ -0,0 +1,8 @@ +{ + "schema": "resourceitem1.schema.json", + "id": "config/color/gradient/pixel_count", + "datatype": "UInt8", + "access": "RW", + "public": true, + "description": "Number of pixels on Aqara LED Strip T1." +} diff --git a/devices/generic/items/state_music_sync_item.json b/devices/generic/items/state_music_sync_item.json new file mode 100644 index 0000000000..0aae9508c6 --- /dev/null +++ b/devices/generic/items/state_music_sync_item.json @@ -0,0 +1,8 @@ +{ + "schema": "resourceitem1.schema.json", + "id": "state/music_sync", + "datatype": "Bool", + "access": "RW", + "public": true, + "description": "Music sync on or off." +} diff --git a/devices/xiaomi/lumi_light_acn132.json b/devices/xiaomi/lumi_light_acn132.json new file mode 100644 index 0000000000..305c6e3cce --- /dev/null +++ b/devices/xiaomi/lumi_light_acn132.json @@ -0,0 +1,368 @@ +{ + "schema": "devcap1.schema.json", + "manufacturername": [ + "$MF_AQARA", + "$MF_LUMI" + ], + "modelid": [ + "lumi.light.acn132", + "lumi.light.acn132" + ], + "product": "Aqara LED Strip T1", + "sleeper": false, + "status": "Gold", + "subdevices": [ + { + "type": "$TYPE_EXTENDED_COLOR_LIGHT", + "restapi": "/lights", + "uuid": [ + "$address.ext", + "0x01" + ], + "items": [ + { + "name": "attr/id" + }, + { + "name": "attr/lastannounced" + }, + { + "name": "attr/lastseen" + }, + { + "name": "attr/manufacturername" + }, + { + "name": "attr/modelid" + }, + { + "name": "attr/name" + }, + { + "name": "attr/swversion", + "parse": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x00EE", + "eval": "Item.val = '0.0.0_' + ('0000' + ((Attr.val & 0xFF00) >> 8).toString() + (Attr.val & 0xFF).toString()).slice(-4)" + }, + "read": { + "fn": "zcl", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": [ + "0x00EE" + ] + }, + "refresh.interval": 86400 + }, + { + "name": "attr/swversion_bis", + "parse": { + "fn": "xiaomi:special", + "mf": "0x115F", + "at": "0x00F7", + "idx": "0x0D", + "eval": "R.item('attr/swversion').val = '0.0.0_' + ('0000' + ((Attr.val & 0xFF00) >> 8).toString() + (Attr.val & 0xFF).toString()).slice(-4)" + }, + "read": { + "fn": "none" + } + }, + { + "name": "attr/type" + }, + { + "name": "attr/uniqueid" + }, + { + "name": "cap/bri/move_with_onoff" + }, + { + "name": "cap/color/capabilities", + "read": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0x0300", + "at": [ + "0x400a", + "0x400b", + "0x400c", + "0x000f" + ] + }, + "refresh.interval": 86400 + }, + { + "name": "cap/color/ct/max" + }, + { + "name": "cap/color/ct/min" + }, + { + "name": "cap/color/ct/computes_xy" + }, + { + "name": "cap/color/gradient/pixel_length", + "static": 2000 + }, + { + "name": "cap/color/xy/blue_x", + "static": 0 + }, + { + "name": "cap/color/xy/blue_y", + "static": 0 + }, + { + "name": "cap/color/xy/green_x", + "static": 0 + }, + { + "name": "cap/color/xy/green_y", + "static": 65279 + }, + { + "name": "cap/color/xy/red_x", + "static": 65279 + }, + { + "name": "cap/color/xy/red_y", + "static": 0 + }, + { + "name": "config/bri/execute_if_off", + "read": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0x0008", + "at": [ + "0x000f", + "0x0010", + "0x0011" + ] + }, + "refresh.interval": 86400 + }, + { + "name": "config/bri/max", + "parse": { + "fn": "zcl", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x0516", + "eval": "Item.val = Math.round(Attr.val * 2.54)" + }, + "read": { + "fn": "zcl", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": [ + "0x0516", + "0x0515" + ] + }, + "refresh.interval": 3600, + "write": { + "fn": "zcl", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x0516", + "dt": "0x20", + "eval": "Math.round(Item.val / 2.54)" + } + }, + { + "name": "config/bri/min", + "parse": { + "fn": "zcl", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x0515", + "eval": "Item.val = Math.round(Attr.val * 2.54)" + }, + "read": { + "fn": "none" + }, + "write": { + "fn": "zcl", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x0515", + "dt": "0x20", + "eval": "Math.round(Item.val / 2.54)" + } + }, + { + "name": "config/bri/on_level", + "read": { + "fn": "none" + } + }, + { + "name": "config/bri/onoff_transitiontime", + "read": { + "fn": "none" + } + }, + { + "name": "config/color/execute_if_off", + "read": { + "fn": "none" + } + }, + { + "name": "config/color/gradient/pixel_count", + "parse": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x051B", + "eval": "Item.val = Attr.val" + }, + "read": { + "fn": "zcl", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": [ + "0x051B" + ] + }, + "refresh.interval": 86400, + "write": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x051B", + "dt": "0x20", + "eval": "Math.max(5, Math.min(Item.val, 50))" + } + }, + { + "name": "config/on/startup", + "parse": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x0517", + "eval": "Item.val = [1, 255, 0][Attr.val]" + }, + "read": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": [ + "0x0517" + ] + }, + "write": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x0517", + "dt": "0x20", + "eval": "Item.val === 1 ? 0 : Item.val === 0 ? 2 : 1" + } + }, + { + "name": "state/alert" + }, + { + "name": "state/on", + "refresh.interval": 360 + }, + { + "name": "state/bri", + "refresh.interval": 360 + }, + { + "name": "state/colormode", + "parse": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0x0300", + "at": "0x4001", + "eval": "Item.val = ['hs', 'xy', 'ct', 'xy'][Attr.val]" + }, + "read": { + "fn": "zcl:attr", + "ep": "0x01", + "cl": "0x0300", + "at": [ + "0x4001", + "0x0003", + "0x0004", + "0x0007" + ] + }, + "refresh.interval": 360 + }, + { + "name": "state/music_sync", + "parse": { + "fn": "zcl", + "ep": 0, + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x051C", + "eval": "Item.val = Attr.val === 1" + }, + "read": { + "fn": "zcl", + "ep": 0, + "cl": "0xFCC0", + "mf": "0x115F", + "at": [ + "0x051C" + ] + }, + "refresh.interval": 305, + "write": { + "fn": "zcl", + "ep": 0, + "cl": "0xFCC0", + "mf": "0x115F", + "at": "0x051C", + "dt": "0x20", + "eval": "Item.val ? 1 : 0" + } + }, + { + "name": "state/x", + "read": { + "fn": "none" + } + }, + { + "name": "state/y", + "read": { + "fn": "none" + } + }, + { + "name": "state/ct", + "read": { + "fn": "none" + } + }, + { + "name": "state/reachable" + } + ] + } + ] +} diff --git a/general.xml b/general.xml index aad65ab122..baf664ee24 100644 --- a/general.xml +++ b/general.xml @@ -5542,7 +5542,7 @@ Contactor > On/off=0003 - HP/HC=0004 - + @@ -5596,7 +5596,7 @@ Contactor > On/off=0003 - HP/HC=0004 - + Write 1 to calibrate @@ -5648,7 +5648,26 @@ Contactor > On/off=0003 - HP/HC=0004 - + + + + + OnOff state on power on: 0: on, 1: previous, 2: off. + + + Number of 20cm segments. + + + Music Sync: 0: off, 1: on. + + + Music Sync Effecy: 0: random, 1: breathing, 2: rainbow, 3: chasing. + + + Music Sync Sensitivity: 0: low, 2: high. + + + diff --git a/light_node.cpp b/light_node.cpp index 5469f1540b..9ce84c3fa1 100644 --- a/light_node.cpp +++ b/light_node.cpp @@ -424,6 +424,11 @@ void LightNode::setHaEndpoint(const deCONZ::SimpleDescriptor &endpoint) { removeItem(RCapColorCapabilities); } + // else if (modelId() == QLatin1String("lumi.light.acn132")) + else if (deviceId == DEV_ID_HA_COLOR_DIMMABLE_LIGHT && manufacturerCode() == 0 && + manufacturer().isEmpty() && modelId().isEmpty()) + { + } else { addItem(DataTypeString, RStateEffect)->setValue(RStateEffectValues[R_EFFECT_NONE]); diff --git a/resource.cpp b/resource.cpp index 69230ba9ed..e4c8c23a18 100644 --- a/resource.cpp +++ b/resource.cpp @@ -74,6 +74,7 @@ const char *RAttrProductId = "attr/productid"; const char *RAttrProductName = "attr/productname"; const char *RAttrSwconfigid = "attr/swconfigid"; const char *RAttrSwVersion = "attr/swversion"; +const char *RAttrSwVersionBis = "attr/swversion_bis"; const char *RAttrType = "attr/type"; const char *RAttrUniqueId = "attr/uniqueid"; @@ -134,6 +135,7 @@ const char *RStateLowBattery = "state/lowbattery"; const char *RStateLux = "state/lux"; const char *RStateMoisture = "state/moisture"; const char *RStateMountingModeActive = "state/mountingmodeactive"; +const char *RStateMusicSync = "state/music_sync"; const char *RStateOn = "state/on"; const char *RStateOpen = "state/open"; const char *RStateOpenBis = "state/open_bis"; @@ -242,6 +244,7 @@ const char *RConfigClickMode = "config/clickmode"; const char *RConfigColorCapabilities = "config/colorcapabilities"; const char *RConfigColorCtStartup = "config/color/ct/startup"; const char *RConfigColorExecuteIfOff = "config/color/execute_if_off"; +const char *RConfigColorGradientPixelCount = "config/color/gradient/pixel_count"; const char *RConfigColorGradientReversed = "config/color/gradient/reversed"; const char *RConfigColorXyStartupX = "config/color/xy/startup_x"; const char *RConfigColorXyStartupY = "config/color/xy/startup_y"; @@ -374,6 +377,7 @@ void initResourceDescriptors() rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeString, QVariant::String, RAttrProductName)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeString, QVariant::String, RAttrSwconfigid)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeString, QVariant::String, RAttrSwVersion)); + rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeString, QVariant::String, RAttrSwVersionBis)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeString, QVariant::String, RAttrType)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeString, QVariant::String, RAttrUniqueId)); @@ -435,6 +439,7 @@ void initResourceDescriptors() rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeUInt32, QVariant::Double, RStateLux)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeInt16, QVariant::Double, RStateMoisture)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeBool, QVariant::Bool, RStateMountingModeActive)); + rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeBool, QVariant::Bool, RStateMusicSync)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeBool, QVariant::Bool, RStateOn)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeBool, QVariant::Bool, RStateOpen)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeBool, QVariant::Bool, RStateOpenBis)); @@ -529,6 +534,7 @@ void initResourceDescriptors() rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeUInt16, QVariant::Double, RConfigColorCapabilities)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeUInt16, QVariant::Double, RConfigColorCtStartup)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeBool, QVariant::Bool, RConfigColorExecuteIfOff)); + rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeUInt8, QVariant::Double, RConfigColorGradientPixelCount, 5, 50)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeBool, QVariant::Bool, RConfigColorGradientReversed)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeUInt16, QVariant::Double, RConfigColorXyStartupX)); rItemDescriptors.emplace_back(ResourceItemDescriptor(DataTypeUInt16, QVariant::Double, RConfigColorXyStartupY)); diff --git a/resource.h b/resource.h index a5b658bbd6..bd44d4f952 100644 --- a/resource.h +++ b/resource.h @@ -103,6 +103,7 @@ extern const char *RAttrProductId; extern const char *RAttrProductName; extern const char *RAttrSwconfigid; extern const char *RAttrSwVersion; +extern const char *RAttrSwVersionBis; extern const char *RAttrType; extern const char *RAttrUniqueId; @@ -163,6 +164,7 @@ extern const char *RStateLowBattery; extern const char *RStateLux; extern const char *RStateMoisture; extern const char *RStateMountingModeActive; +extern const char *RStateMusicSync; extern const char *RStateOn; extern const char *RStateOpen; extern const char *RStateOpenBis; @@ -256,6 +258,7 @@ extern const char *RConfigClickMode; extern const char *RConfigColorCapabilities; // Deprecated extern const char *RConfigColorCtStartup; extern const char *RConfigColorExecuteIfOff; +extern const char *RConfigColorGradientPixelCount; extern const char *RConfigColorGradientReversed; extern const char *RConfigColorXyStartupX; extern const char *RConfigColorXyStartupY; diff --git a/rest_lights.cpp b/rest_lights.cpp index 3e99f4f4df..5b5127ef47 100644 --- a/rest_lights.cpp +++ b/rest_lights.cpp @@ -319,6 +319,7 @@ bool DeRestPluginPrivate::lightToMap(const ApiRequest &req, const LightNode *lig else if (rid.suffix == RConfigBriStartup) { configBri["startup"] = item->toNumber() == 0xFF ? QVariant(QLatin1String("previous")) : item->toNumber(); } else if (rid.suffix == RConfigColorCtStartup) { configColorCt["startup"] = item->toNumber() == 0xFFFF ? QVariant(QLatin1String("previous")) : item->toNumber(); } else if (rid.suffix == RConfigColorExecuteIfOff) { configColor["execute_if_off"] = item->toBool(); } + else if (rid.suffix == RConfigColorGradientPixelCount) { configColorGradient["pixel_count"] = item->toNumber(); } else if (rid.suffix == RConfigColorGradientReversed) { configColorGradient["reversed"] = item->toBool(); } else if (rid.suffix == RConfigColorXyStartupX) { isx = item; } else if (rid.suffix == RConfigColorXyStartupY) { isy = item; } @@ -345,6 +346,7 @@ bool DeRestPluginPrivate::lightToMap(const ApiRequest &req, const LightNode *lig else if (rid.suffix == RStateLift) { state["lift"] = item->toNumber(); } else if (rid.suffix == RStateOn) { state["on"] = item->toBool(); } else if (rid.suffix == RStateOpen) { state["open"] = item->toBool(); } + else if (rid.suffix == RStateMusicSync) { state["music_sync"] = item->toBool(); } else if (rid.suffix == RStateReachable) { state["reachable"] = item->toBool(); } else if (rid.suffix == RStateSat) { state["sat"] = item->toNumber(); } else if (rid.suffix == RStateSpeed) { state["speed"] = item->toNumber(); } @@ -647,6 +649,12 @@ int DeRestPluginPrivate::setLightState(const ApiRequest &req, ApiResponse &rsp) } Device *device = static_cast(taskRef.lightNode->parentResource()); + Resource *rsub = nullptr; + StateChange change(StateChange::StateCallFunction, SC_WriteZclAttribute, taskRef.lightNode->haEndpoint().endpoint()); + if (device && device->managed()) + { + rsub = DEV_GetSubDevice(device, nullptr, taskRef.lightNode->uniqueId()); + } rsp.httpStatus = HttpStatusOk; if (!taskRef.lightNode->isAvailable()) @@ -753,6 +761,8 @@ int DeRestPluginPrivate::setLightState(const ApiRequest &req, ApiResponse &rsp) quint16 colorloopSpeed = 25; bool hasGradient = false; QVariantMap gradient; + bool hasMusicSync = false; + bool targetMusicSync = false; QString alert; bool hasSpeed = false; quint8 targetSpeed = 0; @@ -906,6 +916,17 @@ int DeRestPluginPrivate::setLightState(const ApiRequest &req, ApiResponse &rsp) valueOk = effect >= 0; } } + else if (param == "music_sync" && taskRef.lightNode->item(RStateMusicSync)) + { + paramOk = true; + hasCmd = true; + if (map[param].type() == QVariant::Bool) + { + valueOk = true; + hasMusicSync = true; + targetMusicSync = map[param].toBool(); + } + } else if (param == "colorloopspeed" && taskRef.lightNode->item(RStateEffect)) { paramOk = true; @@ -1464,6 +1485,19 @@ int DeRestPluginPrivate::setLightState(const ApiRequest &req, ApiResponse &rsp) } } + if (hasMusicSync) + { + change.addTargetValue(RStateMusicSync, targetMusicSync); + taskRef.lightNode->setValue(RStateMusicSync, targetMusicSync); + DB_StoreSubDeviceItem(taskRef.lightNode, taskRef.lightNode->item(RStateMusicSync)); + + QVariantMap rspItem; + QVariantMap rspItemState; + rspItemState[QString("/lights/%1/state/music_sync").arg(id)] = targetMusicSync; + rspItem["success"] = rspItemState; + rsp.list.append(rspItem); + } + // state.alert if (!alert.isEmpty()) { @@ -1604,6 +1638,10 @@ int DeRestPluginPrivate::setLightState(const ApiRequest &req, ApiResponse &rsp) } rsp.etag = taskRef.lightNode->etag; + if (rsub) + { + rsub->addStateChange(change); + } processTasks(); return REQ_READY_SEND; } @@ -1965,7 +2003,27 @@ int DeRestPluginPrivate::setLightConfig(const ApiRequest &req, ApiResponse &rsp) QString path2 = QString("%1/%2").arg(path1).arg(key); value = map2[key]; - if (key == "reversed") + if (key == "pixel_count") + { + ResourceItem *item = lightNode->item(RConfigColorGradientPixelCount); + if (item) + { + paramOk = true; + if (value.type() == QVariant::Double) + { + const quint8 pixelCount = value.toUInt(&ok); + if (ok) + { + valueOk = true; + change.addTargetValue(RConfigColorGradientPixelCount, pixelCount); + lightNode->setValue(RConfigColorGradientPixelCount, pixelCount); + DB_StoreSubDeviceItem(lightNode, item); + } + } + } + + } + else if (key == "reversed") { ResourceItem *item = lightNode->item(RConfigColorGradientReversed); if (item) @@ -3848,6 +3906,7 @@ void DeRestPluginPrivate::handleLightEvent(const Event &e) else if (rid.suffix == RConfigBriStartup) { configBri["startup"] = item->toNumber() == 0xFF ? QVariant(QLatin1String("previous")) : item->toNumber(); } else if (rid.suffix == RConfigColorCtStartup) { configColorCt["startup"] = item->toNumber() == 0xFFFF ? QVariant(QLatin1String("previous")) : item->toNumber(); } else if (rid.suffix == RConfigColorExecuteIfOff) { configColor["execute_if_off"] = item->toBool(); } + else if (rid.suffix == RConfigColorGradientPixelCount) { configColorGradient["pixel_count"] = item->toNumber(); } else if (rid.suffix == RConfigColorGradientReversed) { configColorGradient["reversed"] = item->toBool(); } else if (rid.suffix == RConfigLocked) { config["locked"] = item->toBool(); } else if (rid.suffix == RConfigOnStartup) { configOn["startup"] = item->toNumber() == 0xFF ? QVariant(QLatin1String("previous")) : item->toBool(); } @@ -3871,6 +3930,7 @@ void DeRestPluginPrivate::handleLightEvent(const Event &e) else if (rid.suffix == RStateLift) { state["lift"] = item->toNumber(); } else if (rid.suffix == RStateOn) { state["on"] = item->toBool(); } else if (rid.suffix == RStateOpen) { state["open"] = item->toBool(); } + else if (rid.suffix == RStateMusicSync) { state["music_sync"] = item->toBool(); } else if (rid.suffix == RStateReachable) { state["reachable"] = item->toBool(); } else if (rid.suffix == RStateSat) { state["sat"] = item->toNumber(); } else if (rid.suffix == RStateSpeed) { state["speed"] = item->toNumber(); }