// Version: 2.1.251211 const deviceGuid = ""; // GUID from the IO-unit "Kiona - Edge AI" const source = "edgeai.csv"; // Name of the Tag-list for "Kiona - Edge AI" const username = ""; // API - Client ID const password = ""; // API - Client Secret const createSteeringsTags = false; // Set as "true" when you need to create new tags in edgeai.csv, otherwise leave as "false" const properties = [ { name: "", // Building name + Address (used for logging) contract: 9999999, // Contract number in Edge, will also act as prefix for all 5 AI-tags shouldUseSteering: true, // Is it "true" or "false" that we should steer this building? outdoortemp: "", // Tag used for the outdoor sensor (full tag name) circuitPrefix: "", // Tag prefix for the circuit we wish to steer, used to fetch correct X/Y values when creating new AI-tags for fallback curve. numberOfXY: 5, // Amount of setpoints/breakpoints in heating curve startsHigh: false, // Is it "true" or "false" that X1 has a positive value(ex 20ºC or 15ºC) (False is default) measuringTags: [ { name: "", valueType: "6", dataPoint: "1" }, // Secondary heating - Supply | ºC { name: "", valueType: "6", dataPoint: "1" }, // Secondary heating - Return | ºC { name: "", valueType: "6", dataPoint: "1" }, // Hot water - Supply | ºC { name: "", valueType: "6", dataPoint: "1" }, // Hot water - Return (Warm water circulation) | ºC { name: "", valueType: "6", dataPoint: "1" }, // Primary heating - Supply | ºC { name: "", valueType: "6", dataPoint: "1" }, // Primary heating - Return | ºC { name: "", valueType: "31", dataPoint: "140" }, // Tap water | m³ { name: "", valueType: "55", dataPoint: "140" }, // Tap water | l { name: "", valueType: "50", dataPoint: "110" }, // Electricity | kWh { name: "", valueType: "35", dataPoint: "110" }, // Heat energy | MWh { name: "", valueType: "8", dataPoint: "110" }, // Heat energy | kWh { name: "", valueType: "33", dataPoint: "140" }, // Cold water | m³ { name: "", valueType: "6", dataPoint: "1" }, // Temperature | ºC { name: "", valueType: "6", dataPoint: "1" }, // Temperature | ºC { name: "", valueType: "6", dataPoint: "1" }, // Temperature | ºC { name: "", valueType: "6", dataPoint: "1" } // Temperature | ºC ], roomSensors: { suffix: { tempSuffix: "_PV", // The end-part of the tag for the temperature datapoint humSuffix: "" // The end-part of the tag for the humidity datapoint. (If empty it will be ignored) }, prefix: [ "", "", "" ] } }, ]; const edgeTimestampTag = "EDGEAI_TS"; const edgeForceReadTag = "EDGEAI_FORCEREAD"; const tokenTag = "EDGEAI_TOKEN"; const timestampTag = "EDGEAI_TOKENTS"; const lastStateSuffix = "LAST"; const baseUrl = "https://connector.egain.io/v1/"; const pushUrl = "https://connector.egain.io/v1/"; const tokenUrl = "https://identity.egain.io/connect/token"; let tagChange = false; const now = DateTime.Now; let force = false; let lastRun; let token = ""; const aiTags = { iotag: "_AI_IO", manaienable: "_AI_MCMD", aienable: "_AI_V", aireg: "_AI", orgcsp: "_AI_CSP1", }; const DebugLevel = { NONE: 0, ERROR: 1, ALL: 2, }; const debugLevel = DebugLevel.ERROR; if (createSteeringsTags) { createEdgeTags(); } else if (tags.ContainsKey(edgeForceReadTag) && tags[edgeForceReadTag].Value == "1") { // Om icke init och forceradtaggen är satt till 1 sätt force till true logDebug("Doing forced reading"); force = true; } tryUpdateLastRunTime(); //Om nyare än 5 min, kör bara uppdatera värden- //på alla kurvskirvning, kolla om X1 < Xmax. Vänd då ordning if (lastRun) { logDebug("lastRun: " + lastRun.ToString()); } run(); if (tagChange) { saveTagSource(source); } tagWrite(edgeForceReadTag, "0"); if (!createSteeringsTags) { updateTags(); } ("OK"); function run() { const shouldRun = force || createSteeringsTags || !lastRun || lastRun.Ticks < now.AddMinutes(-10).Ticks; logDebug("run: " + shouldRun); if (shouldRun) { try { if (createSteeringsTags || handleToken()) { for (let i = 0; i < properties.length; i++) { const prop = properties[i]; if (createSteeringsTags) { createTags(prop); } else { try { getAndSetValues(prop); pushValues(prop); } catch (e) { logError("run: " + i); logError(e); } } } } else { logDebug("Token not handled"); } } catch (e) { logError("run"); logError(e); } tagWrite(edgeTimestampTag, now.ToString("s")); } } function getAndSetValues(build) { try { logDebug("getAndSetValues: " + build.name); if (!build.shouldUseSteering) { logDebug("Not using steering for: " + build.name); return true; } const response = getCurve(build); if (!response) { throw new Error("Failed to get response for: " + build.name); } logDebug("status:" + response.status); logDebug("result:" + response.result); const object = JSON.parse(response.result); object["timestamp"] = now.ToString("s"); const ioTag = getFullName(build, aiTags.iotag); tagWrite(ioTag, JSON.stringify(object)); } catch (e) { logError("getAndSetValues"); logError(e); } try { //Hitta när utetempen kan bli 0. const ageAndJson = getAgeAndJson(build); if (!ageAndJson) { throw new Error("getAgeAndJson failed for: " + build.name); } const isActive = checkActive(build); logDebug("isActive: " + isActive); if (isActive) { setCurveFromJson(ageAndJson, build); return true; } } catch (e) { logError("getAndSetValues"); logError(e); return false; } } function pushValues(build) { try { logDebug("pushValues: " + build.name); const readings = { timestamp: formatTimestamp(now), }; if (tryGetTag(build.outdoortemp)) { readings["outdoorTemp"] = getValidValue(build.outdoortemp); } const serializedSteeringBody = JSON.stringify(readings); const steeringUrl = pushUrl + "steering/groups/" + build.contract + "/readings"; logDebug("Steering request body:" + serializedSteeringBody); logDebug("Steering request url: " + steeringUrl); const response = webPost(steeringUrl, serializedSteeringBody, "application/json", token); logDebug("Steering response result: " + response.result); logDebug("Steering response status: " + response.status); const body = []; addMeasuringData(build, body); addAiEnableState(build, body); addRoomSensors(build, body); const serializedMeteringBody = JSON.stringify(body); const meeteringUrl = pushUrl + "metering/contracts/" + build.contract + "/values"; logDebug("serializedMeteringBody: " + serializedMeteringBody); logDebug("url: " + meeteringUrl); const meteringResponse = webPost( meeteringUrl, serializedMeteringBody, "application/json", token ); logDebug("Metering response result:" + meteringResponse.result); logDebug("Metering response status:" + meteringResponse.status); return serializedMeteringBody; } catch (e) { logError("pushValues"); logError(e); } return null; } function addRoomSensors(build, values) { try { logDebug("addRoomSensors: " + build.name); const shouldAdd = build.roomSensors && build.roomSensors.prefix && build.roomSensors.prefix.length; if (!shouldAdd) { logDebug("No sensors to add"); return; } for (let i = 0; i < build.roomSensors.prefix.length; i++) { const sensor = build.roomSensors.prefix[i]; logDebug("Trying to add sensor: " + sensor); if (!sensor) { continue; } const sensorData = { externalId: sensor, }; const dataPoints = []; if (build.roomSensors.suffix.tempSuffix) { const tempTag = sensor + build.roomSensors.suffix.tempSuffix; const tag = tryGetTag(tempTag); if (!tag) { logDebug("Tag not found: " + tempTag); } else { const value = getValidValue(tempTag); dataPoints.push({ dataPointTypeId: "1", valueTypeId: "1", values: [ { timestamp: formatTimestamp(tag.Timestamp), value: value, }, ], }); } } if (build.roomSensors.suffix.humSuffix) { const humTag = sensor + build.roomSensors.suffix.humSuffix; const tag = tryGetTag(humTag); if (!tag) { logDebug("Tag not found: " + humTag); } else { const value = getValidValue(humTag); dataPoints.push({ dataPointTypeId: "31", valueTypeId: "4", values: [ { timestamp: formatTimestamp(tag.Timestamp), value: value, }, ], }); } } sensorData["dataPoints"] = dataPoints; logDebug("sensorData: " + JSON.stringify(sensorData)); values.push(sensorData); } } catch (error) { logError("tryAddRoomSensors"); logError(error); } } function addMeasuringData(build, values) { const shouldAdd = build.measuringTags && build.measuringTags.length ? true : false; logDebug("tryAddOtherDataPoints: " + shouldAdd); if (!shouldAdd) { return; } for (let i = 0; i < build.measuringTags.length; i++) { const item = build.measuringTags[i]; const tag = tryGetTag(item.name); if (!tag) { continue; } values.push(getDataPoint(tag, item.dataPoint, item.valueType)); } } function addAiEnableState(build, values) { if (!build.shouldUseSteering) { logDebug("Not using steering for: " + build.name); return; } const tagName = getFullName(build, aiTags.aienable); const tag = tryGetTag(tagName); if (!tag) { logDebug("Tag not found: " + tagName); return; } values.push(getDataPoint(tag, 201, 62)); } function getDataPoint(tag, dataPointType, valueType) { const value = getValidValue(tag.Name); const values = [ { value: value, timestamp: formatTimestamp(tag.Timestamp), }, ]; const dataPoints = [ { dataPointTypeId: dataPointType, valueTypeId: valueType, values: values, }, ]; const dataPoint = { externalId: tag.Name, dataPoints: dataPoints, }; return dataPoint; } function formatTimestamp(dateTime) { return dateTime.ToUniversalTime().ToString("s") + "Z"; } function checkStateChange(state, lastState) { const current = parseInt(state); const last = parseInt(lastState); logDebug("checkStateChange: current:" + current + " - last:" + last); const stateInfo = { changed: current !== last, toDisabled: current < last, toEnabled: current > last, }; return stateInfo; } function updateTags() { logDebug("updateTags"); for (let i = 0; i < properties.length; i++) { try { const build = properties[i]; logDebug("updateTags: " + build.name); if (!build.shouldUseSteering) { logDebug("Not using steering for: " + build.name); continue; } const manualAiEnableTag = getFullName(build, aiTags.manaienable); const manualAiEnabled = getValidValue(manualAiEnableTag); const manualAiEnabledLast = getValidValue(manualAiEnableTag + lastStateSuffix); const aiEnableTag = getFullName(build, aiTags.aienable); //Kolla hur gamla värden vi har på IO //Om äldre än 30-60 min? Återställ //Måste få med ai-enable också. const stateChange = checkStateChange(manualAiEnabled, manualAiEnabledLast); const toEnabled = stateChange.toEnabled; logDebug("toEnabled: " + toEnabled); const toDisabled = stateChange.toDisabled; logDebug("toDisabled: " + toDisabled); tags[manualAiEnableTag + lastStateSuffix].WriteValue(manualAiEnabled, false); let ageAndJson = null; if (toEnabled) { logDebug("Pushing curve for: " + build.name); handleToken(); pushCurve(build); // Skicka baskurva till edge få optimerad kurva tillbaks const getCurveResponse = getCurve(build); // Skicka temperatur till edge, få optimerad kurva tillbaks logDebug("getCurve response: " + getCurveResponse.result); logDebug("getCurve status: " + getCurveResponse.status); if (getCurveResponse && getCurveResponse.result) { saveCurve(build, getCurveResponse); // Spara optimerad kurva till tag } else { logDebug("Failed to get curve for: " + build.name); } ageAndJson = getAgeAndJson(build); // Kolla om datan är tillräckligt "färsk" if (checkAgeOfCurve(build, ageAndJson, 60)) { setCurveFromJson(ageAndJson, build); // Skriv kurva till styrtag? } //Pusha bara om olika från edge kurva //Hämta ny kurva. } const supplyPrefix = getFullName(build, aiTags.aireg); try { //Sätt en resetbool istället och gör utanför. let shouldReset = true; if (ageAndJson == null) { ageAndJson = getAgeAndJson(build); } if (ageAndJson != null) { const timestamp = ageAndJson["timestamp"]; logDebug("ageAndJson timestamp: " + timestamp); const lastUpdate = DateTime.ParseExact( timestamp, "yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture ); logDebug("lastUpdate: " + lastUpdate); if (manualAiEnabled == "1") { if (lastUpdate.Ticks > now.AddMinutes(-60).Ticks) { logDebug("Value is OK"); shouldReset = false; } else { logDebug("Value too old for: " + build.name); } } } logDebug("shouldReset: " + shouldReset); if (shouldReset) { logDebug("Reset for:" + aiEnableTag); tags[aiEnableTag].WriteValue("0", true); logDebug("toDisabled: " + toDisabled); if (toDisabled) { writeToCurve(supplyPrefix, build.circuitPrefix, build.numberOfXY); } } else { tagWrite(aiEnableTag, "1"); } } catch (e) { logError("updateTags"); logError(e); } if (aiTags.orgcsp) { const setPoint = getSetpoint( supplyPrefix, build.numberOfXY, 20, 80, getValidValue(build.outdoortemp) ); logDebug("orgcsp: " + setPoint); tags[getFullName(build, aiTags.orgcsp)].WriteValue(setPoint, true); } } catch (e) { logError("updateTags"); logError(e); } } } function saveCurve(build, res) { logDebug("saveCurve: " + build.name); try { const object = JSON.parse(res.result); if (object) { const tagName = getFullName(build, aiTags.iotag); object["timestamp"] = now.ToString("s"); tagWrite(tagName, JSON.stringify(object)); } else { logDebug("Failed parsing response: " + build.name); } } catch (e) { logError(e); } } function checkAgeOfCurve(build, ageAndJson, maxAge) { try { logDebug("checkAgeOfCurve: " + build.name); if (ageAndJson != null) { const timestamp = ageAndJson["timestamp"]; logDebug("timestamp: " + timestamp); const lastUpdate = DateTime.Parse(timestamp); logDebug("lastUpdate: " + lastUpdate); const minValidTimestamp = now.AddMinutes(-maxAge); logDebug("minValidTimestamp: " + minValidTimestamp); if ( lastUpdate > minValidTimestamp && getValidValue(getFullName(build, aiTags.manaienable)) == "1" ) { logDebug("lastUpdate and value is OK"); return true; } else { logDebug("lastUpdate too old for: " + build.name); return false; } } logDebug("ageAndJson is null for: " + build.name); return false; } catch (e) { logError(e); return false; } } function writeToCurve(fromPrefix, toPrefix, numberOfXY) { for (let i = 1; i < numberOfXY + 1; i++) { const tagFromX = fromPrefix + "_X" + i; const tagFromY = fromPrefix + "_Y" + i; const tagToX = toPrefix + "_X" + i; const tagToY = toPrefix + "_Y" + i; if ( tags.ContainsKey(tagToX) && tags.ContainsKey(tagToY) && tags.ContainsKey(tagFromX) && tags.ContainsKey(tagFromY) ) { const newValueX = tags[tagFromX].Value; if (tags[tagToX].Value != newValueX) { logDebug("Writing from: " + tagFromX + " to " + tagToX + " with value: " + newValueX); tags[tagToX].WriteValue(newValueX, true); } const newValueY = tags[tagFromY].Value; if (tags[tagToY].Value != newValueY) { logDebug("Writing from: " + tagFromY + " to " + tagToY + " with value: " + newValueY); tags[tagToY].WriteValue(newValueY, true); } } } } function setCurveFromJson(object, build) { logDebug("setCurveFromJson: " + build.name); const steeringCurve = object["optimizedSteeringCurve"]; const newSteeringCurve = []; for (const point of steeringCurve) { newSteeringCurve.push([point["x"], point["y"]]); } if (!build.startsHigh) { newSteeringCurve.reverse(); } logDebug( "newSteeringCurve.length: " + newSteeringCurve.length + " - numberOfXY: " + build.numberOfXY + " - prefix: " + build.circuitPrefix + " - startsHigh: " + build.startsHigh ); logDebug(newSteeringCurve); if (build.numberOfXY != newSteeringCurve.length) { logDebug("numberOfXY mismatch"); return; } for (let i = 1; i < build.numberOfXY + 1; i++) { const tagNameX = build.circuitPrefix + "_X" + i; const tagNameY = build.circuitPrefix + "_Y" + i; if (tags.ContainsKey(tagNameX) && tags.ContainsKey(tagNameY)) { const tagX = tags[tagNameX]; const tagY = tags[tagNameY]; const valueX = newSteeringCurve[i - 1][0]; let newPointX = toNumber(valueX); const oldPointX = toNumber(tagX.Value); const hasBoundaryX = tagX.Engmin < tagX.Engmax; if (hasBoundaryX) { const adjustedPointX = adjustForBoundary(newPointX, tagX); if (adjustedPointX != newPointX) { logDebug( "Adjusted value for " + tagNameX + " from " + newPointX + " to " + adjustedPointX ); } newPointX = adjustedPointX; } logDebug( "tagX: " + tagNameX + " - newPointX: " + newPointX + " (" + valueX + ") " + "- oldPointX: " + oldPointX ); if (newPointX != oldPointX) { logDebug("Updating value for " + tagNameX); tagX.WriteValue(asString(newPointX), true); } const valueY = newSteeringCurve[i - 1][1]; let newPointY = toNumber(valueY); const oldPointY = toNumber(tagY.Value); const hasBoundaryY = tagY.Engmin < tagY.Engmax; if (hasBoundaryY) { const adjustedPointY = adjustForBoundary(newPointY, tagY); if (adjustedPointY != newPointY) { logDebug( "Adjusted value for " + tagNameY + " from " + newPointY + " to " + adjustedPointY ); } newPointY = adjustedPointY; } logDebug( "tagY: " + tagNameY + " - newPointY: " + newPointY + " (" + valueY + ") " + "- oldPointY: " + oldPointY ); if (newPointY != oldPointY) { logDebug("Updating value for " + tagNameY); tagY.WriteValue(asString(newPointY), true); } } } } function getAgeAndJson(build) { try { logDebug("getAgeAndJson: " + build.name); const tagName = getFullName(build, aiTags.iotag); logDebug("tagName: " + aiTags.iotag); const object = JSON.parse(tags[tagName].Value); const dateTimes = toDateTimeArray(object["utcDateTime"]); const first = new DateTime(dateTimes[0]).ToLocalTime(); const age = now.Ticks - first.Ticks; const maxAge = new TimeSpan(25, 0, 0).Ticks; if (!object || age > maxAge) { logDebug(build.name + " forecast too old: " + age); return null; } return object; } catch (e) { logError("getAgeAndJson"); logError(e); return null; } } function handleToken() { logDebug("handleToken"); const checkUrl = baseUrl + "steering/groups/" + properties[0].contract; logDebug("checkUrl: " + checkUrl); token = tagRead(tokenTag); logDebug("token: " + token); let tokenTimestamp = tagRead(timestampTag); debug(tokenTimestamp); logDebug("timestampTag: " + timestampTag); logDebug("tokenTimestamp: " + tokenTimestamp); if (!(parseInt(tokenTimestamp) > -1)) { tokenTimestamp = 0; } let validUntil = 0; try { validUntil = new DateTime(tokenTimestamp); let tokenIsInvalid = validUntil < now; if (!tokenIsInvalid) { const webcheck = webPost(checkUrl, "{}", "application/json", token); logDebug("webcheck:" + webcheck); tokenIsInvalid = !webcheck || !webcheck.result || webcheck.result.indexOf("401") != -1; } if (tokenIsInvalid) { const body = "grant_type=client_credentials&client_id=" + username + "&client_secret=" + password + "&scope=Connector Read Write Steering"; const response = webPost(tokenUrl, body, "application/x-www-form-urlencoded"); const result = JSON.parse(response.result); logDebug("result: " + result); const accessToken = result["access_token"]; if (accessToken) { token = accessToken; tagWrite(tokenTag, accessToken); const expiresIn = result["expires_in"]; logDebug("expiresIn: " + expiresIn); const ticks = new DateTime(parseFloat(expiresIn)).Ticks; logDebug("ticks: " + ticks); validUntil = now.AddSeconds(ticks); const validUntilTicks = validUntil.Ticks; logDebug("validUntil: " + validUntil + " (" + validUntilTicks + ")"); tagWrite(timestampTag, validUntilTicks); } else { logDebug("Failed to get token"); } } } catch (e) { logError("handleToken"); logError(e.stack); } return validUntil >= now; } function getFullName(build, suffix) { suffix = typeof suffix === "string" || typeof suffix === "number" ? String(suffix) : ""; const contract = build && build.contract; let prefix = typeof contract === "string" || typeof contract === "number" ? String(contract) : ""; if (!prefix || !suffix) { return suffix; } const prefixIndex = prefix.lastIndexOf("_"); const suffixIndex = suffix.indexOf("_"); const endsWith = prefixIndex === prefix.length - 1; const startsWith = suffixIndex === 0; if (endsWith && startsWith) { prefix = prefix.substring(0, prefixIndex); } else if (!endsWith && !startsWith) { prefix += "_"; } return prefix + suffix; } function toDateTimeArray(utd) { const dta = []; for (const dt of utd) { dta.push(toLocalTime(dt).Ticks); } return dta; } function toLocalTime(dt) { //Newtonsoft converts data to local timeformat. return DateTime.Parse(dt); } function webPost(url, body, type, auth) { logDebug("webPost: " + url); const res = {}; try { const httpWebRequest = WebRequest.Create(url); if (auth) { httpWebRequest.Headers[HttpRequestHeader.Authorization] = "Bearer " + auth; } httpWebRequest.ContentType = type; httpWebRequest.Method = "POST"; const streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()); streamWriter.Write(body); streamWriter.Close(); streamWriter.Dispose(); const httpResponse = httpWebRequest.GetResponse(); const streamReader = new StreamReader(httpResponse.GetResponseStream()); res.status = httpResponse.StatusCode; res.result = streamReader.ReadToEnd(); streamReader.Close(); streamReader.Dispose(); } catch (e) { logError("webPost"); logError(e); } return res; } function pushCurve(build) { try { logDebug("pushCurve: " + build.name); const currentSteeringCurve = []; if (aiTags.aireg) { const aiPrefix = getFullName(build, aiTags.aireg); for (let i = 1; i < build.numberOfXY + 1; i++) { const tagX = aiPrefix + "_X" + i; const tagY = aiPrefix + "_Y" + i; const hasX = tags.ContainsKey(tagX); const hasY = tags.ContainsKey(tagY); if (hasX && hasY) { const x = getValidValue(tagX); const y = getValidValue(tagY); if (x && y) { const point = { x, y, }; currentSteeringCurve.push(point); } else { logDebug( "Invalid values: " + tagX + " (" + tags[tagX].Value + ")" + " - " + tagY + " (" + tags[tagY].Value + ")" ); return null; } } else { const missingTags = []; if (!hasX) { missingTags.push(tagX); } if (!hasY) { missingTags.push(tagY); } logDebug("Tags not found: " + missingTags.join(", ")); } } } const body = { Settings: { CurrentSteeringCurve: currentSteeringCurve, }, }; const serializedBody = JSON.stringify(body); logDebug("serializedBody: " + serializedBody); const response = webPost( pushUrl + "steering-settings/groups/" + build.contract, serializedBody, "application/json", token ); return response; } catch (e) { logError(e); return null; } } function getCurve(build) { try { logDebug("getCurve: " + build.name); const body = { groupName: build.contract, }; const outDoorTempTag = tryGetTag(build.outdoortemp); if (outDoorTempTag) { tagRead(build.outdoortemp); const outdoorTemp = tagValue(build.outdoortemp); if (outDoorTempTag.IsValid) { body["outdoorTemperature"] = outdoorTemp; } else { logDebug("No valid value for " + build.outdoortemp + ": " + outDoorTempTag.Value); } } const serializedBody = JSON.stringify(body); logDebug("body:" + serializedBody); const res = webPost( baseUrl + "steering/groups/" + build.contract, serializedBody, "application/json", token ); return res; } catch (e) { logDebug("getCurve"); logError(e); return null; } } function checkActive(build) { logDebug("checkActive: " + build.name); if (aiTags.aienable) { const tagName = getFullName(build, aiTags.aienable); logDebug("tagName:" + tagName); if (tagName && tags.ContainsKey(tagName)) { const value = getValidValue(tagName); if (value == "1") { logDebug(build.name + " is active"); return true; } logDebug(build.name + " is inactive"); } else { logDebug("Tag not found: " + tagName); } } return false; } function linearr(max, min, x, y, val) { val = parseFloat(val); const xl = x.length; if (parseFloat(x[0]) > parseFloat(x[xl - 1])) { for (let k = 1; k < xl; k++) { logDebug("k= " + k + " val<=x[k] : val=" + val + " x[k]=" + x[k]); if (val >= x[k]) { return line(max, min, x[k], x[k - 1], y[k], y[k - 1], val); } } return max; } else { for (let k = 1; k < xl; k++) { logDebug("k= " + k + " val<=x[k] : val=" + val + " x[k]=" + x[k]); if (val <= x[k]) { return line(max, min, x[k], x[k - 1], y[k], y[k - 1], val); } } return min; } } function createTagArr(tagname, suffix, numbertags) { const rtaga = []; let valid = true; for (let i = 0; i < numbertags; i++) { const value = getValidValue(tagname + "_" + suffix + (i + 1)); if (value) { rtaga[i] = parseFloat(value); } else { valid = false; } } return valid ? rtaga : false; } function line(max, min, bp1, bp2, t1, t2, x) { const r = t2 + (bp2 - x) * ((t1 - t2) / (bp2 - bp1)); logDebug(`max=${max} min=${min} bp1=${bp1} bp2=${bp2} t1=${t1} t2=${t2} x=${x}`); logDebug(`r=${r}`); if (r > max) { return max; } if (r < min) { return min; } return r; } function tryGetTag(tagName) { logDebug("tryGetTag"); if (!tagName) { logDebug("No tag name provided"); return null; } if (typeof tagName !== "string" || !tags.ContainsKey(tagName)) { logDebug("Tag not found: " + tagName); return null; } logDebug("Tag found: " + tagName); return tags[tagName]; } function getValidValue(tagName) { logDebug("getValidValue: " + tagName); const tag = tryGetTag(tagName); if (!tag) { logDebug("Tag not found: " + tagName); return ""; } let value = ""; if (tag.IsValid && !tag.IsOld) { value = tag.Value; } else if (tag.Timestamp != DateTime.MinValue) { value = tagValue(tagName); } else { value = tagRead(tagName); } logDebug("value: " + value); return value; } function createTags(build) { try { logDebug("createTags: " + build.name); if (!build.shouldUseSteering) { logDebug("Not using steering for: " + build.name); return; } if (!createSteeringsTags) { logDebug("Not creating tags for: " + build.name); return; } tagChange = tryCreateTag({ name: getFullName(build, aiTags.iotag), description: "IO", dataType: DataType.STRING, source, deviceGuid, }) || tagChange; tagChange = tryCreateTag({ name: getFullName(build, aiTags.manaienable), description: "Aktivera AI-styrning", dataType: DataType.DIGITAL, parameters: { trendoptions: "t:600", }, source, deviceGuid, }) || tagChange; tagChange = tryCreateTag({ name: getFullName(build, aiTags.manaienable + lastStateSuffix), description: "Senaste tillstånd för manuella variabeln", dataType: DataType.DIGITAL, source, deviceGuid, }) || tagChange; logDebug("build.circuitPrefix: " + build.circuitPrefix); logDebug("aiTags.aireg: " + aiTags.aireg); if (build.circuitPrefix && aiTags.aireg) { for (let i = 1; i < build.numberOfXY + 1; i++) { const tagX = getFullName(build, aiTags.aireg + "_X" + i); const tagY = getFullName(build, aiTags.aireg + "_Y" + i); if (!tags.ContainsKey(tagX)) { tagChange = tryCreateTag({ name: tagX, dataType: DataType.REAL, format: "0.0", unit: "°C", source, deviceGuid, }) || tagChange; const originalTagX = build.circuitPrefix + "_X" + i; if (tags.ContainsKey(originalTagX)) { tagRead(originalTagX); const valueX = tagValue(originalTagX); logDebug("Writing " + valueX + " from " + originalTagX + " to " + tagX); tagWrite(tagX, valueX); } else { logDebug("No original X found: " + originalTagX); } } else { logDebug("X tag already exist: " + tagX); } if (!tags.ContainsKey(tagY)) { tagChange = tryCreateTag({ name: tagY, dataType: DataType.REAL, format: "0.0", unit: "°C", source, deviceGuid, }) || tagChange; const originalTagY = build.circuitPrefix + "_Y" + i; if (tags.ContainsKey(originalTagY)) { tagRead(originalTagY); const valueY = tagValue(originalTagY); logDebug("Writing " + valueY + " from " + originalTagY + " to " + tagY); tagWrite(tagY, valueY); } else { logDebug("No original Y found: " + originalTagY); } } else { logDebug("Y tag already exist: " + tagY); } } if (aiTags.orgcsp) { tagChange = tryCreateTag({ name: getFullName(build, aiTags.orgcsp), description: "Orginal börvärde", dataType: DataType.REAL, format: "0.0", unit: "°C", parameters: { trendoptions: "t:600", }, source, deviceGuid, }) || tagChange; } if (aiTags.aienable) { tagChange = tryCreateTag({ name: getFullName(build, aiTags.aienable), description: "Edge överstyrning aktiv", dataType: DataType.DIGITAL, parameters: { trendoptions: "t:600", }, source, deviceGuid, }) || tagChange; } } } catch (e) { logError("createTags"); logError(e); } } function createEdgeTags() { logDebug("createEdgeTags: " + source); if (!TagManager.Manager.Sources.ContainsKey(source)) { try { const sourceList = TagManager.Manager.NewFileSource(source); TagManager.Manager.Sources.Add(source, sourceList); TagManager.Manager.Sources[source].Save(); } catch (e) { logError("source: " + source + " need to be created manually"); } } tagChange = tryCreateTag({ name: tokenTag, description: "Token till Edge API", dataType: DataType.STRING, source, deviceGuid, }) || tagChange; tagChange = tryCreateTag({ name: edgeTimestampTag, description: "Last Edge get data", dataType: DataType.STRING, source, deviceGuid, }) || tagChange; tagChange = tryCreateTag({ name: edgeForceReadTag, description: "Forces new read from Edge", dataType: DataType.DIGITAL, source, deviceGuid, }) || tagChange; tagChange = tryCreateTag({ name: timestampTag, description: "Giltighetstid för Edge token", dataType: DataType.STRING, source, deviceGuid, }) || tagChange; } function tryCreateTag(options = {}) { try { logDebug("tryCreateEdgeTag: " + options.name); if (!options.name || typeof options.name !== "string") { logDebug("Invalid tag name"); return false; } if (!options.source || typeof options.source !== "string") { logDebug("Invalid source"); return false; } const name = options.name.toUpperCase(); if (tags.ContainsKey(name)) { logDebug("Already exists: " + name); return false; } const tag = new Tag(name); tag.State = TagState.NEW; const { source, deviceGuid = "", dataType = DataType.REAL, description = "", format = "", unit = "", parameters = {}, } = options; tag.Source = source; tag.Deviceguid = deviceGuid; tag.Datatype = dataType; tag.Description = description; tag.Format = format; tag.Unit = unit; if (parameters) { try { for (const key in options) { tag.SetParameter(key, options[key]); } } catch (error) { logError("Error setting tag parameters: " + error); } } tags.Add(name, tag); return tags.ContainsKey(name); } catch (error) { logError("tryCreateEdgeTag"); logError(error); return false; } } function getSetpoint(supplyprefix, numbertags, min, max, outdoortemp) { logDebug("supplyprefix: " + supplyprefix); const tarrx = createTagArr(supplyprefix, "X", numbertags); const tarry = createTagArr(supplyprefix, "Y", numbertags); if (!tarrx || !tarry) { logDebug("X/Y arrays not complete"); return false; } logDebug("xy : " + tarrx.length); const sp = linearr(max, min, tarrx, tarry, outdoortemp); logDebug("sp: " + sp); return sp; } function saveTagSource(source) { logDebug("saveTagSource: " + source); TagManager.Manager.Sources[source].Save(); TagManager.Manager.UpdateTagDatabase(source); } function tryUpdateLastRunTime() { let edgeTimestampValue; try { edgeTimestampValue = tagRead(edgeTimestampTag); lastRun = toLocalTime(edgeTimestampValue); } catch (e) { logError("Error parsing edgeTimestampTag :" + e); logError("edgeTimestampValue :" + edgeTimestampValue); lastRun = DateTime.MinValue; } } function asString(value) { return typeof value === "string" ? value : String(value); } function toNumber(value) { if (typeof value === "number") { return value; } if (typeof value === "string") { value = value.replace(",", "."); } return parseFloat(value); } function adjustForBoundary(value, tag) { value = Math.max(tag.Engmin, value); value = Math.min(tag.Engmax, value); return value; } function logError(message) { if (debugLevel >= DebugLevel.ERROR) { debug("[ERROR]: " + String(message)); } } function logDebug(message) { if (debugLevel >= DebugLevel.ALL) { debug("[DEBUG]: " + String(message)); } }