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, }; }