require "log" require "sys" require "utils" require "patch" require "fs" siganl = require 'posix.signal' syswait = require 'posix.sys.wait' unistd = require 'posix.unistd' mqtt=require "mosquitto" lpack=require("lua_pack") base64=require("base64") dyutils=require("dyutils") math=require("math") cjson=require("cjson") require"lsqlite3" LOG_LEVEL = log.LOGLEVEL_TRACE -------------------------[全局参数定义]------------------------- BATT_TEMP_COUNT = 4 BATT_VOL_COUNT = 14 EMU_PATH = "/app/emu/" EMU_DAT_FILE = EMU_PATH .. "cache.json" -- EMU_DAT_FILE = "/tmp/cache.json" CUSTOM_SCHEDULES_FILE = EMU_PATH .. "CustomSchedules.json" LOCAL_SCHEDULES_FILE = EMU_PATH .. "LocalSchedules.json" SELECT_SCHEDULES_FILE = EMU_PATH .. "SelectSchedules.json" EMU_CONFIG_FILE = EMU_PATH .. "EMU_Config.json" HISTORY_DATABASE_FILE = "/app/collect.db" ADJ_BATT_INTERVAL= 10*60 --动态修改BMS参数 RETRANS_INTERVAL= 30 --重传间隔 CALCULATE_INTERVAL=3*60 --计算间隔 REPORT_INTERVAL =30*60 --上报间隔 -- REPORT_INTERVAL= 0.5*60*60 --上报间隔 -- DEVICE_OFFLINE_TIME = 20*60 DEVICE_OFFLINE_TIME = 20*60*60*60 -- 判断离线时间 ALARM_SEGMENT = 30*60 -- 故障分段时间,每过这个时间认为过一段,在level2 报警中用 -- ALARM_SEGMENT = 30 -- 故障分段时间,每过这个时间认为过一段,在level2 报警中用 ALARM_BEFORE_SAMPLE = 1*60 --故障采样超前时间 ALARM_AFTER_SAMPLE = 1*60 --故障采样滞后时间 ALARM_SAMPLE_SHEILD = false --故障屏蔽 ifu_obj={} pcs_obj={} bms_obj={} RemoteControl = {} sendBuff={} recvBuff={} task={} topic_sub={} cache_fd =nil used_TOU = nil off_service = false scheFiles = {{CUSTOM_SCHEDULES_FILE,"CustomSchedules"},{LOCAL_SCHEDULES_FILE,"LocalSchedules"}} local alarm_tab={ {"PCS","AlarmErrorMessage","Er_InterComm","Communication between microcontrollers is failure",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_EEPROM","EEPROM cannot be read or written",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_Fac","The grid frequency is out of tolerable range",1,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_Relay","Relay check is failure",3,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_VacCon","Different value between Master and Slave for grid voltage",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_FacCon","Different value between Master and Slave for grid frequency",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_DCIniec","The DC injection to grid is too high",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_Iso","Isolation resistance of PV-plant out of tolerable range",3,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_Vac","Grid voltage out of tolerable range",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_FanE","The external fan failure",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_PVoverVol","Pv input voltage is over the tolerable maximum value",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_AutoTestFail","Auto test failure",3,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_InvertOT","Temperature is too high",1,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_Fan","The fan in case failure",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_DCBus","Dc bus is too high",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_GroundI","Ground current is too high",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_UtilityLoss","Utility is unavailable",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_HCT","AC HCT check failure 3 times",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_Relay3","Relay check failure 3 times",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_GFCI3","GFCI check failure 3 times",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_GFCI","Different value between Master and Slave for GFCI",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_DCI","Different value between Master and Slave for output DC current",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_HCTA","The output current sensor is abnormal",2,nil,nil,nil,nil}, {"PCS","AlarmErrorMessage","Er_GFCIA","The GFCI detecting circuit is abnormal",2,nil,nil,nil,nil}, {"PCS","AlarmWarningCode","Warn_Invert","PCS general code, detailed fault check error code",1,nil,nil,nil,nil}, {"PCS","AlarmWarningCode","Warn_BMS","BMS general code, detailed fault check error code",1,nil,nil,nil,nil}, {"PCS","AlarmWarningCode","Warn_PV","PV BOOST hardware overcurrent",1,nil,nil,nil,nil}, {"PCS","AlarmWarningCode","Warn_OverL","Load overload",1,nil,nil,nil,nil}, {"PCS","AlarmWarningCode","Warn_GridOV","Grid voltage sampling is too high",1,nil,nil,nil,nil}, {"PCS","AlarmWarningCode","Warn_OutVolH","Off-grid output voltage is too high",1,nil,nil,nil,nil}, {"PCS","AlarmWarningCode","Warn_OutVolL","Off-grid output voltage is too low",1,nil,nil,nil,nil}, {"PCS","AlarmWarningCode","Warn_OffGBatL","Off-grid mode battery voltage low shutdown protection",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","BatVL","BatteryVoltLow",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","SOC_L","BatterySOCLow",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","SOCIB","BatterySOCInBack",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","ChgDis","BMSDischargeDisable",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","DChg_TO","DischargeTimeOn",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","Chg_TO","ChargeTimeOn",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","DisChgTO","DischargeDriveOn",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","DisChgCL","BMSDischgCurrentLow",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","DisChgCL2","DischargeCurrentLow",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","MeterC","MeterCommLoss",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","MeterCR","MeterConnectReverse",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","SULoadL","SelfUseLoadLight",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","EMSDisIZ","EMSDischargeIZero",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","DisBUS_H","DischargeBUSHigh",2,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","BatDisC","BatteryDisconnect",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","BatOCh","BatteryOvercharge",2,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","BatOT","BMSOverTemperature",2,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","BMSOCh","BMSOvercharge",2,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","BMSChDis","BMSChargeDisable",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","SelfUseOf","SelfUseOff",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","SOC_DOR","SOCDeltaOverRange",1,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","BatSelfDis","BatterySelfDischarge",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","OffGSOC_L","OffgridSOCLow",1,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","GWU","GridWaveUnstable",1,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","FeedPL","FeedPowerLimit",1,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","PFV","PFValueSet",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","RPL","RealPowerLimit",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","DCOn","DCOutputOn",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","SOCProOff","SOCProtectOff",3,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","DischBP","Discharge mode for BP",0,nil,nil,nil,nil}, {"PCS","AlarmDiagStatus","BMSChIm","BMS Charge Immediately",3,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_COV","单体过压警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_CUV","单体欠压警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_POV","总压过压警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_PUV","总压欠压警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_ChaOC","充电过流警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_DisChaOC","放电过流警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_ChaOT","充电高温警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_DisChaOT","放电高温警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_ChaUT","充电低温警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_DisChaUT","放电低温警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_EnvOT","环境高温警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_EntUT","环境低温警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_MosOT","MOSFET高温警告",1,nil,nil,nil,nil}, {"BMS","AlarmWarning","W_SOCLow","SOC low alarm",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_COV","单体过压保护",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_CUV","单体欠压保护",3,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_POV","总体过压保护",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_PUV","总体欠压保护",3,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_ChaOC","充电过流保护",2,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_DisChaOC","放电过流保护",2,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_SC","短路保护",3,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_ChaOV","充电过压保护",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_ChaOT","充电过温保护",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_DisChaOT","放电过温保护",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_ChaUT","充电低温保护",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_DisChaUT","放电低温保护",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_MOSOT","MOSFET高温保护",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_EnvOT","环境高温保护",1,nil,nil,nil,nil}, {"BMS","AlarmProtection","P_EnvUT","环境低温保护",1,nil,nil,nil,nil}, {"BMS","AlarmFault","F_ChaMOS","充电MOS故障",3,nil,nil,nil,nil}, {"BMS","AlarmFault","F_DisChaMOS","放电MOS故障",3,nil,nil,nil,nil}, {"BMS","AlarmFault","F_TemSensor","温感故障",3,nil,nil,nil,nil}, {"BMS","AlarmFault","F_CellFault","电池电压故障",3,nil,nil,nil,nil}, {"BMS","AlarmFault","F_Commu","front end sampling communication fault",3,nil,nil,nil,nil} } -- | BMS/PCS | variable name | emu coefficient | emu variable name | emu coefficient | ifu variable name | local symbol_remap={ {"BMS", "Model_SN", 1, nil, nil, "BMS_ID"}, {"BMS", "Version", 1, nil, nil, "BMS_SoftVer"}, {"BMS", "SOC", 1, nil, 1, "StateOfCharge"}, {"BMS", "SOH", 1, nil, 1, "StateOfHealth"}, {"BMS", "FullCapacity", 1, nil, 1000, "TotalCapacity"}, {"BMS", "ReMainCapacity", 1, nil, 1000, "RemainingCapacity"}, {"BMS", "VoltageOfPack", 1, nil, 1, "BatteryVoltage"}, {"BMS", "Current", 1, nil, 1, "BatteryCurrent"}, {"PCS", "BatteryPower", 1, nil, 1, "BatteryWattage"}, --先不自己计算 {"PCS", "BatteryPower", 1, nil, 1, "PowerAtCharge"}, --先不自己计算 {"PCS", "E_BatteryCharge", 1, nil, 1000, "_AccumulatedLifetimeCharge"}, --先不自己计算 {"PCS", "E_BatteryDischarge", 1, nil, 1000, "_AccumulatedLifetimeDischarge"}, --先不自己计算 {"PCS", "E_BatCharge_Today", 1, nil, 1, "_DailyCharge"}, --先不自己计算 {"PCS", "E_BatDischarge_Today", 1, nil, 1, "_DailyDischarge"}, --先不自己计算 {"PCS", "BMSPackTemp", 1, nil, 1, "BatteryTemp"}, {"PCS", "workMode", 1, nil, 1, "InverterMode"}, {"PCS", "Temperature", 1, nil, 1, "InverterTemp"}, {"PCS", "device_sn", 1, nil, nil, "PCS_ID"}, {"PCS", "version", 1, nil, nil, "PCS_SoftVer"}, {"PCS", "VGrid", 1, nil, 1, "GridVoltage"}, {"PCS", "IGrid", 1, nil, 1, "GridCurrent"}, {"PCS", "PGrid", 1, nil, 1, "GridWattage"}, {"PCS", "FGrid", 1, nil, 1, "GridFrequency"}, {"PCS", "Vpv1", 1, nil, 1, "Ppv1Voltage"}, {"PCS", "Ipv1", 1, nil, 1, "Ppv1Current"}, {"PCS", "Ppv1", 1, nil, 1, "Ppv1Wattage"}, {"PCS", "Vpv2", 1, nil, 1, "Ppv2Voltage"}, {"PCS", "Ipv2", 1, nil, 1, "Ppv2Current"}, {"PCS", "Ppv2", 1, nil, 1, "Ppv2Wattage"}, {"PCS", "E_PV_Total", 1, "TPvA", 1000, "TotalAccumulatedSolar"}, {"PCS", "E_total_Sell", 1, "E_total_Sell", 1, "TotalAccumulativePowerToGrId"}, {"PCS", "E_total_Buy", 1, "E_total_Buy", 1, "TotalAccumulativePowerFromGrid"} } BatterySelectDefult= { LeadBatCapacity=126,-- A.H BattChargeVoltMax=58.3,-- V BattChargeCurrMax=60.0,-- A BattDisChgCurrMax=75.0,-- A BattVoltUnderMin=45,-- V BattSOCUnderMin=80, BattVoltUnderMinOffGrid=44,-- V BattSOCUnderMinOffGrid=90 } local function alarm_status_update(devtype,identifier,tags,time) local change = nil if not devtype or not identifier or not tags or not tags.ErrorMsg then return end -- row = {devtype,identifier,errmsg,describt,level,start_time,clear_time,count} for _,row in ipairs(alarm_tab) do -- 从后向前查,后面权重高 if row[2] == identifier and row[1] == devtype then local errids = string.split(tags.ErrorMsg, ';') local find = false for _,errid in ipairs(errids) do if row[3] == errid then find = true end end if find then if not row[6] or (row[6] and row[7]) then --如果没有开始时间那说明是新的触发,如果开始和结束时间都存在说明故障已恢复 log.info("alarm","set flag:",identifier,row[3]) row[6] = time row[7] = nil row[8] = (row[8] or 0) + 1 change = true end else if not row[7] then --如果没有结束时间那说明是清除已有触发 log.info("alarm","clr flag:",identifier,row[3]) row[7] = os.time() row[9] = nil change = true end end end end return change end local function task_entry_hook(name) if not name then return end if not task[name] then task[name] = {os.time(),rtos.tick(),0,0,coroutine.running()} else task[name][1] = os.time() task[name][2] = rtos.tick() end end local function task_leave_hook(name) if not name or not task[name] then return end local tick = rtos.tick() if task[name][2] and tick >= task[name][2] then task[name][3] = tick - task[name][2] task[name][4] = task[name][4] + 1 else task[name][3] = 0 end end local function task_show() -- 进入循环时间,反应线程是否计入循环无法退出 -- tick -- tick 差值,反应循环有多忙碌 -- count ,10S内调用几次 log.warn("== system debug ==",string.format("name | entry time | used tick | count")) for name,t in pairs(task) do log.warn("== system debug ==",string.format("%s | %s | %d | %d",name,os.date("%H:%M:%S",t[1]),t[3],t[4]),t[5] and coroutine.status(t[5])) end end local function system_reply(cmd) local f = assert(io.popen(cmd)) local s = nil if f then unistd.sleep(1) s = assert(f:read('*a')) end f:close() return s end local function ifu_calculate() if not cache_fd then if not io.exists(EMU_DAT_FILE) then --没有直接创建文件 io.writeFile(EMU_DAT_FILE, "") end cache_fd=io.open(EMU_DAT_FILE, 'r+') end cache_fd:seek('set') local str = cache_fd:read() if str and #str > 0 then cache = cjson.decode(str) end if not cache then cache={} log.warn("calculate","INVALID CACHE FILE !!!") cache.ifu={ -- 逆变器 InverterMode=0,InterconnectionState=0,InverterTemp=0, -- 电池 BatteryVoltage=0,BatteryCurrent=0,BatteryTemp=0,BatteryWattage=0,ReMainCapacity = 0, RemainingCapacity=0,StateOfHealth=0,StateOfCharge=0,PowerAtCharge=0,TotalCapacity=0, AccumulatedLifetimeCharge=0,AccumulatedLifetimeDischarge=0,ChargeCapacity=0,DischargeCapacity=0, -- 电网 GridVoltage=0,GridCurrent=0,GridWattage=0,GridFrequency=0,TotalAccumulativePowerFromGrid=0,TotalAccumulativePowerToGrId=0, DailyCharge=0,DailyDischarge=0, -- 光伏 Ppv1=0,Ppv2=0,Ppv1Voltage=0,Ppv1Current=0,Ppv1Wattage=0, Ppv2Voltage=0,Ppv2Current=0,Ppv2Wattage=0, Ppv3Voltage=0,Ppv3Current=0,Ppv3Wattage=0,TotalAccumulatedSolar=0, -- 热水器 HWCurrent=0,HWWattage=0,HWVoltage=0,HWAccumulatedWattage=0,PowerFactor=0 } cache.date = os.date("*t", os.time()) cache_fd:seek('set') cache_fd:write(cjson.encode(cache) .. '\0') end if not last_cal then last_cal = os.time() end bms_valid = bms_obj and bms_obj.time and os.difftime(os.time(),bms_obj.time) < DEVICE_OFFLINE_TIME pcs_valid = pcs_obj and pcs_obj.time and os.difftime(os.time(),pcs_obj.time) < DEVICE_OFFLINE_TIME if not bms_valid then log.warn("calculate","Invalid source data," .. string.format("bms sample time %d",bms_obj and bms_obj.time or 0)) end if not pcs_valid then log.warn("calculate","Invalid source data," .. string.format("pcs sample time %d",pcs_obj and pcs_obj.time or 0)) end ifu_obj = cache.ifu -- 可以直接映射的变量 for i,v in pairs(symbol_remap) do if bms_valid and v and v[1] == "BMS" then if bms_obj[v[2]] and v[6] then log.warn("calculate",v[1],v[2],bms_obj[v[2]]) if type(bms_obj[v[2]]) == "number" and v[5] then ifu_obj[v[6]] = bms_obj[v[2]] * v[5] else ifu_obj[v[6]] = bms_obj[v[2]] end else log.warn("calculate","bms data not usable",v[2],bms_obj[v[2]]) end elseif pcs_valid and v and v[1] == "PCS" then if pcs_obj[v[2]] and v[6] then log.warn("calculate",v[1],v[2],pcs_obj[v[2]]) if type(pcs_obj[v[2]]) == "number" and v[5] then ifu_obj[v[6]] = pcs_obj[v[2]] * v[5] else ifu_obj[v[6]] = pcs_obj[v[2]] end else log.warn("calculate","pcs data not usable",v[2],ifu_obj[v[6]],pcs_obj[v[2]]) end end end if bms_valid then --如果第一次那么不计入统计 local interval = os.difftime(os.time(),last_cal) if not ifu_obj.ReMainCapacity or ifu_obj.ReMainCapacity == 0 then ifu_obj.ReMainCapacity = (bms_obj.ReMainCapacity or 0) * 1000 end --每天定时清空daily统计 local date = os.date("*t", os.time()); if not ifu_obj.DailyCharge or not ifu_obj.DailyDischarge or not cache.date or date["day"] ~= cache.date["day"] then ifu_obj.DailyCharge = 0 ifu_obj.DailyDischarge = 0 cache.date = date end -- if bms_obj.Model_SN and ifu_obj.BMS_ID and bms_obj.Model_SN ~= ifu_obj.BMS_ID then -- 查看BMS ID是否变更过 -- ifu_obj.AccumulatedLifetimeCharge = 0 -- ifu_obj.AccumulatedLifetimeDischarge = 0 -- end last_cal = os.time() log.info("calculate",string.format("Charge %d,Discharge %d,",bms_obj.charge or 0,bms_obj.discharge or 0)) if bms_obj.charge == 1 then local diff = math.abs(((bms_obj.ReMainCapacity or 0) * 1000) - (ifu_obj.ReMainCapacity or 0)) log.info("calculate",string.format("Now %f,Last %f,Capacity Of Change %f, Output",bms_obj.ReMainCapacity,ifu_obj.ReMainCapacity,diff)) ifu_obj.AccumulatedLifetimeCharge = (ifu_obj.AccumulatedLifetimeCharge or 0) + diff ifu_obj.DailyCharge = ifu_obj.DailyCharge + diff ifu_obj.ReMainCapacity = (bms_obj.ReMainCapacity or 0) * 1000 elseif bms_obj.discharge == 1 then local diff = math.abs(((bms_obj.ReMainCapacity or 0) * 1000) - (ifu_obj.ReMainCapacity or 0)) log.info("calculate",string.format("Now %f,Last %f,Capacity Of Change %f, Output",bms_obj.ReMainCapacity,ifu_obj.ReMainCapacity,diff)) ifu_obj.AccumulatedLifetimeDischarge = (ifu_obj.AccumulatedLifetimeDischarge or 0) + diff ifu_obj.DailyDischarge = ifu_obj.DailyDischarge + diff ifu_obj.ReMainCapacity = (bms_obj.ReMainCapacity or 0) * 1000 end --计算平均温度,最大最小电压 local temp_sum,temp_count,tab = 0,0,{} for i=1, BATT_TEMP_COUNT do local temp = bms_obj["Temperature_" .. i] if not temp then break end temp_sum = temp_sum + temp temp_count = i table.insert(tab,temp) end if temp_count > 0 then ifu_obj.BatteryTemp = temp_sum/BATT_TEMP_COUNT else ifu_obj.BatteryTemp = 0 end if #tab == BATT_TEMP_COUNT then table.sort(tab) ifu_obj.TempMaxBat = tab[#tab] ifu_obj.TempMinBat = tab[1] end if bms_obj.VoltageOfPack and bms_obj.Current then ifu_obj.Pbat = bms_obj.VoltageOfPack * bms_obj.Current end --计算电池温度,最高电压,最低电压,压差 local tab = {} for i=1, BATT_VOL_COUNT do local vol = bms_obj["Voltage_Unit_" .. i] if not vol then break end -- tab[string.format( "%d",i)] = vol table.insert(tab,vol) end if #tab == BATT_VOL_COUNT then table.sort(tab) ifu_obj.VolMaxbat = tab[#tab] ifu_obj.VolMinbat = tab[1] if ifu_obj.VolMaxbat and ifu_obj.VolMinbat then ifu_obj.VolDiffbat = ifu_obj.VolMaxbat - ifu_obj.VolMinbat if ifu_obj.VolDiffbat and type(ifu_obj.VolDiffbat) == "number" then ifu_obj.AlarmVolDiffLevel = 0 if ifu_obj.VolDiffbat > 100 then ifu_obj.AlarmVolDiffLevel = 1 elseif ifu_obj.VolDiffbat > 200 then ifu_obj.AlarmVolDiffLevel = 2 elseif ifu_obj.VolDiffbat > 200 then ifu_obj.AlarmVolDiffLevel = 3 end end end end -- 找出电池ID for i=1, BATT_VOL_COUNT do local vol = bms_obj["Voltage_Unit_" .. i] if vol and ifu_obj.VolMaxbat and ifu_obj.VolMaxbat == vol then ifu_obj.VolMaxBatNo = i end if vol and ifu_obj.VolMinbat and ifu_obj.VolMinbat == vol then ifu_obj.VolMinBatNo = i end end end cache.ifu = ifu_obj cache_fd:seek('set') cache_fd:write(cjson.encode(cache) .. '\0') end local function schedule_of_used() local obj = {} if io.exists(SELECT_SCHEDULES_FILE) then local str = io.readFile(SELECT_SCHEDULES_FILE) if str and #str > 0 then obj = cjson.decode(str or '') if obj then return obj.ScheduleName else log.error("schedule",'select schedule failed') end end end obj.ScheduleName = "Default" io.writeFile(SELECT_SCHEDULES_FILE, cjson.encode(obj), 'w') return obj.ScheduleName end local function get_timezone() if io.exists(EMU_CONFIG_FILE) then local body = nil local str = io.readFile(EMU_CONFIG_FILE) local item, res, err = cjson.decode(str) if item and item["timezone"] then return item["timezone"] end end end local function schedule_body(file,tab,name) if io.exists(file) then local body = nil local str = io.readFile(file) local item, res, err = cjson.decode(str) if item and item[tab] then for _,v in pairs(item[tab]) do if v.ScheduleName == name then --查找正在运行的调度器 body = v break end end -- 去除具体内容 -- log.info("fetch schedule of system used",cjson.encode(item[tab]),cjson.encode(body)) return item[tab],body else log.error("schedule",'get schedule body failed') end end end local function mqtt_send(topic,payload) if not client then return end local mid,errid,errmsg = client:publish(topic, payload) if not mid then log.error("mqtt.send","error:", errid,errmsg) end end local function ServiceResponse(gw_sn,dev_sn,port,identifier,param,mi,time) local topic = string.format( "ipc/%s/%s/device/%s/data_filtered/service/%s",gw_sn,port,dev_sn,identifier) local obj_send={} local json_str obj_send['mi'] = mi or 0 -- obj_send['len'] = #data obj_send['identifier'] = identifier obj_send['sn'] = dev_sn obj_send['time'] = time or os.time() obj_send["data_type"] = 2 obj_send["tags"] = param and type(param) == "table" and param or cjson.decode(param) json_str = cjson.encode(obj_send) log.info("ServiceResponse",topic,json_str) mqtt_send(topic, json_str) end local function Event(gw_sn,dev_sn,port,identifier,param,mi,time) local topic = string.format( "ipc/%s/%s/device/%s/data/event/%s",gw_sn,port,dev_sn,identifier) local obj_send={} local json_str obj_send['mi'] = mi or 0 -- obj_send['len'] = #data obj_send['identifier'] = identifier obj_send['sn'] = dev_sn obj_send['time'] = time or os.time() obj_send["data_type"] = 1 obj_send["tag_node"] = type(param) == "string" and param or cjson.encode(param) json_str = cjson.encode(obj_send) log.info("Event",topic,json_str) mqtt_send(topic, json_str) end local function Set_Rglt_Paser(obj) if obj.identifier == "FetchTOU" then local name = schedule_of_used() local msg = {} for _,scheFile in pairs(scheFiles) do local tab,body = schedule_body(scheFile[1],scheFile[2],name) if body then msg["RunSchedule"] = body end if tab and #tab > 0 then msg[scheFile[2]]={} for _,v in pairs(tab) do table.insert(msg[scheFile[2]], { ScheduleName=v.ScheduleName, ScheduleCreateTime =v.ScheduleCreateTime , Author=v.Author } ) end end end log.info("RunSchedule",cjson.encode(msg)) ServiceResponse(gw_sn,emu_sn,emu_port,obj.identifier,{out=cjson.encode(msg)},obj.mi) elseif obj.identifier == "SelectTOU" then if obj.ScheduleName then used_TOU = obj.ScheduleName io.writeFile(SELECT_SCHEDULES_FILE, cjson.encode(obj), 'w') end ServiceResponse(gw_sn,emu_sn,emu_port,obj.identifier,{},obj.mi) elseif obj.identifier == "EditCustomTOU" then io.writeFile(CUSTOM_SCHEDULES_FILE, cjson.encode(obj), 'w') ServiceResponse(gw_sn,emu_sn,emu_port,obj.identifier,{},obj.mi) elseif obj.identifier == "FetchHistoryData" then ServiceResponse(gw_sn,emu_sn,emu_port,obj.identifier,{},obj.mi) -- local ret,start,stop,devsn,devid = true,os.time()-3*60,os.time()+3*60,{"218801000006_PCS","218801000006_BMS_1"},{"HybridInventerRundata","SystemInformationRequest"} sys.publish("FetchHistoryData",obj.StartTime,obj.EndTime,{pcs_sn,bms_sn},{"HybridInventerRundata","SystemInformationRequest"}) -- 发送查询数据请求只查询这些服务 elseif obj.identifier == "OffOfService" then if obj.enable then off_service = obj.enable == 1 and true or false end ServiceResponse(gw_sn,emu_sn,emu_port,obj.identifier,{},obj.mi) elseif obj.identifier == "FetchServiceStatus" then ServiceResponse(gw_sn,emu_sn,emu_port,obj.identifier,{status= off_service and 0 or 1},obj.mi) end end local function ServiceCall(gw_sn,dev_sn,port,identifier,param) if off_service then return end local topic = string.format( "ipc/%s/%s/device/%s/data/Set_Rglt",gw_sn,port,dev_sn) local obj_send={} local json_str obj_send['mi'] = mi -- obj_send['len'] = #data obj_send['identifier'] = identifier obj_send['sn'] = dev_sn obj_send['time'] = os.time() if param then for k,v in pairs(param)do obj_send[k] = v end end json_str = cjson.encode(obj_send) log.info("ServiceCall",topic,json_str) mqtt_send(topic, json_str) end local function schedule_process(obj) if obj and obj.Signal and obj.PowerRate then log.info("schedule_process",obj.Signal,obj.PowerRate) if obj.Signal == 0 then -- Auto 0 RemoteControl.mode = 0xA RemoteControl.power = 0 elseif obj.Signal == 1 then -- Charge 1 RemoteControl.mode = 0x2 RemoteControl.power = obj.PowerRate == 0 and obj.PowerRate or 9500 elseif obj.Signal == 2 then -- Discharge 2 RemoteControl.mode = 0x1 RemoteControl.power = 0 elseif obj.Signal == 3 then -- Standby 3 RemoteControl={} end else log.warn("Invalid schedule message",cjson.encode(obj)) end end local function on_mqtt_msg(topic, payload) -- ipc/21012C000038/VIRTUAL_1/device/333333333/data_filtered/property/vdev_data local obj local alarm_change = nil if not payload or #payload == 0 then return end if topic == topic_sub[1] or topic == topic_sub[2] then if string.find(payload,"start_upgrade") then off_service = true elseif string.find(payload,"stop_upgrade") then off_service = false end log.warn("upgrade","Notify upgrade",payload) return elseif topic == topic_sub[9] then obj = cjson.decode(payload) pcs_obj = obj.tags pcs_obj.SN = obj.sn pcs_obj.time = os.time() elseif topic == topic_sub[10] then obj = cjson.decode(payload) bms_obj = obj.tags bms_obj.sn = obj.sn bms_obj.time = os.time() elseif topic == topic_sub[3] or topic == topic_sub[4] or topic == topic_sub[5] then obj = cjson.decode(payload) if obj and obj.tag_node then local json_obj = cjson.decode(obj.tag_node) if json_obj then alarm_change = alarm_status_update("PCS",obj.identifier,json_obj,obj.time or os.time()) end end elseif topic == topic_sub[6] or topic == topic_sub[7] or topic == topic_sub[8] then obj = cjson.decode(payload) if obj and obj.tag_node then local json_str = cjson.decode(obj.tag_node) if json_obj then alarm_change = alarm_status_update("BMS",obj.identifier,json_obj,obj.time or os.time()) end end elseif topic == topic_sub[12] then --调度任务 obj = cjson.decode(payload) schedule_process(obj) else log.info("discard topic:",topic) end if alarm_change then --如果报警变化过 -- ServiceResponse(gw_sn,obj.sn,obj.port,obj.identifier,obj.tag_node,0,obj.time) Event(gw_sn,obj.sn,obj.port,obj.identifier,obj.tag_node,0,obj.time) end end ---------------------------------------------------------- 外部消息---------------------------------------------------------- local function mqtt_session(id, addr, port, usr, pwd, subscribe, qos,func) cid, keepAlive, timeout = tonumber(cid) or 1, tonumber(keepAlive) or 300, tonumber(timeout) or 1800 cleansession, qos, retain = tonumber(cleansession) or 0, tonumber(qos) or 0, tonumber(retain) or 0 sys.taskInit(function() mqtt.init() client = mqtt.new() client.ON_CONNECT = function() log.warn("mqtt","client connected") for _,v in pairs(subscribe) do if type(v) == "string" then client:subscribe(v) end end end client.ON_MESSAGE = function(mid, topic, payload) log.info("mqtt recv",topic, payload) if topic == topic_sub[11] then -- 控制命令通道直接调用 local obj,err = cjson.decode(payload) Set_Rglt_Paser(obj) else table.insert(recvBuff,{topic,payload or ''}) sys.publish("INTERNAL_CLIENT_RECV_INT") end end client.ON_DISCONNECT = function() log.warn("mqtt","client disconnected") end client:connect(addr,port) task_entry_hook("mqtt.client") sys.poll_socket=function() client:loop(3000,1) end while(true) do while client:socket() do sys.wait(2000) end sys.wait(1000) client:reconnect() end end) end local function conver2mosquito(gw_sn,gw_port,dev_sn,identifier,param) if not gw_sn or not gw_port or not dev_sn then return end local topic = string.format( "ipc/%s/%s/device/%s/data/Set_Rglt",gw_sn,gw_port,dev_sn) obj_send={} obj_send['mi'] = 0 obj_send['identifier'] = identifier obj_send['sn'] = dev_sn for k,v in pairs(param)do obj_send[k] = v end local payload = string.gsub(cjson.encode(obj_send),"\"","\\\"",99999) return topic,payload end local function db_concat(tab) local tmp if tab and type(tab) =='table' then for i,v in ipairs(tab) do if v and type(v) == 'string' then if not tmp then tmp = '' end if i == #tab then tmp = tmp .. string.format( "'%s'",v ) else tmp = tmp .. string.format( "'%s',",v ) end end end end return tmp end local function handler() local pid, status, code = syswait.wait(-1, syswait.WNOHANG) log.error('system exit', pid, status, code) --打印不出来 os.exit(0) end function main(nodes_cfg,sn) -- local last_used_TOU task_entry_hook("system.init") log.info("calculate", "=============== EMU FOR HOME RUN ============") local subscribe={"ipc/+/+/device/+/data_filtered/property/post","ipc/+/+/device/+/data/service/+","ipc/+/VIRTUAL_1/device/+/data/Set_Rglt","ipc/IFU/BatterySchedule/#","ipc/+/+/Notify_Upgrade"} mqtt_session("emuForHome","localhost", 1883 , nil, nil, subscribe, 0) gw_sn = system_reply("/lib/dyiot/bin/factory get | grep SN | awk -F = '{print $2}' | tr '\\n' '\\0'") pcs_sn = string.format( "%s_PCS",gw_sn) bms_sn = string.format( "%s_BMS_1",gw_sn) emu_sn = string.format( "%s_EMU",gw_sn) pcs_port = "RS485_1" bms_port = "RS485_2" emu_port = "VIRTUAL_1" log.info("sn",gw_sn,pcs_sn,bms_sn,emu_sn) topic_sub[1] = string.format( "ipc/%s/%s/Notify_Upgrade",gw_sn,pcs_port ) topic_sub[2] = string.format( "ipc/%s/%s/Notify_Upgrade",gw_sn,bms_port ) topic_sub[3] = string.format( "ipc/%s/%s/device/%s/data/service/AlarmDiagStatus",gw_sn,pcs_port,pcs_sn ) topic_sub[4] = string.format( "ipc/%s/%s/device/%s/data/service/AlarmWarningCode",gw_sn,pcs_port,pcs_sn ) topic_sub[5] = string.format( "ipc/%s/%s/device/%s/data/service/AlarmErrorMessage",gw_sn,pcs_port,pcs_sn ) topic_sub[6] = string.format( "ipc/%s/%s/device/%s/data/service/AlarmFault",gw_sn,bms_port,bms_sn ) topic_sub[7] = string.format( "ipc/%s/%s/device/%s/data/service/AlarmWarning",gw_sn,bms_port,bms_sn ) topic_sub[8] = string.format( "ipc/%s/%s/device/%s/data/service/AlarmProtection",gw_sn,bms_port,bms_sn ) topic_sub[9] = string.format( "ipc/%s/%s/device/%s/data_filtered/property/post",gw_sn,pcs_port,pcs_sn ) topic_sub[10] = string.format( "ipc/%s/%s/device/%s/data_filtered/property/post",gw_sn,bms_port,bms_sn ) topic_sub[11] = string.format( "ipc/%s/%s/device/%s/data/Set_Rglt",gw_sn,emu_port,emu_sn ) topic_sub[12] = string.format( "ipc/IFU/BatterySchedule/Start") for _,v in pairs(topic_sub) do log.info("subscribe topic list",v) end siganl.signal(siganl.SIGKILL,handler) siganl.signal(siganl.SIGHUP,handler) -- schedule calendar sys.taskInit(function() local last_time = 0 while true do sys.wait(2000) task_entry_hook("schedule") if (used_TOU and used_TOU ~= last_used_TOU) or (os.difftime(os.time(),last_time) > 24*3600) then last_time = os.time() last_used_TOU = used_TOU local tmp,_,body={} local name = schedule_of_used() for i,scheFile in pairs(scheFiles) do _,tmp[i] = schedule_body(scheFile[1],scheFile[2],name) if tmp[i] then body = tmp[i] end end log.info("Run schedule",cjson.encode(body)) -- log.info("schedule",(cjson.encode(body))) if body and body.ScheduleItems and #body.ScheduleItems > 0 then --删除所有未执行调度器 local cmd = "atq | awk '{print $1}' | tr \"\\n\" \" \" | xargs atrm" log.info("Remove schedule",cmd) os.execute(cmd) log.info("Deploy schedule",string.format("Deploy schedule name:%s with author %s",body.ScheduleName or "",body.Author and type(body.Author == "string") or "")) local timezone = get_timezone() or "+00:00" for _,item in ipairs(body.ScheduleItems) do if item and item.Signal and item.PowerRate then -- 测试阶段还原时区, 不使用UTC时区,不使用年月日 if item.StartDate then local payload = cjson.encode(item) -- local payload = string.gsub(cjson.encode(item),"\"","\\\"",99999) -- 转为标准格式,把星期放到后面 cmd = string.format('/lib/dyiot/bin/schedule_add.sh \'%s\' \'%s\' \'%s\'',item.StartDate,timezone,payload) os.execute(cmd) log.info("schedule",string.format("Schedule execute %s",cmd)) end end end end end task_leave_hook("schedule") end end) -- battery select sys.taskInit(function() local last_time = 0 local last_retrans = 0 -- 设置BMS index local obj={RegAddr=0x566,RegValue=0xA1} ServiceCall(gw_sn,pcs_sn,pcs_port,"WriteRegsiterOfModbus",obj) -- 设置BMS protocol local obj={RegAddr=0x5A3,RegValue=30} ServiceCall(gw_sn,pcs_sn,pcs_port,"WriteRegsiterOfModbus",obj) -- disable feed power to grid local obj={RegAddr=0x559,RegValue=1} ServiceCall(gw_sn,pcs_sn,pcs_port,"WriteRegsiterOfModbus",obj) -- set power of feed power to grid local obj={RegAddr=0x567,RegValue=0} ServiceCall(gw_sn,pcs_sn,pcs_port,"WriteRegsiterOfModbus",obj) local batt_param = BatterySelectDefult ServiceCall(gw_sn,pcs_sn,pcs_port,"BatterySelect",batt_param) while true do sys.wait(5000) task_entry_hook("battery") if os.difftime(os.time(),last_time) > ADJ_BATT_INTERVAL then last_time = os.time() -- set power of feed power to grid local obj={RegAddr=0x5A0,RegValue=BatterySelectDefult.BattVoltUnderMinOffGrid*10} ServiceCall(gw_sn,pcs_sn,pcs_port,"WriteRegsiterOfModbus",obj) local obj={RegAddr=0x5A1,RegValue=BatterySelectDefult.BattSOCUnderMinOffGrid*10} ServiceCall(gw_sn,pcs_sn,pcs_port,"WriteRegsiterOfModbus",obj) sys.wait(1000) ServiceCall(gw_sn,pcs_sn,pcs_port,"BatterySelect",batt_param) end task_leave_hook("battery") end end) -- calculate ang data report sys.taskInit(function() local last_time = 0 local report_time = 0 while true do sys.wait(1000) task_entry_hook("report") if os.difftime(os.time(),last_time) >= CALCULATE_INTERVAL then last_time = os.time() ifu_calculate() end if os.difftime(os.time(),report_time) >= REPORT_INTERVAL and (bms_valid or pcs_valid) then report_time = os.time() ServiceResponse(gw_sn,emu_sn,emu_port,"post",ifu_obj,0) end task_leave_hook("report") end end) -- Report history service data sys.taskInit(function() while true do local db = sqlite3.open(HISTORY_DATABASE_FILE) while true do local ret,start,stop,devsn,devid = sys.waitUntil("FetchHistoryData", 1000) task_entry_hook("history") if ret then recording = true repeat local now = os.time() if not start then start = now end if not stop then stop = now end --如果结束时间没有到那么使用当前时间,而且开始循环等待 -- id|sn|tag|time|identifier|type|report|disable_report|report_period|port db:busy_timeout(5000) local sql,SNs,IDs SNs = db_concat(devsn) and string.format("and sn in(%s)",db_concat(devsn)) or "" IDs = db_concat(devid) and string.format("and identifier in(%s)",db_concat(devid)) or "" sql = string.format("SELECT sn,tag,time,identifier,port FROM data_table WHERE time BETWEEN %d and %d %s %s",start,(stop > now and now or stop) ,SNs,IDs) log.info("db",sql) for sn,tag,time,identifier,port in db:urows(sql) do if not sn or not tag or not time or not identifier or not port then log.error("db",db:errcode(),db:errmsg()) break end log.info("db",sn,tag,time,identifier,port) ServiceResponse(gw_sn,sn,port,identifier,tag,0,time) sys.wait(500) end if stop > now then start = now --如果停止时间还没有到重置下次开始采用时间,否则赋值任意数,后面会直接退出循环 sys.wait(30 * 1000) else start = nil end until stop <= now log.info("db","history data sent!") recording = false end task_leave_hook("history") end break end db:close() -- close sys.wait(1000) end) -- alarm toggle history date sys.timerLoopStart(function() local now = os.time() -- row = {devtype,identifier,errmsg,describt,level,start_time,clear_time,count} local valid = false if not ALARM_SAMPLE_SHEILD then for _,row in ipairs(alarm_tab) do if not row[9] and (row[5] == 3 and not row[7] and row[6]) or (row[5] == 2 and not row[7] and row[6] and row[6] > 0 and os.difftime(now,row[6]) >= ALARM_SEGMENT) then row[9] = true valid = true log.info("alarm", "alarm has toggled by",row[1],row[2],row[3],row[4],row[5],"at",os.time()) end end if valid then if recording then log.warn("alarm", "record task busy") else sys.publish("FetchHistoryData",os.time()-ALARM_BEFORE_SAMPLE,os.time()+ALARM_AFTER_SAMPLE,{pcs_sn,bms_sn},{"HybridInventerRundata","SystemInformationRequest"}) -- 发送查询数据请求只查询这些服务 end end end end,5 * 1000) -- 充电点参数循环发送 sys.timerLoopStart(function() if next(RemoteControl) ~= nil then ServiceCall(gw_sn,pcs_sn,pcs_port,"DisCharge_Charge_Set",RemoteControl) end end,RETRANS_INTERVAL * 1000) -- mqtt received and process sys.taskInit(function() while true do sys.waitUntil("INTERNAL_CLIENT_RECV_INT",2000) task_entry_hook("mqtt.recv") if #recvBuff > 0 then local packet = table.remove(recvBuff) if packet then on_mqtt_msg(packet[1],packet[2]) packet = nil end end task_leave_hook("mqtt.recv") end end) -- -- debug -- sys.taskInit(function() -- while true do -- sys.wait(1000) -- task_entry_hook("task.show") -- task_show() -- task_leave_hook("task.show") -- end -- end) --启动系统框架 sys.init(0, 0) sys.run() end function main_loop(nodes_cfg,sn) local ret,err = pcall(main,nodes_cfg,sn) if not ret then -- log.error("=====SYSTEM====",err) end return 99,err end main_loop(arg[1],arg[2])