|
|
|
@@ -22,6 +22,7 @@
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <map>
|
|
|
|
|
#include <optional>
|
|
|
|
|
#include <set>
|
|
|
|
|
#include <string_view>
|
|
|
|
@@ -35,6 +36,18 @@ namespace {
|
|
|
|
|
constexpr const char* kTag = "gateway_bridge";
|
|
|
|
|
constexpr int kDefaultModbusPort = 1502;
|
|
|
|
|
constexpr size_t kModbusMaxPduBytes = 252;
|
|
|
|
|
constexpr const char* kDiscoveryInventoryKey = "bridge_disc";
|
|
|
|
|
constexpr int kMaxDaliShortAddress = 63;
|
|
|
|
|
constexpr uint32_t kBacnetReliabilityNoFaultDetected = 0;
|
|
|
|
|
constexpr uint32_t kBacnetReliabilityCommunicationFailure = 12;
|
|
|
|
|
|
|
|
|
|
struct BridgeDiscoveryEntry {
|
|
|
|
|
int short_address{0};
|
|
|
|
|
bool online{true};
|
|
|
|
|
DaliDomainSnapshot discovery;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
using BridgeDiscoveryInventory = std::map<int, BridgeDiscoveryEntry>;
|
|
|
|
|
|
|
|
|
|
class LockGuard {
|
|
|
|
|
public:
|
|
|
|
@@ -164,6 +177,69 @@ bool ValidDaliAddress(int address) {
|
|
|
|
|
return address >= 0 && address <= 127;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ValidShortAddress(int address) {
|
|
|
|
|
return address >= 0 && address <= kMaxDaliShortAddress;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsRawBridgeOperation(BridgeOperation operation) {
|
|
|
|
|
switch (operation) {
|
|
|
|
|
case BridgeOperation::send:
|
|
|
|
|
case BridgeOperation::sendExt:
|
|
|
|
|
case BridgeOperation::query:
|
|
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool SnapshotHasDeviceType(const DaliDomainSnapshot& snapshot, int device_type) {
|
|
|
|
|
const auto primary = snapshot.ints.find("primaryType");
|
|
|
|
|
if (primary != snapshot.ints.end() && primary->second == device_type) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const auto types = snapshot.int_arrays.find("types");
|
|
|
|
|
return types != snapshot.int_arrays.end() &&
|
|
|
|
|
std::find(types->second.begin(), types->second.end(), device_type) !=
|
|
|
|
|
types->second.end();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool OperationRequiresDt1(BridgeOperation operation) {
|
|
|
|
|
switch (operation) {
|
|
|
|
|
case BridgeOperation::getEmergencyLevel:
|
|
|
|
|
case BridgeOperation::getEmergencyStatus:
|
|
|
|
|
case BridgeOperation::getEmergencyFailureStatus:
|
|
|
|
|
case BridgeOperation::startEmergencyFunctionTest:
|
|
|
|
|
case BridgeOperation::stopEmergencyTest:
|
|
|
|
|
case BridgeOperation::startEmergencyDurationTest:
|
|
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool OperationRequiresDt8(BridgeOperation operation) {
|
|
|
|
|
switch (operation) {
|
|
|
|
|
case BridgeOperation::setColorTemperature:
|
|
|
|
|
case BridgeOperation::getColorTemperature:
|
|
|
|
|
case BridgeOperation::getColorStatus:
|
|
|
|
|
return true;
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<int> BridgeTargetValue(const BridgeDaliTarget& target) {
|
|
|
|
|
switch (target.kind) {
|
|
|
|
|
case BridgeDaliTargetKind::shortAddress:
|
|
|
|
|
return target.shortAddress;
|
|
|
|
|
case BridgeDaliTargetKind::group:
|
|
|
|
|
return target.groupAddress;
|
|
|
|
|
case BridgeDaliTargetKind::broadcast:
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON* IntArrayToCjson(const std::vector<int>& values) {
|
|
|
|
|
cJSON* array = cJSON_CreateArray();
|
|
|
|
|
if (array == nullptr) {
|
|
|
|
@@ -212,6 +288,248 @@ cJSON* SnapshotToCjson(const DaliDomainSnapshot& snapshot) {
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DaliValue::Object SnapshotToValue(const DaliDomainSnapshot& snapshot) {
|
|
|
|
|
DaliValue::Object out;
|
|
|
|
|
out["gatewayId"] = snapshot.gateway_id;
|
|
|
|
|
out["address"] = snapshot.address;
|
|
|
|
|
out["kind"] = snapshot.kind;
|
|
|
|
|
|
|
|
|
|
if (!snapshot.bools.empty()) {
|
|
|
|
|
DaliValue::Object bools;
|
|
|
|
|
for (const auto& entry : snapshot.bools) {
|
|
|
|
|
bools[entry.first] = entry.second;
|
|
|
|
|
}
|
|
|
|
|
out["bools"] = std::move(bools);
|
|
|
|
|
}
|
|
|
|
|
if (!snapshot.ints.empty()) {
|
|
|
|
|
DaliValue::Object ints;
|
|
|
|
|
for (const auto& entry : snapshot.ints) {
|
|
|
|
|
ints[entry.first] = entry.second;
|
|
|
|
|
}
|
|
|
|
|
out["ints"] = std::move(ints);
|
|
|
|
|
}
|
|
|
|
|
if (!snapshot.numbers.empty()) {
|
|
|
|
|
DaliValue::Object numbers;
|
|
|
|
|
for (const auto& entry : snapshot.numbers) {
|
|
|
|
|
numbers[entry.first] = entry.second;
|
|
|
|
|
}
|
|
|
|
|
out["numbers"] = std::move(numbers);
|
|
|
|
|
}
|
|
|
|
|
if (!snapshot.int_arrays.empty()) {
|
|
|
|
|
DaliValue::Object arrays;
|
|
|
|
|
for (const auto& entry : snapshot.int_arrays) {
|
|
|
|
|
DaliValue::Array values;
|
|
|
|
|
values.reserve(entry.second.size());
|
|
|
|
|
for (const int value : entry.second) {
|
|
|
|
|
values.emplace_back(value);
|
|
|
|
|
}
|
|
|
|
|
arrays[entry.first] = std::move(values);
|
|
|
|
|
}
|
|
|
|
|
out["intArrays"] = std::move(arrays);
|
|
|
|
|
}
|
|
|
|
|
if (!snapshot.number_arrays.empty()) {
|
|
|
|
|
DaliValue::Object arrays;
|
|
|
|
|
for (const auto& entry : snapshot.number_arrays) {
|
|
|
|
|
DaliValue::Array values;
|
|
|
|
|
values.reserve(entry.second.size());
|
|
|
|
|
for (const double value : entry.second) {
|
|
|
|
|
values.emplace_back(value);
|
|
|
|
|
}
|
|
|
|
|
arrays[entry.first] = std::move(values);
|
|
|
|
|
}
|
|
|
|
|
out["numberArrays"] = std::move(arrays);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<DaliDomainSnapshot> SnapshotFromValue(const DaliValue* value) {
|
|
|
|
|
if (value == nullptr) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
const auto* object = value->asObject();
|
|
|
|
|
if (object == nullptr) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DaliDomainSnapshot snapshot;
|
|
|
|
|
snapshot.gateway_id = static_cast<uint8_t>(getObjectInt(*object, "gatewayId").value_or(0));
|
|
|
|
|
snapshot.address = getObjectInt(*object, "address").value_or(0);
|
|
|
|
|
snapshot.kind = getObjectString(*object, "kind").value_or("device");
|
|
|
|
|
|
|
|
|
|
if (const auto* bools = getObjectValue(*object, "bools")) {
|
|
|
|
|
if (const auto* bool_object = bools->asObject()) {
|
|
|
|
|
for (const auto& entry : *bool_object) {
|
|
|
|
|
if (const auto parsed = entry.second.asBool()) {
|
|
|
|
|
snapshot.bools[entry.first] = parsed.value();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (const auto* ints = getObjectValue(*object, "ints")) {
|
|
|
|
|
if (const auto* int_object = ints->asObject()) {
|
|
|
|
|
for (const auto& entry : *int_object) {
|
|
|
|
|
if (const auto parsed = entry.second.asInt()) {
|
|
|
|
|
snapshot.ints[entry.first] = parsed.value();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (const auto* numbers = getObjectValue(*object, "numbers")) {
|
|
|
|
|
if (const auto* number_object = numbers->asObject()) {
|
|
|
|
|
for (const auto& entry : *number_object) {
|
|
|
|
|
if (const auto parsed = entry.second.asDouble()) {
|
|
|
|
|
snapshot.numbers[entry.first] = parsed.value();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (const auto* int_arrays = getObjectValue(*object, "intArrays")) {
|
|
|
|
|
if (const auto* array_object = int_arrays->asObject()) {
|
|
|
|
|
for (const auto& entry : *array_object) {
|
|
|
|
|
const auto* array = entry.second.asArray();
|
|
|
|
|
if (array == nullptr) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
std::vector<int> values;
|
|
|
|
|
values.reserve(array->size());
|
|
|
|
|
for (const auto& item : *array) {
|
|
|
|
|
values.push_back(item.asInt().value_or(0));
|
|
|
|
|
}
|
|
|
|
|
snapshot.int_arrays[entry.first] = std::move(values);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (const auto* number_arrays = getObjectValue(*object, "numberArrays")) {
|
|
|
|
|
if (const auto* array_object = number_arrays->asObject()) {
|
|
|
|
|
for (const auto& entry : *array_object) {
|
|
|
|
|
const auto* array = entry.second.asArray();
|
|
|
|
|
if (array == nullptr) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
std::vector<double> values;
|
|
|
|
|
values.reserve(array->size());
|
|
|
|
|
for (const auto& item : *array) {
|
|
|
|
|
values.push_back(item.asDouble().value_or(0.0));
|
|
|
|
|
}
|
|
|
|
|
snapshot.number_arrays[entry.first] = std::move(values);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return snapshot;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* DiscoveryStateString(bool online) {
|
|
|
|
|
return online ? "online" : "offline";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char* BacnetReliabilityToString(uint32_t reliability) {
|
|
|
|
|
switch (reliability) {
|
|
|
|
|
case kBacnetReliabilityCommunicationFailure:
|
|
|
|
|
return "communication_failure";
|
|
|
|
|
case kBacnetReliabilityNoFaultDetected:
|
|
|
|
|
default:
|
|
|
|
|
return "no_fault_detected";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool SnapshotSupportsDeviceType(const DaliDomainSnapshot& snapshot, int device_type) {
|
|
|
|
|
return SnapshotHasDeviceType(snapshot, device_type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AddDiscoveryCapabilities(cJSON* root, const DaliDomainSnapshot& snapshot) {
|
|
|
|
|
if (root == nullptr) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddBoolToObject(root, "supportsDt1", SnapshotSupportsDeviceType(snapshot, 1));
|
|
|
|
|
cJSON_AddBoolToObject(root, "supportsDt4", SnapshotSupportsDeviceType(snapshot, 4));
|
|
|
|
|
cJSON_AddBoolToObject(root, "supportsDt5", SnapshotSupportsDeviceType(snapshot, 5));
|
|
|
|
|
cJSON_AddBoolToObject(root, "supportsDt6", SnapshotSupportsDeviceType(snapshot, 6));
|
|
|
|
|
cJSON_AddBoolToObject(root, "supportsDt8", SnapshotSupportsDeviceType(snapshot, 8));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON* DiscoveryEntryToCjson(const BridgeDiscoveryEntry& entry) {
|
|
|
|
|
cJSON* root = SnapshotToCjson(entry.discovery);
|
|
|
|
|
if (root == nullptr) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddBoolToObject(root, "discovered", true);
|
|
|
|
|
cJSON_AddBoolToObject(root, "online", entry.online);
|
|
|
|
|
cJSON_AddStringToObject(root, "inventoryState", DiscoveryStateString(entry.online));
|
|
|
|
|
AddDiscoveryCapabilities(root, entry.discovery);
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON* MissingDiscoveryEntryToCjson(uint8_t gateway_id, int short_address) {
|
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
|
|
|
if (root == nullptr) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddNumberToObject(root, "gatewayId", gateway_id);
|
|
|
|
|
cJSON_AddNumberToObject(root, "address", short_address);
|
|
|
|
|
cJSON_AddStringToObject(root, "kind", "device");
|
|
|
|
|
cJSON_AddBoolToObject(root, "discovered", false);
|
|
|
|
|
cJSON_AddBoolToObject(root, "online", false);
|
|
|
|
|
cJSON_AddStringToObject(root, "inventoryState", "never_seen");
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DaliValue::Object DiscoveryEntryToValue(const BridgeDiscoveryEntry& entry) {
|
|
|
|
|
DaliValue::Object out;
|
|
|
|
|
out["shortAddress"] = entry.short_address;
|
|
|
|
|
out["state"] = DiscoveryStateString(entry.online);
|
|
|
|
|
out["snapshot"] = SnapshotToValue(entry.discovery);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::optional<BridgeDiscoveryEntry> DiscoveryEntryFromValue(const DaliValue& value) {
|
|
|
|
|
const auto* object = value.asObject();
|
|
|
|
|
if (object == nullptr) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
const auto short_address = getObjectInt(*object, "shortAddress");
|
|
|
|
|
const auto snapshot = SnapshotFromValue(getObjectValue(*object, "snapshot"));
|
|
|
|
|
if (!short_address.has_value() || !snapshot.has_value() ||
|
|
|
|
|
!ValidShortAddress(short_address.value())) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string state = getObjectString(*object, "state").value_or("online");
|
|
|
|
|
BridgeDiscoveryEntry entry;
|
|
|
|
|
entry.short_address = short_address.value();
|
|
|
|
|
entry.online = state != "offline";
|
|
|
|
|
entry.discovery = snapshot.value();
|
|
|
|
|
entry.discovery.address = short_address.value();
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DaliValue::Object DiscoveryInventoryToValue(const BridgeDiscoveryInventory& inventory) {
|
|
|
|
|
DaliValue::Object out;
|
|
|
|
|
DaliValue::Array entries;
|
|
|
|
|
entries.reserve(inventory.size());
|
|
|
|
|
for (const auto& item : inventory) {
|
|
|
|
|
entries.emplace_back(DiscoveryEntryToValue(item.second));
|
|
|
|
|
}
|
|
|
|
|
out["entries"] = std::move(entries);
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BridgeDiscoveryInventory DiscoveryInventoryFromValue(const DaliValue::Object& object) {
|
|
|
|
|
BridgeDiscoveryInventory inventory;
|
|
|
|
|
const auto* entries = getObjectValue(object, "entries");
|
|
|
|
|
if (entries == nullptr || entries->asArray() == nullptr) {
|
|
|
|
|
return inventory;
|
|
|
|
|
}
|
|
|
|
|
for (const auto& item : *entries->asArray()) {
|
|
|
|
|
const auto entry = DiscoveryEntryFromValue(item);
|
|
|
|
|
if (!entry.has_value()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
inventory[entry->short_address] = entry.value();
|
|
|
|
|
}
|
|
|
|
|
return inventory;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GatewayBridgeHttpResponse SnapshotResponse(const std::optional<DaliDomainSnapshot>& snapshot,
|
|
|
|
|
const char* missing_message) {
|
|
|
|
|
if (!snapshot.has_value()) {
|
|
|
|
@@ -568,8 +886,10 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
#endif
|
|
|
|
|
std::unique_ptr<DaliCloudBridge> cloud;
|
|
|
|
|
BridgeRuntimeConfig bridge_config;
|
|
|
|
|
BridgeDiscoveryInventory discovery_inventory;
|
|
|
|
|
std::optional<GatewayCloudConfig> cloud_config;
|
|
|
|
|
bool bridge_config_loaded{false};
|
|
|
|
|
bool discovery_inventory_loaded{false};
|
|
|
|
|
bool cloud_config_loaded{false};
|
|
|
|
|
bool cloud_started{false};
|
|
|
|
|
bool modbus_started{false};
|
|
|
|
@@ -601,6 +921,11 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
|
|
|
|
|
BridgeProvisioningStore bridge_store(bridgeNamespace());
|
|
|
|
|
bridge_config_loaded = bridge_store.load(&bridge_config) == ESP_OK;
|
|
|
|
|
DaliValue::Object discovery_object;
|
|
|
|
|
if (bridge_store.loadObject(kDiscoveryInventoryKey, &discovery_object) == ESP_OK) {
|
|
|
|
|
discovery_inventory = DiscoveryInventoryFromValue(discovery_object);
|
|
|
|
|
discovery_inventory_loaded = true;
|
|
|
|
|
}
|
|
|
|
|
applyBridgeConfigLocked();
|
|
|
|
|
|
|
|
|
|
GatewayProvisioningStore cloud_store(cloudNamespace());
|
|
|
|
@@ -643,6 +968,98 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
bacnet_started = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
esp_err_t saveDiscoveryInventoryLocked() const {
|
|
|
|
|
BridgeProvisioningStore store(bridgeNamespace());
|
|
|
|
|
if (discovery_inventory.empty()) {
|
|
|
|
|
return store.clearKey(kDiscoveryInventoryKey);
|
|
|
|
|
}
|
|
|
|
|
return store.saveObject(kDiscoveryInventoryKey, DiscoveryInventoryToValue(discovery_inventory));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BridgeDiscoveryEntry* findDiscoveryEntryLocked(int short_address) const {
|
|
|
|
|
const auto entry = discovery_inventory.find(short_address);
|
|
|
|
|
return entry == discovery_inventory.end() ? nullptr : &entry->second;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BridgeDiscoveryEntry* findDiscoveryEntryLocked(int short_address) {
|
|
|
|
|
const auto entry = discovery_inventory.find(short_address);
|
|
|
|
|
return entry == discovery_inventory.end() ? nullptr : &entry->second;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BridgeDiscoveryEntry* updateDiscoveryEntryLocked(int short_address, bool persist) {
|
|
|
|
|
if (!ValidShortAddress(short_address)) {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto snapshot = domain.discoverDeviceTypes(channel.gateway_id, short_address);
|
|
|
|
|
if (snapshot.has_value()) {
|
|
|
|
|
auto& entry = discovery_inventory[short_address];
|
|
|
|
|
entry.short_address = short_address;
|
|
|
|
|
entry.online = true;
|
|
|
|
|
entry.discovery = snapshot.value();
|
|
|
|
|
entry.discovery.address = short_address;
|
|
|
|
|
entry.discovery.gateway_id = channel.gateway_id;
|
|
|
|
|
if (persist) {
|
|
|
|
|
saveDiscoveryInventoryLocked();
|
|
|
|
|
}
|
|
|
|
|
return &entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto* existing = findDiscoveryEntryLocked(short_address);
|
|
|
|
|
if (existing != nullptr) {
|
|
|
|
|
existing->online = false;
|
|
|
|
|
if (persist) {
|
|
|
|
|
saveDiscoveryInventoryLocked();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return existing;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BridgeDiscoveryEntry* ensureDiscoveryEntryLocked(int short_address) {
|
|
|
|
|
const auto* existing = findDiscoveryEntryLocked(short_address);
|
|
|
|
|
if (existing != nullptr) {
|
|
|
|
|
return existing;
|
|
|
|
|
}
|
|
|
|
|
return updateDiscoveryEntryLocked(short_address, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<int> referencedShortAddressesLocked() const {
|
|
|
|
|
std::set<int> addresses;
|
|
|
|
|
for (const auto& model : bridge_config.models) {
|
|
|
|
|
if (model.dali.kind != BridgeDaliTargetKind::shortAddress ||
|
|
|
|
|
!model.dali.shortAddress.has_value() ||
|
|
|
|
|
!ValidShortAddress(model.dali.shortAddress.value())) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
addresses.insert(model.dali.shortAddress.value());
|
|
|
|
|
}
|
|
|
|
|
return std::vector<int>(addresses.begin(), addresses.end());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
esp_err_t syncBacnetServerLocked() {
|
|
|
|
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
|
|
|
|
if (!service_config.bacnet_enabled) {
|
|
|
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
|
|
|
}
|
|
|
|
|
if (bacnet == nullptr) {
|
|
|
|
|
return ESP_ERR_INVALID_STATE;
|
|
|
|
|
}
|
|
|
|
|
const auto bindings = bacnetObjectBindingsLocked();
|
|
|
|
|
if (bindings.empty() && !bacnet_started) {
|
|
|
|
|
return ESP_ERR_NOT_FOUND;
|
|
|
|
|
}
|
|
|
|
|
const auto server_config = bacnetServerConfigLocked();
|
|
|
|
|
return GatewayBacnetServer::instance().registerChannel(
|
|
|
|
|
channel.gateway_id, server_config, bindings,
|
|
|
|
|
[this](BridgeObjectType object_type, uint32_t object_instance,
|
|
|
|
|
const std::string& property, const DaliValue& value) {
|
|
|
|
|
return handleBacnetWrite(object_type, object_instance, property, value);
|
|
|
|
|
});
|
|
|
|
|
#else
|
|
|
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void applyCloudModelsLocked() {
|
|
|
|
|
if (cloud_started && cloud != nullptr) {
|
|
|
|
|
cloud->stop();
|
|
|
|
@@ -760,6 +1177,45 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
return model == bridge_config.models.end() ? model_id : model->displayName();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool shouldPublishBacnetBindingLocked(const BacnetObjectBinding& binding) {
|
|
|
|
|
if (binding.objectInstance < 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (IsRawBridgeOperation(binding.operation) ||
|
|
|
|
|
binding.target.kind != BridgeDaliTargetKind::shortAddress) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (!binding.target.shortAddress.has_value()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int short_address = binding.target.shortAddress.value();
|
|
|
|
|
const auto* discovered = ensureDiscoveryEntryLocked(short_address);
|
|
|
|
|
if (discovered == nullptr) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (OperationRequiresDt1(binding.operation)) {
|
|
|
|
|
return SnapshotHasDeviceType(discovered->discovery, 1);
|
|
|
|
|
}
|
|
|
|
|
if (OperationRequiresDt8(binding.operation)) {
|
|
|
|
|
return SnapshotHasDeviceType(discovered->discovery, 8);
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<BacnetObjectBinding> effectiveBacnetObjectsLocked() {
|
|
|
|
|
std::vector<BacnetObjectBinding> bindings;
|
|
|
|
|
if (bacnet == nullptr) {
|
|
|
|
|
return bindings;
|
|
|
|
|
}
|
|
|
|
|
for (const auto& binding : bacnet->describeObjects()) {
|
|
|
|
|
if (shouldPublishBacnetBindingLocked(binding)) {
|
|
|
|
|
bindings.push_back(binding);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return bindings;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
|
|
|
|
GatewayBacnetServerConfig bacnetServerConfigLocked() const {
|
|
|
|
|
GatewayBacnetServerConfig config;
|
|
|
|
@@ -776,22 +1232,32 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<GatewayBacnetObjectBinding> bacnetObjectBindingsLocked() const {
|
|
|
|
|
std::vector<GatewayBacnetObjectBinding> bacnetObjectBindingsLocked() {
|
|
|
|
|
std::vector<GatewayBacnetObjectBinding> bindings;
|
|
|
|
|
if (bacnet == nullptr) {
|
|
|
|
|
return bindings;
|
|
|
|
|
}
|
|
|
|
|
for (const auto& binding : bacnet->describeObjects()) {
|
|
|
|
|
for (const auto& binding : effectiveBacnetObjectsLocked()) {
|
|
|
|
|
if (binding.objectInstance < 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
bool out_of_service = false;
|
|
|
|
|
uint32_t reliability = kBacnetReliabilityNoFaultDetected;
|
|
|
|
|
if (binding.target.kind == BridgeDaliTargetKind::shortAddress &&
|
|
|
|
|
binding.target.shortAddress.has_value()) {
|
|
|
|
|
if (const auto* discovery = findDiscoveryEntryLocked(binding.target.shortAddress.value())) {
|
|
|
|
|
if (!discovery->online) {
|
|
|
|
|
out_of_service = true;
|
|
|
|
|
reliability = kBacnetReliabilityCommunicationFailure;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
bindings.push_back(GatewayBacnetObjectBinding{channel.gateway_id,
|
|
|
|
|
binding.modelID,
|
|
|
|
|
modelName(binding.modelID),
|
|
|
|
|
binding.objectType,
|
|
|
|
|
static_cast<uint32_t>(binding.objectInstance),
|
|
|
|
|
binding.property.empty() ? "presentValue"
|
|
|
|
|
: binding.property});
|
|
|
|
|
: binding.property,
|
|
|
|
|
out_of_service,
|
|
|
|
|
reliability});
|
|
|
|
|
}
|
|
|
|
|
return bindings;
|
|
|
|
|
}
|
|
|
|
@@ -819,17 +1285,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
if (bacnet == nullptr) {
|
|
|
|
|
return ESP_ERR_INVALID_STATE;
|
|
|
|
|
}
|
|
|
|
|
const auto bindings = bacnetObjectBindingsLocked();
|
|
|
|
|
if (bindings.empty()) {
|
|
|
|
|
return ESP_ERR_NOT_FOUND;
|
|
|
|
|
}
|
|
|
|
|
const auto server_config = bacnetServerConfigLocked();
|
|
|
|
|
const esp_err_t err = GatewayBacnetServer::instance().registerChannel(
|
|
|
|
|
channel.gateway_id, server_config, bindings,
|
|
|
|
|
[this](BridgeObjectType object_type, uint32_t object_instance,
|
|
|
|
|
const std::string& property, const DaliValue& value) {
|
|
|
|
|
return handleBacnetWrite(object_type, object_instance, property, value);
|
|
|
|
|
});
|
|
|
|
|
const esp_err_t err = syncBacnetServerLocked();
|
|
|
|
|
bacnet_started = err == ESP_OK;
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
@@ -864,6 +1320,9 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
cJSON_AddNumberToObject(root, "channel", channel.channel_index);
|
|
|
|
|
cJSON_AddStringToObject(root, "name", channel.name.c_str());
|
|
|
|
|
cJSON_AddBoolToObject(root, "bridgeConfigLoaded", bridge_config_loaded);
|
|
|
|
|
cJSON_AddBoolToObject(root, "discoveryInventoryLoaded", discovery_inventory_loaded);
|
|
|
|
|
cJSON_AddNumberToObject(root, "inventoryCount",
|
|
|
|
|
static_cast<double>(discovery_inventory.size()));
|
|
|
|
|
cJSON_AddNumberToObject(root, "modelCount", static_cast<double>(bridge_config.models.size()));
|
|
|
|
|
|
|
|
|
|
cJSON* modbus_json = cJSON_CreateObject();
|
|
|
|
@@ -922,6 +1381,154 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
return GatewayBridgeHttpResponse{ESP_OK, BridgeRuntimeConfigToJson(bridge_config)};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GatewayBridgeHttpResponse inventoryJson() const {
|
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
|
|
|
|
cJSON_AddBoolToObject(root, "loaded", discovery_inventory_loaded);
|
|
|
|
|
cJSON_AddNumberToObject(root, "count", static_cast<double>(discovery_inventory.size()));
|
|
|
|
|
cJSON* inventory = cJSON_CreateArray();
|
|
|
|
|
if (inventory != nullptr) {
|
|
|
|
|
for (const auto& entry : discovery_inventory) {
|
|
|
|
|
cJSON_AddItemToArray(inventory, DiscoveryEntryToCjson(entry.second));
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddItemToObject(root, "inventory", inventory);
|
|
|
|
|
}
|
|
|
|
|
return JsonOk(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GatewayBridgeHttpResponse effectiveModelJsonLocked() {
|
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
|
|
|
|
cJSON_AddStringToObject(root, "name", channel.name.c_str());
|
|
|
|
|
cJSON* models = cJSON_CreateArray();
|
|
|
|
|
const auto effective_bacnet = effectiveBacnetObjectsLocked();
|
|
|
|
|
if (models != nullptr) {
|
|
|
|
|
for (const auto& model : bridge_config.models) {
|
|
|
|
|
cJSON* item = ToCjson(DaliValue(model.toJson()));
|
|
|
|
|
if (item == nullptr) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddStringToObject(item, "displayName", model.displayName().c_str());
|
|
|
|
|
if (model.dali.kind == BridgeDaliTargetKind::shortAddress &&
|
|
|
|
|
model.dali.shortAddress.has_value() &&
|
|
|
|
|
ValidShortAddress(model.dali.shortAddress.value())) {
|
|
|
|
|
const int short_address = model.dali.shortAddress.value();
|
|
|
|
|
if (const auto* entry = findDiscoveryEntryLocked(short_address)) {
|
|
|
|
|
cJSON_AddBoolToObject(item, "discovered", true);
|
|
|
|
|
cJSON_AddBoolToObject(item, "online", entry->online);
|
|
|
|
|
cJSON_AddStringToObject(item, "inventoryState", DiscoveryStateString(entry->online));
|
|
|
|
|
cJSON_AddItemToObject(item, "discovery", DiscoveryEntryToCjson(*entry));
|
|
|
|
|
} else {
|
|
|
|
|
cJSON_AddBoolToObject(item, "discovered", false);
|
|
|
|
|
cJSON_AddBoolToObject(item, "online", false);
|
|
|
|
|
cJSON_AddStringToObject(item, "inventoryState", "never_seen");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (model.protocol == BridgeProtocolKind::bacnet &&
|
|
|
|
|
model.external.objectInstance.has_value()) {
|
|
|
|
|
const auto binding = std::find_if(
|
|
|
|
|
effective_bacnet.begin(), effective_bacnet.end(),
|
|
|
|
|
[&model](const auto& item) { return item.modelID == model.id; });
|
|
|
|
|
const bool published = binding != effective_bacnet.end();
|
|
|
|
|
cJSON_AddBoolToObject(item, "bacnetPublished", published);
|
|
|
|
|
if (published && model.dali.kind == BridgeDaliTargetKind::shortAddress &&
|
|
|
|
|
model.dali.shortAddress.has_value()) {
|
|
|
|
|
const auto* entry = findDiscoveryEntryLocked(model.dali.shortAddress.value());
|
|
|
|
|
const bool out_of_service = entry != nullptr && !entry->online;
|
|
|
|
|
cJSON_AddBoolToObject(item, "bacnetOutOfService", out_of_service);
|
|
|
|
|
cJSON_AddStringToObject(item, "bacnetReliability",
|
|
|
|
|
BacnetReliabilityToString(out_of_service
|
|
|
|
|
? kBacnetReliabilityCommunicationFailure
|
|
|
|
|
: kBacnetReliabilityNoFaultDetected));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddItemToArray(models, item);
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddItemToObject(root, "models", models);
|
|
|
|
|
}
|
|
|
|
|
return JsonOk(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GatewayBridgeHttpResponse effectiveModelJson() {
|
|
|
|
|
LockGuard guard(lock);
|
|
|
|
|
return effectiveModelJsonLocked();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GatewayBridgeHttpResponse scanInventory(std::string_view body) {
|
|
|
|
|
std::string mode = "referenced";
|
|
|
|
|
if (!body.empty()) {
|
|
|
|
|
cJSON* root = cJSON_ParseWithLength(body.data(), body.size());
|
|
|
|
|
if (root == nullptr) {
|
|
|
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "invalid scan JSON");
|
|
|
|
|
}
|
|
|
|
|
if (const char* mode_value = JsonString(root, "mode")) {
|
|
|
|
|
mode = mode_value;
|
|
|
|
|
} else if (const char* mode_value = JsonString(root, "scanMode")) {
|
|
|
|
|
mode = mode_value;
|
|
|
|
|
}
|
|
|
|
|
cJSON_Delete(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LockGuard guard(lock);
|
|
|
|
|
std::vector<int> addresses;
|
|
|
|
|
if (mode == "all" || mode == "full") {
|
|
|
|
|
addresses.reserve(kMaxDaliShortAddress + 1);
|
|
|
|
|
for (int address = 0; address <= kMaxDaliShortAddress; ++address) {
|
|
|
|
|
addresses.push_back(address);
|
|
|
|
|
}
|
|
|
|
|
mode = "all";
|
|
|
|
|
} else {
|
|
|
|
|
addresses = referencedShortAddressesLocked();
|
|
|
|
|
mode = "referenced";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t online_count = 0;
|
|
|
|
|
size_t offline_count = 0;
|
|
|
|
|
size_t unseen_count = 0;
|
|
|
|
|
cJSON* results = cJSON_CreateArray();
|
|
|
|
|
for (const int address : addresses) {
|
|
|
|
|
const auto* entry = updateDiscoveryEntryLocked(address, false);
|
|
|
|
|
if (entry != nullptr) {
|
|
|
|
|
if (entry->online) {
|
|
|
|
|
++online_count;
|
|
|
|
|
} else {
|
|
|
|
|
++offline_count;
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddItemToArray(results, DiscoveryEntryToCjson(*entry));
|
|
|
|
|
} else {
|
|
|
|
|
++unseen_count;
|
|
|
|
|
cJSON_AddItemToArray(results, MissingDiscoveryEntryToCjson(channel.gateway_id, address));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const esp_err_t persist_err = saveDiscoveryInventoryLocked();
|
|
|
|
|
if (persist_err != ESP_OK) {
|
|
|
|
|
cJSON_Delete(results);
|
|
|
|
|
return ErrorResponse(persist_err, "failed to persist discovery inventory");
|
|
|
|
|
}
|
|
|
|
|
discovery_inventory_loaded = discovery_inventory_loaded || !discovery_inventory.empty();
|
|
|
|
|
|
|
|
|
|
if (bacnet_started) {
|
|
|
|
|
const esp_err_t err = syncBacnetServerLocked();
|
|
|
|
|
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND) {
|
|
|
|
|
cJSON_Delete(results);
|
|
|
|
|
return ErrorResponse(err, "failed to refresh BACnet bridge after scan");
|
|
|
|
|
}
|
|
|
|
|
bacnet_started = err == ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddBoolToObject(root, "ok", true);
|
|
|
|
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
|
|
|
|
cJSON_AddStringToObject(root, "mode", mode.c_str());
|
|
|
|
|
cJSON_AddNumberToObject(root, "scanned", static_cast<double>(addresses.size()));
|
|
|
|
|
cJSON_AddNumberToObject(root, "onlineCount", static_cast<double>(online_count));
|
|
|
|
|
cJSON_AddNumberToObject(root, "offlineCount", static_cast<double>(offline_count));
|
|
|
|
|
cJSON_AddNumberToObject(root, "neverSeenCount", static_cast<double>(unseen_count));
|
|
|
|
|
cJSON_AddItemToObject(root, "inventory", results);
|
|
|
|
|
return JsonOk(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GatewayBridgeHttpResponse modbusBindingsJson() const {
|
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
|
|
|
@@ -941,13 +1548,13 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
return JsonOk(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GatewayBridgeHttpResponse bacnetBindingsJson() const {
|
|
|
|
|
GatewayBridgeHttpResponse bacnetBindingsJson() {
|
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
|
|
|
cJSON_AddNumberToObject(root, "gatewayId", channel.gateway_id);
|
|
|
|
|
cJSON* bindings = cJSON_CreateArray();
|
|
|
|
|
#if defined(CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED)
|
|
|
|
|
if (bindings != nullptr && bacnet != nullptr) {
|
|
|
|
|
for (const auto& binding : bacnet->describeObjects()) {
|
|
|
|
|
for (const auto& binding : effectiveBacnetObjectsLocked()) {
|
|
|
|
|
cJSON* item = cJSON_CreateObject();
|
|
|
|
|
if (item == nullptr) {
|
|
|
|
|
continue;
|
|
|
|
@@ -956,6 +1563,36 @@ struct GatewayBridgeService::ChannelRuntime {
|
|
|
|
|
cJSON_AddStringToObject(item, "objectType", bridgeObjectTypeToString(binding.objectType));
|
|
|
|
|
cJSON_AddNumberToObject(item, "objectInstance", binding.objectInstance);
|
|
|
|
|
cJSON_AddStringToObject(item, "property", binding.property.c_str());
|
|
|
|
|
cJSON_AddStringToObject(item, "operation", bridgeOperationToString(binding.operation));
|
|
|
|
|
cJSON_AddStringToObject(item, "targetKind",
|
|
|
|
|
bridgeDaliTargetKindToString(binding.target.kind));
|
|
|
|
|
if (const auto target_value = BridgeTargetValue(binding.target)) {
|
|
|
|
|
cJSON_AddNumberToObject(item, "targetAddress", target_value.value());
|
|
|
|
|
}
|
|
|
|
|
if (binding.target.rawAddress.has_value()) {
|
|
|
|
|
cJSON_AddNumberToObject(item, "rawAddress", binding.target.rawAddress.value());
|
|
|
|
|
}
|
|
|
|
|
if (binding.target.rawCommand.has_value()) {
|
|
|
|
|
cJSON_AddNumberToObject(item, "rawCommand", binding.target.rawCommand.value());
|
|
|
|
|
}
|
|
|
|
|
if (binding.target.kind == BridgeDaliTargetKind::shortAddress &&
|
|
|
|
|
binding.target.shortAddress.has_value()) {
|
|
|
|
|
if (const auto* discovery = findDiscoveryEntryLocked(binding.target.shortAddress.value())) {
|
|
|
|
|
const bool out_of_service = !discovery->online;
|
|
|
|
|
cJSON_AddBoolToObject(item, "outOfService", out_of_service);
|
|
|
|
|
cJSON_AddStringToObject(item, "reliability",
|
|
|
|
|
BacnetReliabilityToString(out_of_service
|
|
|
|
|
? kBacnetReliabilityCommunicationFailure
|
|
|
|
|
: kBacnetReliabilityNoFaultDetected));
|
|
|
|
|
cJSON_AddStringToObject(item, "inventoryState",
|
|
|
|
|
DiscoveryStateString(discovery->online));
|
|
|
|
|
} else {
|
|
|
|
|
cJSON_AddBoolToObject(item, "outOfService", false);
|
|
|
|
|
cJSON_AddStringToObject(item, "reliability",
|
|
|
|
|
BacnetReliabilityToString(kBacnetReliabilityNoFaultDetected));
|
|
|
|
|
cJSON_AddStringToObject(item, "inventoryState", "never_seen");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cJSON_AddItemToArray(bindings, item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -1226,7 +1863,7 @@ const GatewayBridgeService::ChannelRuntime* GatewayBridgeService::findRuntime(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
|
|
|
|
const std::string& action_arg, int gateway_id_arg, const std::string& query_arg) const {
|
|
|
|
|
const std::string& action_arg, int gateway_id_arg, const std::string& query_arg) {
|
|
|
|
|
if (!config_.bridge_enabled) {
|
|
|
|
|
return ErrorResponse(ESP_ERR_NOT_SUPPORTED, "bridge service is disabled");
|
|
|
|
|
}
|
|
|
|
@@ -1257,7 +1894,7 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
|
|
|
|
if (!gateway_id.has_value()) {
|
|
|
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "gateway id is required");
|
|
|
|
|
}
|
|
|
|
|
const auto* runtime = findRuntime(gateway_id.value());
|
|
|
|
|
auto* runtime = findRuntime(gateway_id.value());
|
|
|
|
|
if (runtime == nullptr) {
|
|
|
|
|
return ErrorResponse(ESP_ERR_NOT_FOUND, "unknown gateway id");
|
|
|
|
|
}
|
|
|
|
@@ -1274,6 +1911,12 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
|
|
|
|
if (action == "bacnet") {
|
|
|
|
|
return runtime->bacnetBindingsJson();
|
|
|
|
|
}
|
|
|
|
|
if (action == "inventory") {
|
|
|
|
|
return runtime->inventoryJson();
|
|
|
|
|
}
|
|
|
|
|
if (action == "effective_model") {
|
|
|
|
|
return runtime->effectiveModelJson();
|
|
|
|
|
}
|
|
|
|
|
if (action == "cloud") {
|
|
|
|
|
return runtime->cloudJson();
|
|
|
|
|
}
|
|
|
|
@@ -1282,8 +1925,21 @@ GatewayBridgeHttpResponse GatewayBridgeService::handleGet(
|
|
|
|
|
if (!address.has_value() || !ValidDaliAddress(address.value())) {
|
|
|
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "valid addr is required");
|
|
|
|
|
}
|
|
|
|
|
return SnapshotResponse(dali_domain_.discoverDeviceTypes(gateway_id.value(), address.value()),
|
|
|
|
|
"device did not respond to type discovery");
|
|
|
|
|
{
|
|
|
|
|
LockGuard guard(runtime->lock);
|
|
|
|
|
const auto* entry = runtime->updateDiscoveryEntryLocked(address.value(), true);
|
|
|
|
|
if (entry != nullptr && runtime->bacnet_started) {
|
|
|
|
|
const esp_err_t err = runtime->syncBacnetServerLocked();
|
|
|
|
|
if (err != ESP_OK && err != ESP_ERR_NOT_FOUND) {
|
|
|
|
|
return ErrorResponse(err, "failed to refresh BACnet bridge after discovery");
|
|
|
|
|
}
|
|
|
|
|
runtime->bacnet_started = err == ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
if (entry != nullptr) {
|
|
|
|
|
return JsonOk(DiscoveryEntryToCjson(*entry));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ErrorResponse(ESP_ERR_NOT_FOUND, "device did not respond to type discovery");
|
|
|
|
|
}
|
|
|
|
|
if (action == "dt4" || action == "dt5" || action == "dt6") {
|
|
|
|
|
const auto address = QueryInt(query, "addr", "address");
|
|
|
|
@@ -1443,6 +2099,9 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
|
|
|
|
|
}
|
|
|
|
|
return handleGet("bacnet", gateway_id.value());
|
|
|
|
|
}
|
|
|
|
|
if (action == "scan") {
|
|
|
|
|
return runtime->scanInventory(body);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ErrorResponse(ESP_ERR_INVALID_ARG, "unknown bridge POST action");
|
|
|
|
|
}
|
|
|
|
|