require "log" require "sys" require "utils" require "patch" require "pack" signal = require 'posix.signal' syswait = require 'posix.sys.wait' unistd = require 'posix.unistd' mqtt=require "mosquitto" base64=require("base64") dyutils=require("dyutils") lpack=require("lua_pack") require "internal_api" 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 --故障屏蔽 EMU_SN_SUFFIX="_EMU" local TIMEZONE_OFFSET = 8*3600 local ifu_obj={} local pcs_obj={} local bms_obj={} local RemoteControl = {} local task={} local topic_sub={} local initalized = false local cache_fd =nil local used_TOU = nil local off_service = false local inital_param_reg={} local local_job={Sun={},Mon={},Tue={},Wed={},Thu={},Fri={},Sat={}} local singal_pase_modes={AUTO=0xA,CHA=2,DISCH=1,STANBY=nil} local signal_modes={S0="AUTO",S1="CHA",S2="DISCH",S3="STANBY"} local scheFiles = {{CUSTOM_SCHEDULES_FILE,"LocalSchedules"},{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"} } 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 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 -- file:文件路径 -- tab:调度器表名 -- name:当前选中的策略名 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 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 iot2app(topic,payload) -- print("iot2app",topic,payload) if string.find(topic,EMU_SN_SUFFIX) then local obj = cjson.decode(payload) if obj.identifier == "FetchTOU" then local name = schedule_of_used() local msg = {} for _,scheFile in pairs(scheFiles) do -- 可选的几个调度器 log.info("RunSchedule",_,scheFile[1]) local tab,body = schedule_body(scheFile[1],scheFile[2],name) if body then msg["RunSchedule"] = body end if tab and #tab > 0 then if not msg.LocalSchedules then msg.LocalSchedules={} end for _,v in pairs(tab) do table.insert(msg.LocalSchedules, { ScheduleName=v.ScheduleName, ScheduleCreateTime =v.ScheduleCreateTime , Author=v.Author } ) end end end print("RunSchedule",cjson.encode(msg)) internal_api.service_response(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 internal_api.service_response(gw_sn,emu_sn,emu_port,obj.identifier,{},obj.mi) elseif obj.identifier == "EditCustomTOU" then io.writeFile(CUSTOM_SCHEDULES_FILE, cjson.encode(obj), 'w') internal_api.service_response(gw_sn,emu_sn,emu_port,obj.identifier,{},obj.mi) elseif obj.identifier == "FetchHistoryData" then internal_api.service_response(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 internal_api.service_response(gw_sn,emu_sn,emu_port,obj.identifier,{},obj.mi) elseif obj.identifier == "FetchServiceStatus" then internal_api.service_response(gw_sn,emu_sn,emu_port,obj.identifier,{status= off_service and 0 or 1},obj.mi) end end end local function pp2app(topic,payload) local currTime = os.time() -- print("pp2app",topic,payload) local obj = cjson.decode(payload) local tags = obj.tag_node and cjson.decode(obj.tag_node) or nil ---- 非通用方式计算 -- paser_special(obj,tags) -- ---- 先对数据做汇聚 -- realDataCala(obj,tags) -- ---- 用原始数据做只有原始数据做报警预处理 -- realAlarmCala(obj,tags) if 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 end if alarm_change then --如果报警变化过 -- internal_api.service_response(gw_sn,obj.sn,obj.port,obj.identifier,obj.tag_node,0,obj.time) internal_api.event(gw_sn,obj.sn,obj.port,obj.identifier,obj.tag_node,0,obj.time) end end local function others_cb(topic,payload) local currTime = os.time() local obj local alarm_change = nil if not payload or #payload == 0 then return end 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) end local function history_service() while not initalized do sys.wait(1000) end while true do local db = sqlite3.open(HISTORY_DATABASE_FILE) while true do local ret,start,stop,devsn,devid = sys.waitUntil("FetchHistoryData", 1000) 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) internal_api.service_response(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 end break end db:close() -- close sys.wait(1000) end local function alarm_toggle() while not initalized do sys.wait(1000) end 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 local function spilt_day(str) local week,hour,min local s = string.split(str," ") if s[1] and #s[1] == 3 then week = s[1] end if s[2] then local t = string.split(s[2],":") if t[1] and t[2] then hour = tonumber(t[1]) min = tonumber(t[2]) end end if week and hour and min then return true,week,hour,min else return false end end local function get_cerrjob(jobs) local timestamp = os.time() + TIMEZONE_OFFSET local date = os.date("*t",timestamp) local job local index_conform={} local found for index,obj in ipairs(jobs) do local conflict_index,found_index local t_s,t_e,mode,power = obj[1],obj[2],obj[3],obj[4] local time_s = os.time({year =date.year, month = date.month, day =date.day, hour = t_s[1], min = t_s[2], sec = date.sec }) local time_e = os.time({year =date.year, month = date.month, day =date.day, hour = t_e[1], min = t_e[2], sec = date.sec }) if time_e > time_s then --正常时间结束大于开始 if timestamp >= time_s and timestamp < time_e then if not found then job = obj found = true found_index = true else conflict_index = true end end elseif time_e < time_s then --不做任何处理 if timestamp >= time_s and timestamp < (time_e + 24*3600) then if not found then job = obj found = true found_index = true else conflict_index = true end end end log.info("Monitor",string.format("%s %d\t%02d:%02d<->%02d:%02d\t%s\t%s\t [%s]", found_index and "->" or " ", index, t_s[1] and t_s[1] or 0 , t_s[2] and t_s[2] or 0 , t_e[1] and t_e[1] or 0 , t_e[2] and t_e[2] or 0 , mode and mode or "XXX", power and power or "XXXX", conflict_index and "*" or "-" )) end return job,date end local function schedule_service() while not initalized do sys.wait(1000) end while true do sys.wait(10000) local time = os.time()+TIMEZONE_OFFSET local currweek = os.date("%a",time) -- 获取周 local currtime = os.date("%H:%M:%S",time) -- 获取周 log.info("Monitor",string.format("Week:%s Time:%s TOU Service:%s",currweek,currtime,off_service and "No Run" or "Running")) local currjobs = local_job[currweek] if currjobs and #currjobs and not off_service then local currjob = get_cerrjob(currjobs) -- TODO 检查并使其生效 if currjob then local mode = currjob[3] local power = currjob[4] local cmd = singal_pase_modes[mode] log.info("Control",string.format("Mode %x(%s) Power %s",cmd and cmd or 0,mode,power and power or 0)) internal_api.call_dev_service(gw_sn,pcs_sn,pcs_port,"DisCharge_Charge_Set",{mode=cmd and cmd or 0,powerlimit=power and power or 0}) end end end end local function schedule_load() while not initalized do sys.wait(1000) end local last_time = 0 while true do sys.wait(1000) 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 body={} local name = schedule_of_used() -- 当前使用的策列 for i,scheFile in pairs(scheFiles) do -- 可选的几个调度器 local _,tmp = schedule_body(scheFile[1],scheFile[2],name) if tmp then body = tmp break end end log.info("sche_config",cjson.encode(body)) if body and body.ScheduleItems and #body.ScheduleItems > 0 then local job = {Sun={},Mon={},Tue={},Wed={},Thu={},Fri={},Sat={}} for _,item in ipairs(body.ScheduleItems) do if item then local mode,power if item.Mode then mode = item.Mode elseif item.Signal then mode = signal_modes[string.format("S%d",item.Signal)] else mode = "STANBY" log.warn("sche_config","Schedule update failed","Must need run Mode or Signal") end if item.PowerRate then power = item.PowerRate end if item.StartDate and item.EndDate then local ret_s,week_s,hour_s,min_s = spilt_day(item.StartDate) local ret_e,week_e,hour_e,min_e = spilt_day(item.EndDate) if ret_s and ret_e and week_s == week_e then if not job[week_s] then log.warn("sche_config","Week's format failed",week_s) break end table.insert(job[week_s],{{hour_s,min_s},{hour_e,min_e},mode,power}) else log.warn("sche_config","spilt_day faile, date string",item.StartDate,item.StartDate) end end else log.warn("sche_config","Paramter failed",item and item.PowerRate , item and item.StartDate , item and item.EndDate) end end for week,obj in pairs(job) do -- 更新全局job if local_job[week] then local_job[week] = obj log.info("sche_config",week,cjson.encode(obj)) else obj = {} end end log.info("sche_config","Paramter update sucesseed") end end end end local function collect_realdata() while not initalized do sys.wait(1000) end local last_time = 0 local report_time = 0 while true do sys.wait(1000) 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() internal_api.service_response(gw_sn,emu_sn,emu_port,"post",ifu_obj,0) end end end local function get_node_cfg(nodes_cfg) local node_cfg if nodes_cfg then node_cfg = cjson.decode(nodes_cfg) else local file = io.readFile("/app/node/nodes_cfg.json") if file then local file_obj = cjson.decode(file) node_cfg = {} local found = false for i,obj in ipairs(file_obj.nodes_cfg) do if obj.sn == emu_sn then table.insert(node_cfg,obj) found = true break end end if not found then node_cfg = nil end end end return node_cfg end -- 扩展字段使用例子 --[[ { "initParam":[ {"reg":"0x561","value":5}, {"reg":"0x568","value":1}, {"reg":"0x599","value":20}, {"reg":"0x7A2","value":1}, {"reg":"1441","value":20} ], "offset_TZ":28800 } ]] local function load_node_cfg(nodes_cfg) local node_cfg = get_node_cfg(nodes_cfg) if node_cfg and #node_cfg > 0 then local extra_str = node_cfg[1].ext_data if extra_str then extra_params=cjson.decode(extra_str) if extra_params.initParam then local param = extra_params.initParam if "table" == type(param) then for i,obj in ipairs(param) do log.info("load", string.format("reg_addr:%s(%s)=%d",tonumber(obj.reg),obj.reg,obj.value )) table.insert(inital_param_reg,{RegAddr=tonumber(obj.reg),RegValue=obj.value}) end end end if extra_params.offset_TZ and "number" == type(extra_params.offset_TZ) then TIMEZONE_OFFSET = extra_params.offset_TZ log.info("load","TIMEZONE_OFFSET:",TIMEZONE_OFFSET) else log.warn("load","Using default Timezone:UTC+8") end end end end function main(nodes_cfg,sn) log.info("calculate", "=============== EMU FOR HOME 2.1 ============") local function handler() local pid, status, code = syswait.wait(-1, syswait.WNOHANG) os.exit(0) end signal.signal(signal.SIGKILL,handler) signal.signal(signal.SIGHUP,handler) gw_sn = sn if not gw_sn then gw_sn = io.getGatewayID() end 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) load_node_cfg(nodes_cfg) local subscribe={"ipc/IFU/BatterySchedule/#","ipc/+/+/Notify_Upgrade"} local client = internal_api.local_mqtt_session({pcs_sn,bms_sn,emu_sn},"APP",iot2app,pp2app,subscribe,others_cb) -- PCS 参数 sys.taskInit(function() while true do sys.wait(1000) if client:socket() then break end end while true do sys.wait(1000) local obj = table.remove(inital_param_reg,1) if not obj then break end log.info("init", string.format("reg_addr:%s=%d",obj.RegAddr,obj.RegValue )) internal_api.call_dev_service(gw_sn,pcs_sn,pcs_port,"WriteRegsiterOfModbus",obj) end initalized = true end) -- 加载调度器 sys.taskInit(schedule_load) -- 执行调度器 sys.taskInit(schedule_service) -- real data process sys.taskInit(collect_realdata) -- Report history service data sys.taskInit(history_service) --启动系统框架 sys.init(0, 0) sys.run() end main(arg[1],arg[2])