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 --故障屏蔽 PACK_CAPACITY = 105 --105AH PACK_VOLTAGE_LOW_THR = 44 --20V PACK_SOC_LOW_THR = 20 --20% ifu_obj={} pcs_obj={} pack_obj={} RemoteControl = {} sendBuff={} recvBuff={} task={} topic_sub={} cache_fd =nil used_TOU = nil stop_control = false -- stop_sample = 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=105,-- 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 pcs_valid = pcs_obj and pcs_obj.time and os.difftime(os.time(),pcs_obj.time) < DEVICE_OFFLINE_TIME 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 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 local max_voltage,sum_current,sum_soc,sum_soh = 0,0,0,0 local active_count = 0 local max_vol,max_vol_cell_id,max_vol_pack_id,min_vol,min_vol_cell_id,min_vol_pack_id local max_temp,max_temp_cell_id,max_temp_pack_id,min_temp,min_temp_cell_id,min_temp_pack_id now = os.time() for k,v in pairs(pack_obj) do if v and v.time and os.difftime(os.time(),v.time) < DEVICE_OFFLINE_TIME then active_count = active_count + 1 sum_soc = sum_soc + v.SOC sum_soh = sum_soh + v.SOH sum_current = sum_current + v.Current max_voltage = v.Voltage > max_voltage and v.Voltage or max_voltage local ReMainCapacity = PACK_CAPACITY * sum_soc / 100 log.info("calculate",string.format("ChangeState %d,ReMainCapacity %d",v.ChangeState,ReMainCapacity)) if v.ChangeState == 2 then local diff = math.abs(((ReMainCapacity or 0) * 1000) - (ifu_obj.ReMainCapacity or 0)) log.info("calculate",string.format("Now %f,Last %f,Capacity Of Change %f, Output",ReMainCapacity,ifu_obj.ReMainCapacity,diff)) ifu_obj.AccumulatedLifetimeCharge = (ifu_obj.AccumulatedLifetimeCharge or 0) + diff ifu_obj.DailyCharge = ifu_obj.DailyCharge + diff ifu_obj.ReMainCapacity = (ReMainCapacity or 0) * 1000 elseif v.ChangeState == 1 then local diff = math.abs(((ReMainCapacity or 0) * 1000) - (ifu_obj.ReMainCapacity or 0)) log.info("calculate",string.format("Now %f,Last %f,Capacity Of Change %f, Output",ReMainCapacity,ifu_obj.ReMainCapacity,diff)) ifu_obj.AccumulatedLifetimeDischarge = (ifu_obj.AccumulatedLifetimeDischarge or 0) + diff ifu_obj.DailyDischarge = ifu_obj.DailyDischarge + diff ifu_obj.ReMainCapacity = (ReMainCapacity or 0) * 1000 end if not max_vol or v.MaxCellVoltage > max_vol then max_vol = v.MaxCellVoltage max_vol_cell_id = v.MaxVolCellID max_vol_pack_id = tonumber(string.match(v.sn,"_(%d+)")) end if not min_vol or v.MinCellVoltage < min_vol then min_vol = v.MinCellVoltage min_vol_cell_id = v.MinVolCellID min_vol_pack_id = tonumber(string.match(v.sn,"_(%d+)")) end if not max_temp or v.MaxCellTemp > max_temp then max_temp = v.MaxCellTemp max_temp_cell_id = v.MaxTempCellID max_temp_pack_id = tonumber(string.match(v.sn,"_(%d+)")) end if not min_temp or v.MinCellTemp < min_temp then min_temp = v.MinCellTemp min_temp_cell_id = v.MinTempCellID min_temp_pack_id = tonumber(string.match(v.sn,"_(%d+)")) end else v = nil --如果时间过期直接清零 end end if active_count > 0 then ifu_obj.StateOfCharge = sum_soc/active_count ifu_obj.StateOfHealth = sum_soh/active_count ifu_obj.BatteryVoltage = max_voltage ifu_obj.BatteryCurrent = sum_current/active_count ifu_obj.VolMaxbat = max_vol ifu_obj.VolMinbat = min_vol ifu_obj.VolMinBatNo = min_vol_cell_id ifu_obj.VolMaxBatNo = max_vol_cell_id ifu_obj.VolMinPackNo = min_vol_pack_id ifu_obj.VolMaxPackNo = max_vol_pack_id ifu_obj.TempMaxBat = max_temp ifu_obj.TempMinBat = min_temp 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 if ifu_obj.BatteryVoltage and ifu_obj.BatteryCurrent then ifu_obj.Pbat = ifu_obj.BatteryVoltage * ifu_obj.BatteryCurrent end if (ifu_obj.BatteryVoltage and ifu_obj.BatteryVoltage < PACK_VOLTAGE_LOW_THR) or (ifu_obj.StateOfCharge and ifu_obj.StateOfCharge <= PACK_SOC_LOW_THR) then sys.publish("BMS_LOW_POWER") end 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 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 -- 禁用BMS采样,恢复BMS采样 local function stopSample(gw_sn,bms_port,enable) local topic = string.format( "ipc/%s/%s/Notify_Upgrade",gw_sn,bms_port) mqtt_send(topic,enable and "start_upgrade" or "stop_upgrade") --暂停所有轮询 end -- 拆分MQTT消息,转为内部变量和obj local function MessageSpilt(topic,payload) local gw_sn,dev_sn,port,identifier,param,mi,time local s = string.split(topic, '/') if s[1] == 'ipc' then gw_sn = s[2] port = s[3] if s[4] == "device" then dev_sn = s[5] identifier = s[8] local obj = cjson.decode(payload) if obj then mi = obj.mi time = bj.time if s[6] == "data_filtered" then param = obj["tags"] elseif s[6] == "data" and obj["tag_node"]then param =cjson.decode(obj["tag_node"]) else return nil end else return true,gw_sn,dev_sn,port,identifier end else return nil end end return true,gw_sn,dev_sn,port,identifier,param,mi,time 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 stop_control = 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= stop_control and 0 or 1},obj.mi) end end -- 调动内部服务 local function ServiceCall(gw_sn,dev_sn,port,identifier,param,mi) 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 or 0 -- 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 = 0x2 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 if not payload or #payload == 0 then return end local ret,gw_sn,dev_sn,port,identifier,param,mi,time = MessageSpilt(topic,payload) log.info("MessageSpilt",ret,gw_sn,dev_sn,port,identifier,param,mi,time) if not ret then if topic == topic_sub[1]then if string.find(payload,"start_upgrade") then stop_control = true elseif string.find(payload,"stop_upgrade") then stop_control = false end log.warn("upgrade","Notify upgrade",payload) return elseif topic == topic_sub[4] then --调度任务 obj = cjson.decode(payload) schedule_process(obj) else log.info("discard topic:",topic) end else if port == "RS485_1" and identifier == "post" then pcs_obj = param pcs_obj.SN = dev_sn pcs_obj.time = os.time() elseif port == "RS485_2" and identifier == "post" then pack_obj[dev_sn] = param pack_obj[dev_sn].sn = dev_sn pack_obj[dev_sn].time = os.time() elseif topic == topic_sub[3] or topic == topic_sub[4] or topic == topic_sub[5] then if alarm_status_update("PCS",identifier,param,time or os.time()) then Event(gw_sn,dev_sn,port,identifier,param,0,time) end elseif topic == topic_sub[6] or topic == topic_sub[7] or topic == topic_sub[8] then if alarm_status_update("BMS",identifier,param,time or os.time()) then Event(gw_sn,dev_sn,port,identifier,param,0,time) end end 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[2] 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/device/%s/data/Set_Rglt",gw_sn,emu_port,emu_sn ) topic_sub[3] = 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 and not stop_control 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 and not stop_control 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) -- 休眠处理逻辑 sys.taskInit(function() while true do local ret = sys.waitUntil("BMS_LOW_POWER",10000) --最快一个计算周期才会产生一次,如果处于欠压状态,下次采样仍然会触发这个信号,产生周期等于计算周期 if ret then --收到低功耗触发逻辑 if ifu_obj.BatteryCurrent < -1.0 then --如果已经在充电,判断充电电流是否足够,如果电流不够不唤醒板子,充电电流是负数 log.info("log power","BatteryCurrent",ifu_obj.BatteryCurrent) elseif pcs_valid and pcs_obj.GridVoltage > 100 then --有市电开启充电模式 RemoteControl.mode = 0x2 RemoteControl.power = 3600 else -- 没有充电,也没有市电,被迫等待电池休眠,休眠后电池断开放电MOS stopSample(gw_sn,bms_port,true) --先将BMS 采样停掉保证休眠 log.info("EMU","entry low power mode!") while true do sys.wait(30000) --等待30 if pcs_valid and pcs_obj.GridVoltage > 100 or ifu_obj.BatteryCurrent < -1.0 then --有市电开启充电模式 ;充电电流是负数 break end end stopSample(gw_sn,bms_port,false) --先将BMS 采样停掉保证休眠 log.info("EMU","leave low power mode!") end end 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])