118 lines
3.0 KiB
JavaScript
118 lines
3.0 KiB
JavaScript
import config from "../config.js";
|
|
import { Device, Patch } from "../db/models/index.js";
|
|
import { getLogger } from "../logger.js";
|
|
import {
|
|
formatClientPatches,
|
|
updatePatchesFromUplinkPayload,
|
|
} from "./patches.js";
|
|
import { applyTankLevelFromPayload } from "./waterTank.js";
|
|
import { applyPowerFromPayload } from "./powerState.js";
|
|
|
|
const logger = getLogger("Devices");
|
|
|
|
export function toPlainDevice(doc, patchDocs = []) {
|
|
if (!doc) return null;
|
|
const o = doc.toObject ? doc.toObject() : doc;
|
|
o.id = o._id?.toString();
|
|
delete o._id;
|
|
o.__v = undefined;
|
|
o.patches = formatClientPatches(patchDocs);
|
|
return o;
|
|
}
|
|
|
|
/** Single TTN device from config — used for watering and lookups. */
|
|
export async function getConfiguredDevice() {
|
|
const { ttnApplicationId, ttnDeviceId } = config.device;
|
|
if (!ttnApplicationId?.trim() || !ttnDeviceId?.trim()) return null;
|
|
return Device.findOne({
|
|
ttnApplicationId: ttnApplicationId.trim(),
|
|
ttnDeviceId: ttnDeviceId.trim(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Ensures exactly one Device row matches config.device; removes others and their patches.
|
|
*/
|
|
export async function syncDeviceFromConfig(cfg) {
|
|
const { name, ttnApplicationId, ttnDeviceId } = cfg.device || {};
|
|
const tid = ttnApplicationId?.trim();
|
|
const did = ttnDeviceId?.trim();
|
|
if (!tid || !did) {
|
|
logger.warn(
|
|
"config.device: set ttnApplicationId and ttnDeviceId (or DEMO_TTN_* / DEVICE_TTN_* env)",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const displayName = (name && String(name).trim()) || "Allotment node";
|
|
|
|
const match = await Device.findOne({
|
|
ttnApplicationId: tid,
|
|
ttnDeviceId: did,
|
|
});
|
|
const toRemove = await Device.find(
|
|
match ? { _id: { $ne: match._id } } : {},
|
|
);
|
|
|
|
for (const d of toRemove) {
|
|
await Patch.deleteMany({ deviceId: d._id });
|
|
await Device.deleteOne({ _id: d._id });
|
|
}
|
|
|
|
if (!match) {
|
|
await Device.create({
|
|
name: displayName,
|
|
ttnApplicationId: tid,
|
|
ttnDeviceId: did,
|
|
});
|
|
logger.info("Created device from config.device");
|
|
return;
|
|
}
|
|
|
|
if (match.name !== displayName) {
|
|
match.name = displayName;
|
|
await match.save();
|
|
}
|
|
}
|
|
|
|
export async function applyTtnUplink({ applicationId, deviceId, decodedPayload }) {
|
|
if (!decodedPayload || typeof decodedPayload !== "object") return null;
|
|
|
|
const tid = config.device.ttnApplicationId?.trim();
|
|
const did = config.device.ttnDeviceId?.trim();
|
|
if (
|
|
!tid ||
|
|
!did ||
|
|
applicationId !== tid ||
|
|
deviceId !== did
|
|
) {
|
|
return { skipped: true, reason: "not_configured_device" };
|
|
}
|
|
|
|
const dev = await Device.findOne({
|
|
ttnApplicationId: tid,
|
|
ttnDeviceId: did,
|
|
});
|
|
if (!dev) {
|
|
return { skipped: true, reason: "no_matching_device" };
|
|
}
|
|
|
|
const payload = decodedPayload;
|
|
|
|
await updatePatchesFromUplinkPayload(dev, payload);
|
|
|
|
const tankLevel = await applyTankLevelFromPayload(payload);
|
|
const power = await applyPowerFromPayload(payload);
|
|
|
|
dev.online = true;
|
|
await dev.save();
|
|
|
|
const patchDocs = await Patch.find({ deviceId: dev._id }).lean();
|
|
|
|
return {
|
|
device: toPlainDevice(dev, patchDocs),
|
|
tankLevel,
|
|
power,
|
|
};
|
|
}
|