diff --git a/include/bridge.hpp b/include/bridge.hpp index b7c9c69..8a711c9 100644 --- a/include/bridge.hpp +++ b/include/bridge.hpp @@ -1,9 +1,13 @@ #pragma once #include "base.hpp" +#include "addr.hpp" #include "bridge_model.hpp" #include "dali_comm.hpp" #include "dt1.hpp" +#include "dt4.hpp" +#include "dt5.hpp" +#include "dt6.hpp" #include "dt8.hpp" #include @@ -30,6 +34,7 @@ struct DaliBridgeResult { std::optional data; std::string error; DaliValue::Object metadata; + DaliValue::Array results; DaliValue::Object toJson() const; }; @@ -49,7 +54,11 @@ class DaliBridgeEngine { DaliComm& comm_; DaliBase base_; DaliDT1 dt1_; + DaliDT4 dt4_; + DaliDT5 dt5_; + DaliDT6 dt6_; DaliDT8 dt8_; + DaliAddr addr_; std::map models_; DaliBridgeResult executeResolved(const DaliBridgeRequest& request, diff --git a/include/bridge_model.hpp b/include/bridge_model.hpp index 4f5bfe6..74bdaf8 100644 --- a/include/bridge_model.hpp +++ b/include/bridge_model.hpp @@ -57,6 +57,36 @@ enum class BridgeOperation { startEmergencyFunctionTest = 18, stopEmergencyTest = 19, startEmergencyDurationTest = 20, + setColorTemperatureRaw = 21, + setColourXY = 22, + setColourRGB = 23, + setTemporaryColourTemperature = 24, + setTemporaryColourXY = 25, + setTemporaryRGBDimLevels = 26, + setTemporaryColourMask = 27, + activateTemporaryColour = 28, + copyReportToTemporary = 29, + storeDt8SceneSnapshot = 30, + storeDt8PowerOnLevelSnapshot = 31, + storeDt8SystemFailureLevelSnapshot = 32, + getDt1Snapshot = 33, + getDt4Snapshot = 34, + getDt5Snapshot = 35, + getDt6Snapshot = 36, + getDt8StatusSnapshot = 37, + getGroupMask = 38, + setGroupMask = 39, + getSceneLevel = 40, + setSceneLevel = 41, + removeSceneLevel = 42, + getSceneMap = 43, + getAddressSettings = 44, + setAddressSettings = 45, + searchAddressRange = 46, + allocateAllShortAddresses = 47, + resetAndAllocateShortAddresses = 48, + stopAddressAllocation = 49, + batch = 50, }; enum class BridgeValueEncoding { diff --git a/src/base.cpp b/src/base.cpp index 4b4363d..37960e3 100644 --- a/src/base.cpp +++ b/src/base.cpp @@ -255,13 +255,13 @@ std::optional> DaliBase::getGradualChange(int a) { std::optional DaliBase::getGradualChangeRate(int a) { const auto rs = getGradualChange(a); if (!rs.has_value()) return std::nullopt; - return rs->second; + return rs->first; } std::optional DaliBase::getGradualChangeSpeed(int a) { const auto rs = getGradualChange(a); if (!rs.has_value()) return std::nullopt; - return rs->first; + return rs->second; } bool DaliBase::setPowerOnLevel(int a, int value) { return setDTR(value) && storeDTRAsPoweredBright(a); } @@ -291,7 +291,7 @@ bool DaliBase::setFadeTime(int a, int value) { std::optional DaliBase::getFadeTime(int a) { const auto rs = getGradualChange(a); if (!rs.has_value()) return std::nullopt; - return rs->first; + return rs->second; } bool DaliBase::setFadeRate(int a, int value) { return setDTR(value) && storeDTRAsFadeRate(a); } @@ -299,7 +299,7 @@ bool DaliBase::setFadeRate(int a, int value) { return setDTR(value) && storeDTRA std::optional DaliBase::getFadeRate(int a) { const auto rs = getGradualChange(a); if (!rs.has_value()) return std::nullopt; - return rs->second; + return rs->first; } std::optional DaliBase::getNextDeviceType(int a) { return query(a, DALI_CMD_QUERY_NEXT_DEVICE_TYPE); } diff --git a/src/bridge.cpp b/src/bridge.cpp index a1256ba..807ac45 100644 --- a/src/bridge.cpp +++ b/src/bridge.cpp @@ -1,12 +1,155 @@ #include "bridge.hpp" +#include #include +#include +#include #include namespace { +std::string byteBinary(int value) { + std::string out; + out.reserve(8); + for (int bit = 7; bit >= 0; --bit) { + out.push_back(((value >> bit) & 0x1) != 0 ? '1' : '0'); + } + return out; +} + +void putRaw(DaliValue::Object* object, const std::string& key, int value) { + if (object == nullptr) return; + (*object)[key] = value & 0xFF; + (*object)[key + "Binary"] = byteBinary(value); +} + +void putOptionalInt(DaliValue::Object* object, const std::string& key, + const std::optional& value) { + if (object == nullptr || !value.has_value()) return; + (*object)[key] = value.value(); +} + +void putOptionalRaw(DaliValue::Object* object, const std::string& key, + const std::optional& value) { + if (object == nullptr || !value.has_value()) return; + putRaw(object, key, value.value()); +} + +const DaliValue* paramValue(const DaliBridgeRequest& request, const char* key) { + if (key == nullptr) return nullptr; + if (const auto* object = request.value.asObject()) { + if (const auto* value = getObjectValue(*object, key)) return value; + } + return getObjectValue(request.metadata, key); +} + +const DaliValue* paramValueAny(const DaliBridgeRequest& request, + std::initializer_list keys) { + for (const char* key : keys) { + if (const auto* value = paramValue(request, key)) return value; + } + return nullptr; +} + +std::optional intParam(const DaliBridgeRequest& request, + std::initializer_list keys) { + if (const auto* value = paramValueAny(request, keys)) return value->asInt(); + return std::nullopt; +} + +std::optional doubleParam(const DaliBridgeRequest& request, + std::initializer_list keys) { + if (const auto* value = paramValueAny(request, keys)) return value->asDouble(); + return std::nullopt; +} + +std::optional boolParam(const DaliBridgeRequest& request, + std::initializer_list keys) { + if (const auto* value = paramValueAny(request, keys)) return value->asBool(); + return std::nullopt; +} + +std::optional stringParam(const DaliBridgeRequest& request, + std::initializer_list keys) { + if (const auto* value = paramValueAny(request, keys)) return value->asString(); + return std::nullopt; +} + +bool isKnownRequestKey(const std::string& key) { + static const char* known[] = {"type", "seq", "sequence", "model", + "modelID", "modelId", "op", "operation", + "addr", "rawAddress", "cmd", "rawCommand", + "shortAddress", "short_address", "value", "meta"}; + for (const char* item : known) { + if (key == item) return true; + } + return false; +} + +DaliBridgeRequest bridgeRequestFromValue(const DaliValue::Object& object, + const DaliBridgeRequest* parent = nullptr, + int index = -1) { + DaliBridgeRequest request; + if (parent != nullptr) request.sequence = parent->sequence; + request.sequence = getObjectString(object, "seq") + .value_or(getObjectString(object, "sequence").value_or(request.sequence)); + if (request.sequence.empty() && parent != nullptr && index >= 0) { + request.sequence = parent->sequence + ":" + std::to_string(index); + } + request.modelID = getObjectString(object, "model") + .value_or(getObjectString(object, "modelID") + .value_or(getObjectString(object, "modelId").value_or(""))); + const std::string op = getObjectString(object, "op") + .value_or(getObjectString(object, "operation").value_or("")); + if (!op.empty()) request.operation = bridgeOperationFromString(op); + request.rawAddress = getObjectInt(object, "addr").has_value() ? getObjectInt(object, "addr") + : getObjectInt(object, "rawAddress"); + request.rawCommand = getObjectInt(object, "cmd").has_value() ? getObjectInt(object, "cmd") + : getObjectInt(object, "rawCommand"); + request.shortAddress = getObjectInt(object, "shortAddress").has_value() + ? getObjectInt(object, "shortAddress") + : getObjectInt(object, "short_address"); + if (const auto* value = getObjectValue(object, "value")) request.value = *value; + if (const auto* meta = getObjectValue(object, "meta")) { + if (const auto* metaObject = meta->asObject()) request.metadata = *metaObject; + } + for (const auto& entry : object) { + if (!isKnownRequestKey(entry.first)) request.metadata[entry.first] = entry.second; + } + return request; +} + +bool isShortAddress(int address) { return address >= 0 && address <= 63; } + +bool isReadOperation(BridgeOperation operation) { + switch (operation) { + case BridgeOperation::query: + case BridgeOperation::getBrightness: + case BridgeOperation::getStatus: + case BridgeOperation::getColorTemperature: + case BridgeOperation::getColorStatus: + case BridgeOperation::getEmergencyLevel: + case BridgeOperation::getEmergencyStatus: + case BridgeOperation::getEmergencyFailureStatus: + case BridgeOperation::getDt1Snapshot: + case BridgeOperation::getDt4Snapshot: + case BridgeOperation::getDt5Snapshot: + case BridgeOperation::getDt6Snapshot: + case BridgeOperation::getDt8StatusSnapshot: + case BridgeOperation::getGroupMask: + case BridgeOperation::getSceneLevel: + case BridgeOperation::getSceneMap: + case BridgeOperation::getAddressSettings: + case BridgeOperation::searchAddressRange: + return true; + default: + return false; + } +} + void addStatusMetadata(DaliBridgeResult* result, int rawStatus) { const DaliStatus status = DaliStatus::fromByte(static_cast(rawStatus)); + putRaw(&result->metadata, "statusRaw", rawStatus); result->metadata["controlGearPresent"] = status.controlGearPresent; result->metadata["lampFailure"] = status.lampFailure; result->metadata["lampPowerOn"] = status.lampPowerOn; @@ -14,9 +157,219 @@ void addStatusMetadata(DaliBridgeResult* result, int rawStatus) { result->metadata["fadingCompleted"] = status.fadingCompleted; result->metadata["resetState"] = status.resetState; result->metadata["missingShortAddress"] = status.missingShortAddress; + result->metadata["powerSupplyFault"] = status.psFault; result->metadata["psFault"] = status.psFault; } +void addColorStatusMetadata(DaliValue::Object* metadata, const ColorStatus& status) { + putRaw(metadata, "colorStatusRaw", status.raw()); + (*metadata)["xyOutOfRange"] = status.xyOutOfRange(); + (*metadata)["ctOutOfRange"] = status.ctOutOfRange(); + (*metadata)["autoCalibrationActive"] = status.autoCalibrationActive(); + (*metadata)["autoCalibrationSuccess"] = status.autoCalibrationSuccess(); + (*metadata)["xyActive"] = status.xyActive(); + (*metadata)["ctActive"] = status.ctActive(); + (*metadata)["primaryNActive"] = status.primaryNActive(); + (*metadata)["rgbwafActive"] = status.rgbwafActive(); +} + +void addColorFeatureMetadata(DaliValue::Object* metadata, const ColorTypeFeature& features) { + putRaw(metadata, "colorTypeFeaturesRaw", features.features()); + (*metadata)["primaryCount"] = features.primaryCount(); + (*metadata)["rgbwafChannels"] = features.rgbwafChannels(); + (*metadata)["xyCapable"] = features.xyCapable(); + (*metadata)["ctCapable"] = features.ctCapable(); + (*metadata)["primaryNCapable"] = features.primaryNCapable(); + (*metadata)["rgbwafCapable"] = features.rgbwafCapable(); +} + +void addDt1DetailedMetadata(DaliBridgeResult* result, const DT1TestStatusDetailed& status) { + putOptionalRaw(&result->metadata, "failureStatusRaw", status.failureStatus); + putOptionalRaw(&result->metadata, "emergencyStatusRaw", status.emergencyStatus); + putOptionalRaw(&result->metadata, "emergencyModeRaw", status.emergencyMode); + putOptionalRaw(&result->metadata, "featuresRaw", status.feature); + putOptionalRaw(&result->metadata, "deviceStatusRaw", status.deviceStatus); + result->metadata["testInProgress"] = status.testInProgress; + result->metadata["lampFailure"] = status.lampFailure; + result->metadata["batteryFailure"] = status.batteryFailure; + result->metadata["functionTestActive"] = status.functionTestActive; + result->metadata["durationTestActive"] = status.durationTestActive; + result->metadata["testDone"] = status.testDone; + result->metadata["identifyActive"] = status.identifyActive; + result->metadata["physicalSelectionActive"] = status.physicalSelectionActive; + result->metadata["circuitFailure"] = status.circuitFailure; + result->metadata["batteryDurationFailure"] = status.batteryDurationFailure; + result->metadata["emergencyLampFailure"] = status.emergencyLampFailure; + result->metadata["functionTestMaxDelayExceeded"] = status.functionTestMaxDelayExceeded; + result->metadata["durationTestMaxDelayExceeded"] = status.durationTestMaxDelayExceeded; + result->metadata["functionTestFailed"] = status.functionTestFailed; + result->metadata["durationTestFailed"] = status.durationTestFailed; + result->metadata["functionTestResultValid"] = status.functionTestResultValid; + result->metadata["durationTestResultValid"] = status.durationTestResultValid; + result->metadata["batteryFullyCharged"] = status.batteryFullyCharged; + result->metadata["functionTestPending"] = status.functionTestPending; + result->metadata["durationTestPending"] = status.durationTestPending; + result->metadata["restModeActive"] = status.restModeActive; + result->metadata["normalModeActive"] = status.normalModeActive; + result->metadata["emergencyModeActive"] = status.emergencyModeActive; + result->metadata["extendedEmergencyModeActive"] = status.extendedEmergencyModeActive; + result->metadata["hardwiredInhibitActive"] = status.hardwiredInhibitActive; + result->metadata["hardwiredSwitchOn"] = status.hardwiredSwitchOn; + result->metadata["supportsAutoTest"] = status.supportsAutoTest; + result->metadata["supportsAdjustableEmergencyLevel"] = status.supportsAdjustableEmergencyLevel; +} + +void addDt4SnapshotMetadata(DaliBridgeResult* result, DaliDT4& dt4, int address) { + putOptionalInt(&result->metadata, "extendedVersion", dt4.getExtendedVersion(address)); + putOptionalInt(&result->metadata, "dimmingCurve", dt4.getDimmingCurve(address)); + putOptionalInt(&result->metadata, "dimmerTemperatureRaw", dt4.getDimmerTemperatureRaw(address)); + putOptionalInt(&result->metadata, "rmsSupplyVoltageRaw", dt4.getRmsSupplyVoltageRaw(address)); + putOptionalInt(&result->metadata, "supplyFrequencyRaw", dt4.getSupplyFrequencyRaw(address)); + putOptionalInt(&result->metadata, "rmsLoadVoltageRaw", dt4.getRmsLoadVoltageRaw(address)); + putOptionalInt(&result->metadata, "rmsLoadCurrentRaw", dt4.getRmsLoadCurrentRaw(address)); + putOptionalInt(&result->metadata, "realLoadPowerRaw", dt4.getRealLoadPowerRaw(address)); + putOptionalInt(&result->metadata, "loadRatingRaw", dt4.getLoadRatingRaw(address)); + if (const auto status = dt4.getDimmerStatus(address)) { + putRaw(&result->metadata, "dimmerStatusRaw", status->raw()); + result->metadata["leadingEdgeModeRunning"] = status->leadingEdgeModeRunning(); + result->metadata["trailingEdgeModeRunning"] = status->trailingEdgeModeRunning(); + result->metadata["referenceMeasurementRunning"] = status->referenceMeasurementRunning(); + result->metadata["nonLogarithmicDimmingCurveActive"] = + status->nonLogarithmicDimmingCurveActive(); + } + if (const auto features = dt4.getFeatures(address)) { + putRaw(&result->metadata, "featuresRaw1", features->raw1()); + putRaw(&result->metadata, "featuresRaw2", features->raw2()); + putRaw(&result->metadata, "featuresRaw3", features->raw3()); + result->metadata["dimmingMethodCode"] = features->dimmingMethodCode(); + result->metadata["canQueryLoadOverCurrentShutdown"] = + features->canQueryLoadOverCurrentShutdown(); + result->metadata["canQueryOpenCircuitDetection"] = features->canQueryOpenCircuitDetection(); + result->metadata["canQueryLoadDecrease"] = features->canQueryLoadDecrease(); + result->metadata["canQueryLoadIncrease"] = features->canQueryLoadIncrease(); + result->metadata["canQueryThermalShutdown"] = features->canQueryThermalShutdown(); + result->metadata["canQueryThermalOverloadReduction"] = + features->canQueryThermalOverloadReduction(); + result->metadata["canQueryTemperature"] = features->canQueryTemperature(); + result->metadata["canQuerySupplyVoltage"] = features->canQuerySupplyVoltage(); + result->metadata["canQuerySupplyFrequency"] = features->canQuerySupplyFrequency(); + result->metadata["canQueryLoadVoltage"] = features->canQueryLoadVoltage(); + result->metadata["canQueryLoadCurrent"] = features->canQueryLoadCurrent(); + result->metadata["canQueryRealLoadPower"] = features->canQueryRealLoadPower(); + result->metadata["canQueryLoadRating"] = features->canQueryLoadRating(); + result->metadata["canQueryCurrentOverloadReduction"] = + features->canQueryCurrentOverloadReduction(); + result->metadata["physicalSelectionSupported"] = features->physicalSelectionSupported(); + result->metadata["canSelectNonLogarithmicDimmingCurve"] = + features->canSelectNonLogarithmicDimmingCurve(); + result->metadata["canQueryUnsuitableLoad"] = features->canQueryUnsuitableLoad(); + } + if (const auto failure = dt4.getFailureStatus(address)) { + putRaw(&result->metadata, "failureRaw1", failure->raw1()); + putRaw(&result->metadata, "failureRaw2", failure->raw2()); + result->metadata["loadOverCurrentShutdown"] = failure->loadOverCurrentShutdown(); + result->metadata["openCircuitDetected"] = failure->openCircuitDetected(); + result->metadata["loadDecreaseDetected"] = failure->loadDecreaseDetected(); + result->metadata["loadIncreaseDetected"] = failure->loadIncreaseDetected(); + result->metadata["thermalShutdown"] = failure->thermalShutdown(); + result->metadata["thermalOverloadReduction"] = failure->thermalOverloadReduction(); + result->metadata["referenceMeasurementFailed"] = failure->referenceMeasurementFailed(); + result->metadata["loadUnsuitableForSelectedMethod"] = failure->loadUnsuitableForSelectedMethod(); + result->metadata["supplyVoltageOutOfLimits"] = failure->supplyVoltageOutOfLimits(); + result->metadata["supplyFrequencyOutOfLimits"] = failure->supplyFrequencyOutOfLimits(); + result->metadata["loadVoltageOutOfLimits"] = failure->loadVoltageOutOfLimits(); + result->metadata["loadCurrentOverloadReduction"] = failure->loadCurrentOverloadReduction(); + } +} + +void addDt5SnapshotMetadata(DaliBridgeResult* result, DaliDT5& dt5, int address) { + putOptionalInt(&result->metadata, "extendedVersion", dt5.getExtendedVersion(address)); + putOptionalInt(&result->metadata, "dimmingCurve", dt5.getDimmingCurve(address)); + putOptionalInt(&result->metadata, "outputLevelRaw", dt5.getOutputLevelRaw(address)); + if (const auto features = dt5.getConverterFeatures(address)) { + putRaw(&result->metadata, "featuresRaw", features->raw()); + result->metadata["outputRange0To10VSelectable"] = features->outputRange0To10VSelectable(); + result->metadata["internalPullUpSelectable"] = features->internalPullUpSelectable(); + result->metadata["outputFaultDetectionSelectable"] = features->outputFaultDetectionSelectable(); + result->metadata["mainsRelay"] = features->mainsRelay(); + result->metadata["outputLevelQueryable"] = features->outputLevelQueryable(); + result->metadata["nonLogarithmicDimmingCurveSupported"] = + features->nonLogarithmicDimmingCurveSupported(); + result->metadata["physicalSelectionByOutputLossSupported"] = + features->physicalSelectionByOutputLossSupported(); + result->metadata["physicalSelectionSwitchSupported"] = features->physicalSelectionSwitchSupported(); + } + if (const auto failure = dt5.getFailureStatus(address)) { + putRaw(&result->metadata, "failureRaw", failure->raw()); + result->metadata["outputFaultDetected"] = failure->outputFaultDetected(); + } + if (const auto status = dt5.getConverterStatus(address)) { + putRaw(&result->metadata, "converterStatusRaw", status->raw()); + result->metadata["zeroToTenVoltOperation"] = status->zeroToTenVoltOperation(); + result->metadata["internalPullUpOn"] = status->internalPullUpOn(); + result->metadata["nonLogarithmicDimmingCurveActive"] = + status->nonLogarithmicDimmingCurveActive(); + } +} + +void addDt6SnapshotMetadata(DaliBridgeResult* result, DaliDT6& dt6, int address) { + putOptionalInt(&result->metadata, "extendedVersion", dt6.getExtendedVersion(address)); + putOptionalInt(&result->metadata, "dimmingCurve", dt6.getDimmingCurve(address)); + putOptionalInt(&result->metadata, "fastFadeTime", dt6.getFastFadeTime(address)); + putOptionalInt(&result->metadata, "minFastFadeTime", dt6.getMinFastFadeTime(address)); + if (const auto gear = dt6.getGearType(address)) { + putRaw(&result->metadata, "gearTypeRaw", gear->raw()); + result->metadata["ledPowerSupplyIntegrated"] = gear->ledPowerSupplyIntegrated(); + result->metadata["ledModuleIntegrated"] = gear->ledModuleIntegrated(); + result->metadata["acSupplyPossible"] = gear->acSupplyPossible(); + result->metadata["dcSupplyPossible"] = gear->dcSupplyPossible(); + } + if (const auto modes = dt6.getPossibleOperatingModes(address)) { + putRaw(&result->metadata, "possibleOperatingModesRaw", modes->raw()); + result->metadata["pwmModePossible"] = modes->pwmModePossible(); + result->metadata["amModePossible"] = modes->amModePossible(); + result->metadata["currentControlledOutputPossible"] = modes->currentControlledOutput(); + result->metadata["highCurrentPulseModePossible"] = modes->highCurrentPulseMode(); + } + if (const auto features = dt6.getFeatures(address)) { + putRaw(&result->metadata, "featuresRaw", features->raw()); + result->metadata["canQueryShortCircuit"] = features->canQueryShortCircuit(); + result->metadata["canQueryOpenCircuit"] = features->canQueryOpenCircuit(); + result->metadata["canQueryLoadDecrease"] = features->canQueryLoadDecrease(); + result->metadata["canQueryLoadIncrease"] = features->canQueryLoadIncrease(); + result->metadata["canQueryCurrentProtector"] = features->canQueryCurrentProtector(); + result->metadata["canQueryThermalShutdown"] = features->canQueryThermalShutdown(); + result->metadata["canQueryThermalOverloadReduction"] = features->canQueryThermalOverloadReduction(); + result->metadata["physicalSelectionSupported"] = features->physicalSelectionSupported(); + } + if (const auto failure = dt6.getFailureStatus(address)) { + putRaw(&result->metadata, "failureRaw", failure->raw()); + result->metadata["shortCircuit"] = failure->shortCircuit(); + result->metadata["openCircuit"] = failure->openCircuit(); + result->metadata["loadDecrease"] = failure->loadDecrease(); + result->metadata["loadIncrease"] = failure->loadIncrease(); + result->metadata["currentProtectorActive"] = failure->currentProtectorActive(); + result->metadata["thermalShutdown"] = failure->thermalShutdown(); + result->metadata["thermalOverloadReduction"] = failure->thermalOverloadReduction(); + result->metadata["referenceMeasurementFailed"] = failure->referenceMeasurementFailed(); + } + if (const auto mode = dt6.getOperatingMode(address)) { + putRaw(&result->metadata, "operatingModeRaw", mode->raw()); + result->metadata["pwmModeActive"] = mode->pwmModeActive(); + result->metadata["amModeActive"] = mode->amModeActive(); + result->metadata["currentControlledOutput"] = mode->currentControlledOutput(); + result->metadata["highCurrentPulseModeActive"] = mode->highCurrentPulseModeActive(); + result->metadata["nonLogarithmicDimmingCurveActive"] = mode->nonLogarithmicDimmingCurveActive(); + } +} + +DaliValue::Array intArray(const std::vector& values) { + DaliValue::Array out; + out.reserve(values.size()); + for (const int value : values) out.emplace_back(value); + return out; +} + } // namespace DaliValue::Object DaliBridgeResult::toJson() const { @@ -29,38 +382,38 @@ DaliValue::Object DaliBridgeResult::toJson() const { if (data.has_value()) out["data"] = data.value(); if (!error.empty()) out["error"] = error; if (!metadata.empty()) out["meta"] = metadata; + if (!results.empty()) out["results"] = results; return out; } -DaliBridgeEngine::DaliBridgeEngine(DaliComm& comm) : comm_(comm), base_(comm), dt1_(base_), dt8_(base_) {} - +DaliBridgeEngine::DaliBridgeEngine(DaliComm& comm) + : comm_(comm), + base_(comm), + dt1_(base_), + dt4_(base_), + dt5_(base_), + dt6_(base_), + dt8_(base_), + addr_(base_) {} bool DaliBridgeEngine::upsertModel(const BridgeModel& model) { - if (model.id.empty()) { - return false; - } + if (model.id.empty()) return false; models_[model.id] = model; return true; } -bool DaliBridgeEngine::removeModel(const std::string& modelID) { - return models_.erase(modelID) > 0; -} +bool DaliBridgeEngine::removeModel(const std::string& modelID) { return models_.erase(modelID) > 0; } const BridgeModel* DaliBridgeEngine::findModel(const std::string& modelID) const { const auto it = models_.find(modelID); - if (it == models_.end()) { - return nullptr; - } + if (it == models_.end()) return nullptr; return &it->second; } std::vector DaliBridgeEngine::listModels() const { std::vector out; out.reserve(models_.size()); - for (const auto& entry : models_) { - out.push_back(entry.second); - } + for (const auto& entry : models_) out.push_back(entry.second); return out; } @@ -103,6 +456,43 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ result.metadata["modelName"] = model->displayName(); } + if (operation == BridgeOperation::batch) { + const DaliValue::Array* items = request.value.asArray(); + if (items == nullptr) { + if (const auto* requests = paramValueAny(request, {"requests", "steps", "items"})) { + items = requests->asArray(); + } + } + if (items == nullptr) { + result.error = "batch requires requests array"; + return result; + } + bool allOk = true; + int index = 0; + for (const auto& item : *items) { + const auto* object = item.asObject(); + if (object == nullptr) { + DaliBridgeResult child; + child.sequence = request.sequence + ":" + std::to_string(index); + child.error = "batch item must be object"; + allOk = false; + result.results.emplace_back(child.toJson()); + ++index; + continue; + } + DaliBridgeRequest childRequest = bridgeRequestFromValue(*object, &request, index); + const DaliBridgeResult child = execute(childRequest); + allOk = allOk && child.ok; + result.results.emplace_back(child.toJson()); + ++index; + } + result.ok = allOk; + result.data = static_cast(result.results.size()); + result.metadata["count"] = static_cast(result.results.size()); + if (!allOk) result.error = "one or more batch operations failed"; + return result; + } + switch (operation) { case BridgeOperation::send: case BridgeOperation::sendExt: @@ -114,11 +504,14 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ result.error = "invalid addr/cmd"; return result; } + if (operation == BridgeOperation::query && addr.value() >= 0x80) { + result.error = "query target must be a short address"; + return result; + } if (operation == BridgeOperation::send) { result.ok = comm_.sendRaw(static_cast(addr.value()), static_cast(cmd.value())); } else if (operation == BridgeOperation::sendExt) { - result.ok = - comm_.sendExtRaw(static_cast(addr.value()), static_cast(cmd.value())); + result.ok = comm_.sendExtRaw(static_cast(addr.value()), static_cast(cmd.value())); } else { const auto response = comm_.queryRaw(static_cast(addr.value()), static_cast(cmd.value())); @@ -129,300 +522,500 @@ DaliBridgeResult DaliBridgeEngine::executeResolved(const DaliBridgeRequest& requ result.ok = true; result.data = static_cast(response.value()); } - if (!result.ok && result.error.empty()) { - result.error = "dispatch failed"; - } - return result; - } - case BridgeOperation::setBrightness: { - const auto address = resolveTargetAddress(request, model); - const auto value = resolveIntValue(request, model); - if (!address.has_value() || !value.has_value()) { - result.error = "missing address/value"; - return result; - } - result.ok = base_.setBright(address.value(), value.value()); - result.data = value; - break; - } - case BridgeOperation::setBrightnessPercent: { - const auto address = resolveTargetAddress(request, model); - const auto value = resolveDoubleValue(request); - if (!address.has_value() || !value.has_value()) { - result.error = "missing address/value"; - return result; - } - result.ok = base_.setBrightPercentage(address.value(), value.value()); - result.data = static_cast(std::lround(value.value())); - break; - } - case BridgeOperation::on: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - result.ok = base_.on(address.value()); - break; - } - case BridgeOperation::off: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - result.ok = base_.off(address.value()); - break; - } - case BridgeOperation::recallMaxLevel: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - result.ok = base_.recallMaxLevel(address.value()); break; } + case BridgeOperation::setBrightness: + case BridgeOperation::setBrightnessPercent: + case BridgeOperation::on: + case BridgeOperation::off: + case BridgeOperation::recallMaxLevel: case BridgeOperation::recallMinLevel: { const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { result.error = "missing target address"; return result; } - result.ok = base_.recallMinLevel(address.value()); + if (operation == BridgeOperation::setBrightness) { + const auto value = resolveIntValue(request, model); + if (!value.has_value()) { + result.error = "missing value"; + return result; + } + result.ok = base_.setBright(address.value(), value.value()); + result.data = value.value(); + } else if (operation == BridgeOperation::setBrightnessPercent) { + const auto value = resolveDoubleValue(request); + if (!value.has_value()) { + result.error = "missing value"; + return result; + } + result.ok = base_.setBrightPercentage(address.value(), value.value()); + result.data = static_cast(std::lround(value.value())); + } else if (operation == BridgeOperation::on) { + result.ok = base_.on(address.value()); + } else if (operation == BridgeOperation::off) { + result.ok = base_.off(address.value()); + } else if (operation == BridgeOperation::recallMaxLevel) { + result.ok = base_.recallMaxLevel(address.value()); + } else { + result.ok = base_.recallMinLevel(address.value()); + } break; } - case BridgeOperation::setColorTemperature: { + case BridgeOperation::setColorTemperature: + case BridgeOperation::setColorTemperatureRaw: + case BridgeOperation::setColourXY: + case BridgeOperation::setColourRGB: + case BridgeOperation::setTemporaryColourTemperature: + case BridgeOperation::setTemporaryColourXY: + case BridgeOperation::setTemporaryRGBDimLevels: + case BridgeOperation::setTemporaryColourMask: + case BridgeOperation::activateTemporaryColour: + case BridgeOperation::copyReportToTemporary: { const auto address = resolveTargetAddress(request, model); - const auto value = resolveIntValue(request, model); + if (!address.has_value()) { + result.error = "missing target address"; + return result; + } + if (operation == BridgeOperation::setColorTemperature) { + const auto value = resolveIntValue(request, model); + if (!value.has_value()) { + result.error = "missing value"; + return result; + } + result.ok = dt8_.setColorTemperature(address.value(), value.value()); + result.data = value.value(); + } else if (operation == BridgeOperation::setColorTemperatureRaw) { + const auto value = intParam(request, {"mirek", "raw", "value"}); + if (!value.has_value()) { + result.error = "missing mirek value"; + return result; + } + result.ok = dt8_.setColTempRaw(address.value(), value.value()); + result.data = value.value(); + } else if (operation == BridgeOperation::setColourXY) { + const auto x = doubleParam(request, {"x"}); + const auto y = doubleParam(request, {"y"}); + if (!x.has_value() || !y.has_value()) { + result.error = "missing x/y"; + return result; + } + result.ok = dt8_.setColour(address.value(), x.value(), y.value()); + } else if (operation == BridgeOperation::setColourRGB) { + const auto r = intParam(request, {"red", "r"}); + const auto g = intParam(request, {"green", "g"}); + const auto b = intParam(request, {"blue", "b"}); + if (!r.has_value() || !g.has_value() || !b.has_value()) { + result.error = "missing rgb"; + return result; + } + result.ok = dt8_.setColourRGB(address.value(), r.value(), g.value(), b.value()); + } else if (operation == BridgeOperation::setTemporaryColourTemperature) { + const auto value = intParam(request, {"colorTemperature", "colourTemperature", "kelvin", "value"}); + if (!value.has_value()) { + result.error = "missing color temperature"; + return result; + } + result.ok = dt8_.setTemporaryColourTemperature(address.value(), value.value()); + result.data = value.value(); + } else if (operation == BridgeOperation::setTemporaryColourXY) { + const auto x = doubleParam(request, {"x"}); + const auto y = doubleParam(request, {"y"}); + if (!x.has_value() || !y.has_value()) { + result.error = "missing x/y"; + return result; + } + result.ok = dt8_.setTemporaryColourXY(address.value(), x.value(), y.value()); + } else if (operation == BridgeOperation::setTemporaryRGBDimLevels) { + const auto r = intParam(request, {"red", "r"}); + const auto g = intParam(request, {"green", "g"}); + const auto b = intParam(request, {"blue", "b"}); + if (!r.has_value() || !g.has_value() || !b.has_value()) { + result.error = "missing rgb"; + return result; + } + result.ok = dt8_.setTemporaryRGBDimLevels(address.value(), r.value(), g.value(), b.value()); + } else if (operation == BridgeOperation::setTemporaryColourMask) { + result.ok = dt8_.setTemporaryColourMask(address.value()); + } else if (operation == BridgeOperation::activateTemporaryColour) { + result.ok = dt8_.activateTemporaryColour(address.value()); + } else { + result.ok = dt8_.copyReportToTemporary(address.value()); + } + break; + } + case BridgeOperation::storeDt8SceneSnapshot: { + const auto address = resolveTargetAddress(request, model); + const auto scene = intParam(request, {"scene"}); + const auto brightness = intParam(request, {"brightness", "level", "value"}); + if (!address.has_value() || !scene.has_value() || !brightness.has_value()) { + result.error = "missing address/scene/brightness"; + return result; + } + Dt8SceneStoreColorMode mode = Dt8SceneStoreColorMode::disabled; + const std::string modeName = stringParam(request, {"colorMode", "colourMode", "color_mode"}).value_or(""); + if (modeName == "ct" || modeName == "colorTemperature" || modeName == "colourTemperature" || + modeName == "color_temperature" || modeName == "colour_temperature") { + mode = Dt8SceneStoreColorMode::colorTemperature; + } else if (modeName == "rgb") { + mode = Dt8SceneStoreColorMode::rgb; + } else if (intParam(request, {"colorTemperature", "colourTemperature"}).has_value()) { + mode = Dt8SceneStoreColorMode::colorTemperature; + } else if (intParam(request, {"red", "r"}).has_value()) { + mode = Dt8SceneStoreColorMode::rgb; + } + result.ok = dt8_.storeSceneSnapshot( + address.value(), scene.value(), brightness.value(), mode, + intParam(request, {"colorTemperature", "colourTemperature"}).value_or(0), + intParam(request, {"red", "r"}).value_or(0), intParam(request, {"green", "g"}).value_or(0), + intParam(request, {"blue", "b"}).value_or(0)); + break; + } + case BridgeOperation::storeDt8PowerOnLevelSnapshot: + case BridgeOperation::storeDt8SystemFailureLevelSnapshot: { + const auto address = resolveTargetAddress(request, model); + const auto level = intParam(request, {"level", "brightness", "value"}); + if (!address.has_value() || !level.has_value()) { + result.error = "missing address/level"; + return result; + } + result.ok = operation == BridgeOperation::storeDt8PowerOnLevelSnapshot + ? dt8_.storePowerOnLevelSnapshot(address.value(), level.value()) + : dt8_.storeSystemFailureLevelSnapshot(address.value(), level.value()); + result.data = level.value(); + break; + } + case BridgeOperation::getBrightness: + case BridgeOperation::getStatus: + case BridgeOperation::getColorTemperature: + case BridgeOperation::getColorStatus: + case BridgeOperation::getEmergencyLevel: + case BridgeOperation::getEmergencyStatus: + case BridgeOperation::getEmergencyFailureStatus: + case BridgeOperation::getDt1Snapshot: + case BridgeOperation::getDt4Snapshot: + case BridgeOperation::getDt5Snapshot: + case BridgeOperation::getDt6Snapshot: + case BridgeOperation::getDt8StatusSnapshot: + case BridgeOperation::getGroupMask: + case BridgeOperation::getSceneLevel: + case BridgeOperation::getSceneMap: + case BridgeOperation::getAddressSettings: { + const auto address = resolveTargetAddress(request, model); + if (!address.has_value()) { + result.error = "missing target address"; + return result; + } + if (isReadOperation(operation) && !isShortAddress(address.value())) { + result.error = "query target must be a short address"; + return result; + } + if (operation == BridgeOperation::getBrightness) { + const auto value = base_.getBright(address.value()); + if (!value.has_value()) { + result.error = "no response"; + return result; + } + result.ok = true; + result.data = value.value(); + } else if (operation == BridgeOperation::getStatus) { + const auto value = base_.getStatus(address.value()); + if (!value.has_value()) { + result.error = "no response"; + return result; + } + result.ok = true; + result.data = value.value(); + addStatusMetadata(&result, value.value()); + } else if (operation == BridgeOperation::getColorTemperature) { + const auto value = dt8_.getColorTemperature(address.value()); + if (!value.has_value()) { + result.error = "no response"; + return result; + } + result.ok = true; + result.data = value.value(); + } else if (operation == BridgeOperation::getColorStatus || + operation == BridgeOperation::getDt8StatusSnapshot) { + bool hasData = false; + if (const auto value = dt8_.getColorStatus(address.value())) { + hasData = true; + result.data = value->raw(); + addColorStatusMetadata(&result.metadata, value.value()); + } + if (const auto features = dt8_.getColorTypeFeature(address.value())) { + hasData = true; + addColorFeatureMetadata(&result.metadata, features.value()); + } + if (!hasData) { + result.error = "no response"; + return result; + } + result.ok = true; + } else if (operation == BridgeOperation::getEmergencyLevel) { + const auto value = dt1_.getEmergencyLevel(address.value()); + if (!value.has_value()) { + result.error = "no response"; + return result; + } + result.ok = true; + result.data = value.value(); + } else if (operation == BridgeOperation::getEmergencyStatus) { + const auto status = dt1_.getEmergencyStatusDecoded(address.value()); + if (!status.has_value()) { + result.error = "no response"; + return result; + } + result.ok = true; + result.data = status->raw(); + putRaw(&result.metadata, "emergencyStatusRaw", status->raw()); + result.metadata["inhibitMode"] = status->inhibitMode(); + result.metadata["functionTestResultValid"] = status->functionTestResultValid(); + result.metadata["durationTestResultValid"] = status->durationTestResultValid(); + result.metadata["batteryFullyCharged"] = status->batteryFullyCharged(); + result.metadata["functionTestRequestPending"] = status->functionTestRequestPending(); + result.metadata["durationTestRequestPending"] = status->durationTestRequestPending(); + result.metadata["identificationActive"] = status->identificationActive(); + result.metadata["physicallySelected"] = status->physicallySelected(); + } else if (operation == BridgeOperation::getEmergencyFailureStatus || + operation == BridgeOperation::getDt1Snapshot) { + const auto status = dt1_.getDT1TestStatusDetailed(address.value()); + if (!status.has_value()) { + result.error = "no response"; + return result; + } + result.ok = true; + if (status->failureStatus.has_value()) result.data = status->failureStatus.value(); + addDt1DetailedMetadata(&result, status.value()); + } else if (operation == BridgeOperation::getDt4Snapshot) { + addDt4SnapshotMetadata(&result, dt4_, address.value()); + result.ok = !result.metadata.empty(); + } else if (operation == BridgeOperation::getDt5Snapshot) { + addDt5SnapshotMetadata(&result, dt5_, address.value()); + result.ok = !result.metadata.empty(); + } else if (operation == BridgeOperation::getDt6Snapshot) { + addDt6SnapshotMetadata(&result, dt6_, address.value()); + result.ok = !result.metadata.empty(); + } else if (operation == BridgeOperation::getGroupMask) { + const auto value = base_.getGroup(address.value()); + if (!value.has_value()) { + result.error = "no response"; + return result; + } + result.ok = true; + result.data = value.value(); + result.metadata["groupMask"] = value.value(); + } else if (operation == BridgeOperation::getSceneLevel) { + const auto scene = intParam(request, {"scene"}); + if (!scene.has_value()) { + result.error = "missing scene"; + return result; + } + const auto value = base_.getScene(address.value(), scene.value()); + if (!value.has_value()) { + result.error = "no response"; + return result; + } + result.ok = true; + result.data = value.value(); + result.metadata["scene"] = scene.value(); + } else if (operation == BridgeOperation::getSceneMap) { + const auto scenes = base_.getScenes(address.value()); + DaliValue::Object sceneObject; + for (const auto& entry : scenes) sceneObject[std::to_string(entry.first)] = entry.second; + result.ok = true; + result.data = static_cast(scenes.size()); + result.metadata["scenes"] = std::move(sceneObject); + } else { + putOptionalInt(&result.metadata, "powerOnLevel", base_.getPowerOnLevel(address.value())); + putOptionalInt(&result.metadata, "systemFailureLevel", base_.getSystemFailureLevel(address.value())); + putOptionalInt(&result.metadata, "minLevel", base_.getMinLevel(address.value())); + putOptionalInt(&result.metadata, "maxLevel", base_.getMaxLevel(address.value())); + putOptionalInt(&result.metadata, "fadeTime", base_.getFadeTime(address.value())); + putOptionalInt(&result.metadata, "fadeRate", base_.getFadeRate(address.value())); + result.ok = !result.metadata.empty(); + } + break; + } + case BridgeOperation::setGroupMask: { + const auto address = resolveTargetAddress(request, model); + const auto value = intParam(request, {"groupMask", "group_mask", "mask", "value"}); if (!address.has_value() || !value.has_value()) { - result.error = "missing address/value"; + result.error = "missing address/group mask"; return result; } - result.ok = dt8_.setColorTemperature(address.value(), value.value()); - result.data = value; + result.ok = base_.setGroup(address.value(), value.value()); + result.data = value.value(); break; } - case BridgeOperation::getBrightness: { + case BridgeOperation::setSceneLevel: + case BridgeOperation::removeSceneLevel: { + const auto address = resolveTargetAddress(request, model); + const auto scene = intParam(request, {"scene"}); + if (!address.has_value() || !scene.has_value()) { + result.error = "missing address/scene"; + return result; + } + if (operation == BridgeOperation::removeSceneLevel) { + result.ok = base_.removeScene(address.value(), scene.value()); + } else { + const auto level = intParam(request, {"level", "brightness", "value"}); + if (!level.has_value()) { + result.error = "missing scene level"; + return result; + } + result.ok = base_.setDTR(level.value()) && base_.storeDTRAsSceneBright(address.value(), scene.value()); + result.data = level.value(); + } + result.metadata["scene"] = scene.value(); + break; + } + case BridgeOperation::setAddressSettings: { const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { result.error = "missing target address"; return result; } - const auto value = base_.getBright(address.value()); - if (!value.has_value()) { - result.error = "no response"; + bool sawSetting = false; + bool ok = true; + if (const auto value = intParam(request, {"powerOnLevel", "power_on_level"})) { + sawSetting = true; + ok = ok && base_.setPowerOnLevel(address.value(), value.value()); + } + if (const auto value = intParam(request, {"systemFailureLevel", "system_failure_level"})) { + sawSetting = true; + ok = ok && base_.setSystemFailureLevel(address.value(), value.value()); + } + if (const auto value = intParam(request, {"minLevel", "min_level"})) { + sawSetting = true; + ok = ok && base_.setMinLevel(address.value(), value.value()); + } + if (const auto value = intParam(request, {"maxLevel", "max_level"})) { + sawSetting = true; + ok = ok && base_.setMaxLevel(address.value(), value.value()); + } + if (const auto value = intParam(request, {"fadeTime", "fade_time"})) { + sawSetting = true; + ok = ok && base_.setFadeTime(address.value(), value.value()); + } + if (const auto value = intParam(request, {"fadeRate", "fade_rate"})) { + sawSetting = true; + ok = ok && base_.setFadeRate(address.value(), value.value()); + } + if (!sawSetting) { + result.error = "missing settings"; return result; } + result.ok = ok; + break; + } + case BridgeOperation::searchAddressRange: { + const int start = intParam(request, {"start", "startAddress", "start_address"}).value_or(0); + const int end = intParam(request, {"end", "endAddress", "end_address"}).value_or(63); + const auto addresses = addr_.searchAddrRange(start, end); result.ok = true; - result.data = value; + result.data = static_cast(addresses.size()); + result.metadata["addresses"] = intArray(addresses); break; } - case BridgeOperation::getStatus: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; + case BridgeOperation::allocateAllShortAddresses: + case BridgeOperation::resetAndAllocateShortAddresses: + case BridgeOperation::stopAddressAllocation: { + if (operation == BridgeOperation::stopAddressAllocation) { + addr_.stopAllocAddr(); + result.ok = true; + break; } - const auto value = base_.getStatus(address.value()); - if (!value.has_value()) { - result.error = "no response"; - return result; + const int start = intParam(request, {"start", "startAddress", "start_address"}).value_or(0); + if (operation == BridgeOperation::allocateAllShortAddresses) { + result.ok = addr_.allocateAllAddr(start); + } else { + result.ok = addr_.resetAndAllocAddr( + start, boolParam(request, {"removeAddrFirst", "remove_addr_first", "removeAddr"}).value_or(false), + boolParam(request, {"closeLight", "close_light"}).value_or(false)); } - result.ok = true; - result.data = value; - addStatusMetadata(&result, value.value()); - break; - } - case BridgeOperation::getColorTemperature: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - const auto value = dt8_.getColorTemperature(address.value()); - if (!value.has_value()) { - result.error = "no response"; - return result; - } - result.ok = true; - result.data = value; - break; - } - case BridgeOperation::getColorStatus: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - const auto value = dt8_.getColorStatus(address.value()); - if (!value.has_value()) { - result.error = "no response"; - return result; - } - result.ok = true; - result.data = value->raw(); - result.metadata["xyOutOfRange"] = value->xyOutOfRange(); - result.metadata["ctOutOfRange"] = value->ctOutOfRange(); - result.metadata["autoCalibrationActive"] = value->autoCalibrationActive(); - result.metadata["autoCalibrationSuccess"] = value->autoCalibrationSuccess(); - result.metadata["xyActive"] = value->xyActive(); - result.metadata["ctActive"] = value->ctActive(); - result.metadata["primaryNActive"] = value->primaryNActive(); - result.metadata["rgbwafActive"] = value->rgbwafActive(); - break; - } - case BridgeOperation::getEmergencyLevel: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - const auto value = dt1_.getEmergencyLevel(address.value()); - if (!value.has_value()) { - result.error = "no response"; - return result; - } - result.ok = true; - result.data = value; - break; - } - case BridgeOperation::getEmergencyStatus: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - const auto status = dt1_.getEmergencyStatusDecoded(address.value()); - if (!status.has_value()) { - result.error = "no response"; - return result; - } - result.ok = true; - result.data = status->raw(); - result.metadata["inhibitMode"] = status->inhibitMode(); - result.metadata["functionTestResultValid"] = status->functionTestResultValid(); - result.metadata["durationTestResultValid"] = status->durationTestResultValid(); - result.metadata["batteryFullyCharged"] = status->batteryFullyCharged(); - result.metadata["functionTestRequestPending"] = status->functionTestRequestPending(); - result.metadata["durationTestRequestPending"] = status->durationTestRequestPending(); - result.metadata["identificationActive"] = status->identificationActive(); - result.metadata["physicallySelected"] = status->physicallySelected(); - break; - } - case BridgeOperation::getEmergencyFailureStatus: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - const auto status = dt1_.getDT1TestStatusDetailed(address.value()); - if (!status.has_value()) { - result.error = "no response"; - return result; - } - result.ok = true; - result.data = status->failureStatus; - if (status->emergencyStatus.has_value()) result.metadata["emergencyStatus"] = status->emergencyStatus.value(); - if (status->emergencyMode.has_value()) result.metadata["emergencyMode"] = status->emergencyMode.value(); - if (status->feature.has_value()) result.metadata["feature"] = status->feature.value(); - result.metadata["testInProgress"] = status->testInProgress; - result.metadata["lampFailure"] = status->lampFailure; - result.metadata["batteryFailure"] = status->batteryFailure; - result.metadata["functionTestActive"] = status->functionTestActive; - result.metadata["durationTestActive"] = status->durationTestActive; - result.metadata["testDone"] = status->testDone; - result.metadata["identifyActive"] = status->identifyActive; - result.metadata["physicalSelectionActive"] = status->physicalSelectionActive; - break; - } - case BridgeOperation::startEmergencyFunctionTest: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - result.ok = dt1_.startFunctionTestCmd(address.value()); - break; - } - case BridgeOperation::startEmergencyDurationTest: { - const auto address = resolveTargetAddress(request, model); - if (!address.has_value()) { - result.error = "missing target address"; - return result; - } - result.ok = dt1_.startDurationTestCmd(address.value()); + result.data = addr_.lastAllocAddr(); + result.metadata["lastShortAddress"] = addr_.lastAllocAddr(); + result.metadata["isAllocating"] = addr_.isAllocAddr(); break; } + case BridgeOperation::startEmergencyFunctionTest: + case BridgeOperation::startEmergencyDurationTest: case BridgeOperation::stopEmergencyTest: { const auto address = resolveTargetAddress(request, model); if (!address.has_value()) { result.error = "missing target address"; return result; } - result.ok = dt1_.stopTest(address.value()); + if (operation == BridgeOperation::startEmergencyFunctionTest) { + result.ok = dt1_.startFunctionTestCmd(address.value()); + } else if (operation == BridgeOperation::startEmergencyDurationTest) { + result.ok = dt1_.startDurationTestCmd(address.value()); + } else { + result.ok = dt1_.stopTest(address.value()); + } break; } case BridgeOperation::unknown: + case BridgeOperation::batch: default: result.error = "unsupported op"; return result; } - if (!result.ok && result.error.empty()) { - result.error = "dispatch failed"; - } + if (!result.ok && result.error.empty()) result.error = "dispatch failed"; return result; } std::optional DaliBridgeEngine::resolveTargetAddress(const DaliBridgeRequest& request, const BridgeModel* model) const { - if (request.shortAddress.has_value()) { - return request.shortAddress; + if (boolParam(request, {"broadcast"}).value_or(false)) return 127; + if (const auto group = intParam(request, {"groupAddress", "group", "group_address"})) { + return 64 + group.value(); } - if (model != nullptr) { - return model->dali.logicalAddress(); + if (request.shortAddress.has_value()) return request.shortAddress; + if (const auto address = intParam(request, {"address", "target", "targetAddress"})) { + return address.value(); } + if (model != nullptr) return model->dali.logicalAddress(); return std::nullopt; } std::optional DaliBridgeEngine::resolveRawAddress(const DaliBridgeRequest& request, const BridgeModel* model) const { - if (request.rawAddress.has_value()) { - return request.rawAddress; - } - if (model != nullptr) { - return model->dali.rawAddress; - } + if (request.rawAddress.has_value()) return request.rawAddress; + if (const auto address = intParam(request, {"rawAddress", "addr"})) return address.value(); + if (model != nullptr) return model->dali.rawAddress; return std::nullopt; } std::optional DaliBridgeEngine::resolveRawCommand(const DaliBridgeRequest& request, const BridgeModel* model) const { - if (request.rawCommand.has_value()) { - return request.rawCommand; - } - if (model != nullptr) { - return model->dali.rawCommand; - } + if (request.rawCommand.has_value()) return request.rawCommand; + if (const auto command = intParam(request, {"rawCommand", "cmd"})) return command.value(); + if (model != nullptr) return model->dali.rawCommand; return std::nullopt; } std::optional DaliBridgeEngine::resolveIntValue(const DaliBridgeRequest& request, const BridgeModel* model) const { - if (request.value.isNull()) { - return std::nullopt; - } + const DaliValue* value = &request.value; + if (request.value.isObject()) value = paramValueAny(request, {"value", "level", "brightness"}); + if (value == nullptr || value->isNull()) return std::nullopt; if (model != nullptr) { - const auto number = request.value.asDouble(); - if (!number.has_value()) { - return std::nullopt; - } + const auto number = value->asDouble(); + if (!number.has_value()) return std::nullopt; return model->valueTransform.apply(number.value()); } - return request.value.asInt(); + return value->asInt(); } std::optional DaliBridgeEngine::resolveDoubleValue(const DaliBridgeRequest& request) const { + if (request.value.isObject()) { + if (const auto* value = paramValueAny(request, {"value", "percent", "percentage"})) { + return value->asDouble(); + } + return std::nullopt; + } return request.value.asDouble(); -} \ No newline at end of file +} diff --git a/src/bridge_model.cpp b/src/bridge_model.cpp index 454f941..bf66045 100644 --- a/src/bridge_model.cpp +++ b/src/bridge_model.cpp @@ -349,6 +349,66 @@ const char* bridgeOperationToString(BridgeOperation operation) { return "stop_emergency_test"; case BridgeOperation::startEmergencyDurationTest: return "start_emergency_duration_test"; + case BridgeOperation::setColorTemperatureRaw: + return "set_color_temperature_raw"; + case BridgeOperation::setColourXY: + return "set_colour_xy"; + case BridgeOperation::setColourRGB: + return "set_colour_rgb"; + case BridgeOperation::setTemporaryColourTemperature: + return "set_temporary_colour_temperature"; + case BridgeOperation::setTemporaryColourXY: + return "set_temporary_colour_xy"; + case BridgeOperation::setTemporaryRGBDimLevels: + return "set_temporary_rgb_dim_levels"; + case BridgeOperation::setTemporaryColourMask: + return "set_temporary_colour_mask"; + case BridgeOperation::activateTemporaryColour: + return "activate_temporary_colour"; + case BridgeOperation::copyReportToTemporary: + return "copy_report_to_temporary"; + case BridgeOperation::storeDt8SceneSnapshot: + return "store_dt8_scene_snapshot"; + case BridgeOperation::storeDt8PowerOnLevelSnapshot: + return "store_dt8_power_on_level_snapshot"; + case BridgeOperation::storeDt8SystemFailureLevelSnapshot: + return "store_dt8_system_failure_level_snapshot"; + case BridgeOperation::getDt1Snapshot: + return "get_dt1_snapshot"; + case BridgeOperation::getDt4Snapshot: + return "get_dt4_snapshot"; + case BridgeOperation::getDt5Snapshot: + return "get_dt5_snapshot"; + case BridgeOperation::getDt6Snapshot: + return "get_dt6_snapshot"; + case BridgeOperation::getDt8StatusSnapshot: + return "get_dt8_status_snapshot"; + case BridgeOperation::getGroupMask: + return "get_group_mask"; + case BridgeOperation::setGroupMask: + return "set_group_mask"; + case BridgeOperation::getSceneLevel: + return "get_scene_level"; + case BridgeOperation::setSceneLevel: + return "set_scene_level"; + case BridgeOperation::removeSceneLevel: + return "remove_scene_level"; + case BridgeOperation::getSceneMap: + return "get_scene_map"; + case BridgeOperation::getAddressSettings: + return "get_address_settings"; + case BridgeOperation::setAddressSettings: + return "set_address_settings"; + case BridgeOperation::searchAddressRange: + return "search_address_range"; + case BridgeOperation::allocateAllShortAddresses: + return "allocate_all_short_addresses"; + case BridgeOperation::resetAndAllocateShortAddresses: + return "reset_and_allocate_short_addresses"; + case BridgeOperation::stopAddressAllocation: + return "stop_address_allocation"; + case BridgeOperation::batch: + return "batch"; case BridgeOperation::unknown: default: return "unknown"; @@ -381,6 +441,54 @@ BridgeOperation bridgeOperationFromString(const std::string& value) { if (normalized == "start_emergency_duration_test") { return BridgeOperation::startEmergencyDurationTest; } + if (normalized == "set_color_temperature_raw") return BridgeOperation::setColorTemperatureRaw; + if (normalized == "set_colour_xy" || normalized == "set_color_xy") return BridgeOperation::setColourXY; + if (normalized == "set_colour_rgb" || normalized == "set_color_rgb") return BridgeOperation::setColourRGB; + if (normalized == "set_temporary_colour_temperature" || normalized == "set_temporary_color_temperature") { + return BridgeOperation::setTemporaryColourTemperature; + } + if (normalized == "set_temporary_colour_xy" || normalized == "set_temporary_color_xy") { + return BridgeOperation::setTemporaryColourXY; + } + if (normalized == "set_temporary_rgb_dim_levels") return BridgeOperation::setTemporaryRGBDimLevels; + if (normalized == "set_temporary_colour_mask" || normalized == "set_temporary_color_mask") { + return BridgeOperation::setTemporaryColourMask; + } + if (normalized == "activate_temporary_colour" || normalized == "activate_temporary_color") { + return BridgeOperation::activateTemporaryColour; + } + if (normalized == "copy_report_to_temporary") return BridgeOperation::copyReportToTemporary; + if (normalized == "store_dt8_scene_snapshot") return BridgeOperation::storeDt8SceneSnapshot; + if (normalized == "store_dt8_power_on_level_snapshot" || normalized == "store_dt8_power_on_snapshot") { + return BridgeOperation::storeDt8PowerOnLevelSnapshot; + } + if (normalized == "store_dt8_system_failure_level_snapshot" || normalized == "store_dt8_system_failure_snapshot") { + return BridgeOperation::storeDt8SystemFailureLevelSnapshot; + } + if (normalized == "get_dt1_snapshot" || normalized == "dt1_snapshot") return BridgeOperation::getDt1Snapshot; + if (normalized == "get_dt4_snapshot" || normalized == "dt4_snapshot") return BridgeOperation::getDt4Snapshot; + if (normalized == "get_dt5_snapshot" || normalized == "dt5_snapshot") return BridgeOperation::getDt5Snapshot; + if (normalized == "get_dt6_snapshot" || normalized == "dt6_snapshot") return BridgeOperation::getDt6Snapshot; + if (normalized == "get_dt8_status_snapshot" || normalized == "dt8_status_snapshot") { + return BridgeOperation::getDt8StatusSnapshot; + } + if (normalized == "get_group_mask") return BridgeOperation::getGroupMask; + if (normalized == "set_group_mask") return BridgeOperation::setGroupMask; + if (normalized == "get_scene_level") return BridgeOperation::getSceneLevel; + if (normalized == "set_scene_level") return BridgeOperation::setSceneLevel; + if (normalized == "remove_scene_level") return BridgeOperation::removeSceneLevel; + if (normalized == "get_scene_map" || normalized == "get_scenes") return BridgeOperation::getSceneMap; + if (normalized == "get_address_settings") return BridgeOperation::getAddressSettings; + if (normalized == "set_address_settings") return BridgeOperation::setAddressSettings; + if (normalized == "search_address_range") return BridgeOperation::searchAddressRange; + if (normalized == "allocate_all_short_addresses" || normalized == "allocate_all_addr") { + return BridgeOperation::allocateAllShortAddresses; + } + if (normalized == "reset_and_allocate_short_addresses" || normalized == "reset_and_alloc_addr") { + return BridgeOperation::resetAndAllocateShortAddresses; + } + if (normalized == "stop_address_allocation") return BridgeOperation::stopAddressAllocation; + if (normalized == "batch") return BridgeOperation::batch; return BridgeOperation::unknown; } diff --git a/src/gateway_cloud.cpp b/src/gateway_cloud.cpp index 81832da..267f465 100644 --- a/src/gateway_cloud.cpp +++ b/src/gateway_cloud.cpp @@ -1,5 +1,6 @@ #include "gateway_cloud.hpp" +#include #include #ifdef ESP_PLATFORM @@ -33,6 +34,42 @@ int toInt(const cJSON* item, int fallback) { return item->valueint; } +DaliValue fromCjson(const cJSON* item) { + if (item == nullptr || cJSON_IsNull(item)) { + return DaliValue(); + } + if (cJSON_IsBool(item)) { + return DaliValue(cJSON_IsTrue(item)); + } + if (cJSON_IsNumber(item)) { + const double value = item->valuedouble; + if (value == static_cast(item->valueint)) { + return DaliValue(item->valueint); + } + return DaliValue(value); + } + if (cJSON_IsString(item) && item->valuestring != nullptr) { + return DaliValue(std::string(item->valuestring)); + } + if (cJSON_IsArray(item)) { + DaliValue::Array out; + for (const cJSON* child = item->child; child != nullptr; child = child->next) { + out.push_back(fromCjson(child)); + } + return DaliValue(std::move(out)); + } + if (cJSON_IsObject(item)) { + DaliValue::Object out; + for (const cJSON* child = item->child; child != nullptr; child = child->next) { + if (child->string != nullptr) { + out[child->string] = fromCjson(child); + } + } + return DaliValue(std::move(out)); + } + return DaliValue(); +} + cJSON* toCjson(const DaliValue& value) { if (value.isNull()) { return cJSON_CreateNull(); @@ -66,6 +103,18 @@ cJSON* toCjson(const DaliValue& value) { return cJSON_CreateNull(); } +bool isKnownBridgeRequestKey(const char* key) { + if (key == nullptr) return true; + static const char* known[] = {"type", "seq", "sequence", "model", + "modelID", "modelId", "op", "operation", + "addr", "rawAddress", "cmd", "rawCommand", + "shortAddress", "short_address", "value", "meta"}; + for (const char* item : known) { + if (std::strcmp(key, item) == 0) return true; + } + return false; +} + } // namespace #endif @@ -169,45 +218,58 @@ bool DaliCloudBridge::handleDownlink(const std::string& payload) { return false; } - const cJSON* seqItem = cJSON_GetObjectItemCaseSensitive(root, "seq"); - const cJSON* modelItem = cJSON_GetObjectItemCaseSensitive(root, "model"); - const cJSON* opItem = cJSON_GetObjectItemCaseSensitive(root, "op"); - const cJSON* addrItem = cJSON_GetObjectItemCaseSensitive(root, "addr"); - const cJSON* cmdItem = cJSON_GetObjectItemCaseSensitive(root, "cmd"); - const cJSON* shortAddrItem = cJSON_GetObjectItemCaseSensitive(root, "shortAddress"); - const cJSON* valueItem = cJSON_GetObjectItemCaseSensitive(root, "value"); - DaliBridgeRequest request; + const cJSON* seqItem = cJSON_GetObjectItemCaseSensitive(root, "seq"); + if (seqItem == nullptr) seqItem = cJSON_GetObjectItemCaseSensitive(root, "sequence"); request.sequence = toString(seqItem); + + const cJSON* modelItem = cJSON_GetObjectItemCaseSensitive(root, "model"); + if (modelItem == nullptr) modelItem = cJSON_GetObjectItemCaseSensitive(root, "modelID"); + if (modelItem == nullptr) modelItem = cJSON_GetObjectItemCaseSensitive(root, "modelId"); request.modelID = toString(modelItem); + const cJSON* opItem = cJSON_GetObjectItemCaseSensitive(root, "op"); + if (opItem == nullptr) opItem = cJSON_GetObjectItemCaseSensitive(root, "operation"); const std::string op = toString(opItem); if (!op.empty()) { request.operation = bridgeOperationFromString(op); } + const cJSON* addrItem = cJSON_GetObjectItemCaseSensitive(root, "addr"); + if (addrItem == nullptr) addrItem = cJSON_GetObjectItemCaseSensitive(root, "rawAddress"); const int addr = toInt(addrItem, -1); if (addr >= 0) { request.rawAddress = addr; } + const cJSON* cmdItem = cJSON_GetObjectItemCaseSensitive(root, "cmd"); + if (cmdItem == nullptr) cmdItem = cJSON_GetObjectItemCaseSensitive(root, "rawCommand"); const int cmd = toInt(cmdItem, -1); if (cmd >= 0) { request.rawCommand = cmd; } + const cJSON* shortAddrItem = cJSON_GetObjectItemCaseSensitive(root, "shortAddress"); + if (shortAddrItem == nullptr) shortAddrItem = cJSON_GetObjectItemCaseSensitive(root, "short_address"); const int shortAddr = toInt(shortAddrItem, -1); if (shortAddr >= 0) { request.shortAddress = shortAddr; } - if (valueItem != nullptr) { - if (cJSON_IsNumber(valueItem)) { - request.value = valueItem->valuedouble; - } else if (cJSON_IsString(valueItem) && valueItem->valuestring != nullptr) { - request.value = std::string(valueItem->valuestring); - } else if (cJSON_IsBool(valueItem)) { - request.value = cJSON_IsTrue(valueItem); + if (const cJSON* valueItem = cJSON_GetObjectItemCaseSensitive(root, "value")) { + request.value = fromCjson(valueItem); + } + + if (const cJSON* metaItem = cJSON_GetObjectItemCaseSensitive(root, "meta")) { + const DaliValue metaValue = fromCjson(metaItem); + if (const auto* metaObject = metaValue.asObject()) { + request.metadata = *metaObject; + } + } + + for (const cJSON* child = root->child; child != nullptr; child = child->next) { + if (!isKnownBridgeRequestKey(child->string)) { + request.metadata[child->string] = fromCjson(child); } } @@ -217,23 +279,7 @@ bool DaliCloudBridge::handleDownlink(const std::string& payload) { const DaliBridgeResult result = bridge_.execute(request); - cJSON* resp = cJSON_CreateObject(); - cJSON_AddStringToObject(resp, "type", "dali_resp"); - cJSON_AddStringToObject(resp, "seq", result.sequence.c_str()); - if (!result.modelID.empty()) { - cJSON_AddStringToObject(resp, "model", result.modelID.c_str()); - } - cJSON_AddStringToObject(resp, "op", bridgeOperationToString(result.operation)); - cJSON_AddBoolToObject(resp, "ok", result.ok); - if (result.data.has_value()) { - cJSON_AddNumberToObject(resp, "data", result.data.value()); - } - if (!result.error.empty()) { - cJSON_AddStringToObject(resp, "error", result.error.c_str()); - } - if (!result.metadata.empty()) { - cJSON_AddItemToObject(resp, "meta", toCjson(DaliValue(result.metadata))); - } + cJSON* resp = toCjson(DaliValue(result.toJson())); char* raw = cJSON_PrintUnformatted(resp); std::string out = raw == nullptr ? "{}" : std::string(raw);