From a83f5a4ce0f07fac6478f4808bfc14a7f33641fe Mon Sep 17 00:00:00 2001 From: britkat1980 <69121158+britkat1980@users.noreply.github.com> Date: Fri, 3 Nov 2023 21:42:34 +0000 Subject: [PATCH] prep for 3.0.0 --- .dayRate | 0 .gitignore | 2 +- Dockerfile | 25 +- GivEnergy-Smart-Home-Display-givtcp/js/app.js | 6 +- GivTCP/GivLUT.py | 2 +- GivTCP/HA_Discovery.py | 18 +- GivTCP/REST.py | 44 ++- GivTCP/evc.py | 49 ++- GivTCP/palm_settings.py | 2 +- GivTCP/palm_utils.py | 32 +- GivTCP/read.py | 4 +- GivTCP/write.py | 1 + README.md | 10 +- battery control dashboard raw config.txt | 312 ------------------ buildx.bat | 2 +- givenergy_modbus/client.py | 23 +- givenergy_modbus/model/battery.py | 91 +++++ givtcp-vuejs/package.json | 2 +- givtcp-vuejs/src/components/FormCard.vue | 17 + givtcp-vuejs/src/components/StepButton.vue | 34 +- givtcp-vuejs/src/stores/counter.js | 34 ++ index.html | 1 + ingress.conf | 28 ++ startup.py | 2 + startup_3.py | 144 +++++--- updatelib.bat | 1 - 26 files changed, 438 insertions(+), 448 deletions(-) delete mode 100644 .dayRate delete mode 100644 battery control dashboard raw config.txt create mode 100644 index.html create mode 100644 ingress.conf delete mode 100644 updatelib.bat diff --git a/.dayRate b/.dayRate deleted file mode 100644 index e69de29b..00000000 diff --git a/.gitignore b/.gitignore index 062fe681..dc315b39 100644 --- a/.gitignore +++ b/.gitignore @@ -21,8 +21,8 @@ GivTCP/testdata.json .gitattributes GivTCP/settings.py MQTT_TEST.py -REST_TEST.py test.py +*.pkl settings.py __pycache__ givenergy_modbus/model/__pycache__/inverter.cpython-310.pyc diff --git a/Dockerfile b/Dockerfile index 9cd499b2..27ebc66a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # set base image (host OS) FROM python:rc-alpine -RUN apk --no-cache add mosquitto +RUN apk add mosquitto RUN apk add curl RUN apk add --update npm RUN npm install -g serve @@ -11,31 +11,34 @@ RUN apk add musl-utils RUN apk add xsel RUN apk add redis RUN apk add npm - +RUN apk add nginx && mkdir -p /run/nginx # set the working directory in the container WORKDIR /app # copy the dependencies file to the working directory COPY requirements.txt . - -# install dependencies RUN pip install -r requirements.txt +COPY givtcp-vuejs/package.json ./config_frontend/package.json + +RUN cd /app/config_frontend && npm install +COPY givtcp-vuejs ./config_frontend +RUN cd /app/config_frontend && npm run build + +COPY ingress.conf /etc/nginx/http.d/ +RUN rm /etc/nginx/http.d/default.conf + # copy the content of the local src directory to the working directory COPY GivTCP/ ./GivTCP COPY GivEnergy-Smart-Home-Display-givtcp/ ./GivEnergy-Smart-Home-Display-givtcp COPY givenergy_modbus/ /usr/local/lib/python3.10/site-packages/givenergy_modbus COPY startup.py startup.py -#COPY startup_3.py startup_3.py +COPY startup_3.py startup_3.py COPY redis.conf redis.conf COPY settings.json /app/settings.json - -#COPY givtcp-vuejs/package.json ./config_frontend/package.json -#RUN cd /app/config_frontend && npm install -#COPY givtcp-vuejs ./config_frontend -#RUN cd /app/config_frontend && npm run build +COPY index.html /app/index.html ENV NUMINVERTORS=1 ENV INVERTOR_IP_1="" @@ -106,6 +109,6 @@ ENV EVC_IP_ADDRESS="" ENV EVC_SELF_RUN_TIMER=5 -EXPOSE 6345 1883 3000 5173 6379 9181 +EXPOSE 1883 6379 8099 CMD ["python3", "/app/startup.py"] diff --git a/GivEnergy-Smart-Home-Display-givtcp/js/app.js b/GivEnergy-Smart-Home-Display-givtcp/js/app.js index fd5e3b68..14ab5c2a 100644 --- a/GivEnergy-Smart-Home-Display-givtcp/js/app.js +++ b/GivEnergy-Smart-Home-Display-givtcp/js/app.js @@ -11,14 +11,14 @@ class App { me.givTcpHostname = null; me.solarRate = null; me.exportRate = null; - + var host = window.location.protocol + "//" + window.location.hostname+":6345" // Fetch the settings from `app.json` fetch("./app.json") .then(response => { return response.json(); }) .then(data => { - me.givTcpHostname = data.givTcpHostname; + me.givTcpHostname = host; me.solarRate = data.solarRate; me.exportRate = data.exportRate; @@ -47,7 +47,7 @@ class App { fetchData() { const me = this; - fetch(`http://${me.givTcpHostname}/readData`, { + fetch(`${me.givTcpHostname}/readData`, { mode: 'cors', headers: { 'Access-Control-Allow-Origin': 'localhost:63342' diff --git a/GivTCP/GivLUT.py b/GivTCP/GivLUT.py index 7d8fc7bf..5cf2c920 100644 --- a/GivTCP/GivLUT.py +++ b/GivTCP/GivLUT.py @@ -326,7 +326,7 @@ class GivLUT: "Charge_End_Time":GEType("sensor","timestamp","","","",False,False,False), "System_Time":GEType("sensor","timestamp","","","",False,False,False), "Charging_Mode":GEType("select","","setChargingMode","","",False,False,False), - "Import_Cap":GEType("switch","","setImportCap","","",False,False,False), + "Import_Cap":GEType("number","","setImportCap","","",False,False,False), "Max_Session_Energy":GEType("number","","setMaxSessionEnergy","","",False,False,False), } time_slots=[ diff --git a/GivTCP/HA_Discovery.py b/GivTCP/HA_Discovery.py index cb5a3256..6444f8c8 100644 --- a/GivTCP/HA_Discovery.py +++ b/GivTCP/HA_Discovery.py @@ -209,26 +209,32 @@ def create_device_payload(topic,SN): tempObj['options']=options elif GivLUT.entity_type[str(topic).split("/")[-1]].devType=="number": # If its a rate then change to Watts - if "SOC" in str(topic).lower(): + item=str(topic).split("/")[-1] + if "soc" in str(item).lower(): tempObj['unit_of_meas']="%" tempObj['min']=4 tempObj['max']=100 tempObj['mode']="slider" - elif "limit" in str(topic).lower(): #if EVC current + elif "limit" in str(item).lower(): #if EVC current tempObj['unit_of_meas']="A" tempObj['min']=6 tempObj['max']=32 tempObj['mode']="slider" - elif "charge" in str(topic).lower(): - tempObj['unit_of_meas']="W" + elif "cap" in str(item).lower(): #if EVC current + tempObj['unit_of_meas']="A" tempObj['min']=0 - tempObj['max']=HAMQTT.getinvbatmax() + tempObj['max']=100 tempObj['mode']="slider" - elif "max_session_energy" in str(topic).lower(): + elif "energy" in str(item).lower(): tempObj['unit_of_meas']="kWh" tempObj['min']=0 tempObj['max']=100 tempObj['mode']="slider" + elif "charge" in str(item).lower(): + tempObj['unit_of_meas']="W" + tempObj['min']=0 + tempObj['max']=HAMQTT.getinvbatmax() + tempObj['mode']="slider" else: tempObj['unit_of_meas']="%" elif GivLUT.entity_type[str(topic).split("/")[-1]].devType=="button": diff --git a/GivTCP/REST.py b/GivTCP/REST.py index 7a84813c..055f1346 100644 --- a/GivTCP/REST.py +++ b/GivTCP/REST.py @@ -5,6 +5,7 @@ from flask_cors import CORS import read as rd #grab passthrough functions from main read file import write as wr #grab passthrough functions from main write file +import evc as evc import config_dash as cfdash from GivLUT import GivQueue, GivLUT import os @@ -21,11 +22,11 @@ @giv_api.route('/', methods=['GET', 'POST']) def root(): - return send_from_directory('/app/config_frontend/dist/', 'index.html') + return send_from_directory('/app/config_frontend/dist', 'index.html') @giv_api.route('/config') def get_config_page(): - return send_from_directory('/app/config_frontend/dist/', 'index.html') + return send_from_directory('/app/config_frontend/dist', 'index.html') # if request.method=="GET": # return cfdash.get_config() # if request.method=="POST": @@ -42,7 +43,7 @@ def reboot(): @giv_api.route('/restart', methods=['GET']) def restart(): - return wr.rebootAddon + return wr.rebootAddon() #Publish last cached Invertor Data @giv_api.route('/readData', methods=['GET']) @@ -209,5 +210,42 @@ def editFileData(): file.close() return data +@giv_api.route('/setImportCap', methods=['POST']) +def impCap(): + payload = request.get_json(silent=True, force=True) + return evc.setImportCap(payload) + +@giv_api.route('/setCurrentLimit', methods=['POST']) +def currLimit(): + payload = request.get_json(silent=True, force=True) + return evc.setCurrentLimit(payload) + +@giv_api.route('/setChargeControl', methods=['POST']) +def chrgeControl(): + payload = request.get_json(silent=True, force=True) + return evc.setChargeControl(payload) + +@giv_api.route('/setChargeMode', methods=['POST']) +def chrgMode(): + payload = request.get_json(silent=True, force=True) + return evc.setChargeMode(payload) + +@giv_api.route('/setChargingMode', methods=['POST']) +def chrgingMode(): + payload = request.get_json(silent=True, force=True) + return evc.setChargingMode(payload) + +@giv_api.route('/setMaxSessionEnergy', methods=['POST']) +def maxSession(): + payload = request.get_json(silent=True, force=True) + return evc.setMaxSessionEnergy(payload) + +@giv_api.route('/getEVCCache', methods=['GET']) +def gtEVCChce(): + payload = request.get_json(silent=True, force=True) + return evc.getEVCCache() + + + if __name__ == "__main__": giv_api.run() diff --git a/GivTCP/evc.py b/GivTCP/evc.py index 1f5d2bb1..9366b98e 100644 --- a/GivTCP/evc.py +++ b/GivTCP/evc.py @@ -118,8 +118,9 @@ def getEVC(): with open(EVCLut.regcache, 'rb') as inp: evcRegCache= pickle.load(inp) - if output['Charge_Session_Energy']==0: + if output['Charge_Session_Energy']==0 and not output['Charging_State']=='Charging': #If charging has finished, then hold the previous charge session energy output['Charge_Session_Energy']=evcRegCache['Charger']['Charge_Session_Energy'] + startTime=datetime.datetime.now().replace(hour=regs[74],minute=regs[75],second=regs[76],microsecond=0,tzinfo=datetime.timezone.utc).isoformat() endtime=datetime.datetime.now().replace(hour=regs[82],minute=regs[83],second=regs[84],microsecond=0,tzinfo=datetime.timezone.utc).isoformat() @@ -134,7 +135,7 @@ def getEVC(): output['Charge_End_Time']=datetime.datetime.now().replace(hour=regs[82],minute=regs[83],second=regs[84],microsecond=0,tzinfo=datetime.timezone.utc).isoformat() if not "Import_Cap" in evcRegCache['Charger']: - output['Import_Cap']="disable" + output['Import_Cap']=0 else: output['Import_Cap']=evcRegCache['Charger']['Import_Cap'] @@ -142,6 +143,7 @@ def getEVC(): output['Charging_Mode']="Grid" else: output['Charging_Mode']=evcRegCache['Charger']['Charging_Mode'] + if not 'Max_Session_Energy' in evcRegCache['Charger']: output['Max_Session_Energy']=0 else: @@ -153,7 +155,7 @@ def getEVC(): output['Charge_End_Time']=ts.replace(tzinfo=datetime.timezone.utc).isoformat() ts=datetime.datetime.now().replace(hour=regs[74],minute=regs[75],second=regs[76],microsecond=0) output['Charge_Start_Time']=ts.replace(tzinfo=datetime.timezone.utc).isoformat() - output['Import_Cap']="disable" + output['Import_Cap']=0 output['Charging_Mode']="Grid" output['Max_Session_Energy']=0 if regs[0]==4: @@ -171,6 +173,8 @@ def getEVC(): output['Charge_Session_Duration']=str(td) multi_output['Charger']=output # Save new data to Pickle + + with cacheLock: with open(EVCLut.regcache, 'wb') as outp: pickle.dump(multi_output, outp, pickle.HIGHEST_PROTOCOL) @@ -248,7 +252,7 @@ def updateFirstRun(SN): logger.error("Could not access settings file to update EVC Serial Number") break else: - logger.debug("Settings availble evc") + logger.debug("Settings available evc") #Create setting lockfile open(".settings_lockfile",'a').close() @@ -302,6 +306,14 @@ def iterate_dict(array): # Create a publish safe version of the output (c safeoutput[p_load] = output return(safeoutput) +def getEVCCache(): + if exists(EVCLut.regcache): + with open(EVCLut.regcache, 'rb') as inp: + evcRegCache= pickle.load(inp) + return json.dumps(evcRegCache) + else: + return json.dumps("No EVC data found") + def setChargeMode(mode): if mode=="enable": val=0 @@ -311,7 +323,7 @@ def setChargeMode(mode): logger.error("Invalid control mode called: "+str(mode)) return logger.info("Setting Charge mode to: "+ mode) - logger.info("numeric value "+str(val)+ " sent to EVC") + logger.debug("numeric value "+str(val)+ " sent to EVC") try: client=ModbusTcpClient(GiV_Settings.evc_ip_address) client.write_registers(93,val) @@ -323,7 +335,7 @@ def setChargeControl(mode): if mode in GivLUT.charge_control: logger.info("Setting Charge control to: "+ mode) val=GivLUT.charge_control.index(mode) - logger.info("numeric value "+str(val)+ " sent to EVC") + logger.debug("numeric value "+str(val)+ " sent to EVC") try: client=ModbusTcpClient(GiV_Settings.evc_ip_address) client.write_registers(95,val) @@ -357,19 +369,36 @@ def test(): #getEVC() print (result) -def chargeMode(): +def chargeMode(once=False): while True: #Run a regular check and manage load based on current mode and session energy if exists(EVCLut.regcache): with open(EVCLut.regcache, 'rb') as inp: - evcRegCache= pickle.load(inp) + evcRegCache= pickle.load(inp) if evcRegCache['Charger']['Charging_State']=="Charging" or evcRegCache['Charger']['Charging_State']=="Connected": if evcRegCache['Charger']['Charging_Mode']=="Hybrid": hybridmode() elif evcRegCache['Charger']['Charging_Mode']=="Solar": solarmode() - if not evcRegCache['Charger']['Max_Session_Energy']==0 and evcRegCache['Charger']['Charge_Session_Energy']>evcRegCache['Charger']['Max_Session_Energy'] and evcRegCache['Charger']['Charge_Control']=="Start": + if not evcRegCache['Charger']['Max_Session_Energy']==0 and evcRegCache['Charger']['Charge_Session_Energy']>=evcRegCache['Charger']['Max_Session_Energy'] and evcRegCache['Charger']['Charge_Control']=="Start": + logger.info("Session energy limit reached: "+str(evcRegCache['Charger']['Charge_Session_Energy'])+"kWh stopping charge") setChargeControl("Stop") + if not int(evcRegCache['Charger']['Import_Cap'])==0: + if exists(GivLUT.regcache): + with open(GivLUT.regcache, 'rb') as inp: + invRegCache= pickle.load(inp) + if float(invRegCache[4]['Power']['Power']['Grid_Current'])>(float(evcRegCache['Charger']['Import_Cap'])*0.95): + target=float(evcRegCache['Charger']['Import_Cap'])*0.9 + reduction=(float(invRegCache[4]['Power']['Power']['Grid_Current']))-target + newlimit=int(float(evcRegCache['Charger']['Charge_Limit'])-reduction) + if not int(evcRegCache['Charger']['Charge_Limit'])==4: + logger.info("Grid import threshold within 5%, reducing EVC charge current to: "+str(newlimit)) + setCurrentLimit(newlimit) + else: + logger.info("Grid import threshold within 5%, cannot reduce Charge limit below 6A. Stopping Charge") + setChargeControl("Stop") + if once: + break time.sleep(60) @@ -456,7 +485,7 @@ def setChargingMode(mode): with cacheLock: with open(EVCLut.regcache, 'wb') as outp: pickle.dump(evcRegCache, outp, pickle.HIGHEST_PROTOCOL) - chargeMode() # Run an initial call when changing modes + chargeMode(True) # Run an initial call when changing modes else: logger.error("Invalid selection for Charge Mode ("+str(mode)+")") diff --git a/GivTCP/palm_settings.py b/GivTCP/palm_settings.py index 96bdbe7b..f32c1bb0 100644 --- a/GivTCP/palm_settings.py +++ b/GivTCP/palm_settings.py @@ -28,7 +28,7 @@ class GE: # Modify url with system name in place of CExxxxxx and paste API key generated on GivEnergy web portal in place of xxxx url = "https://api.givenergy.cloud/v1/inverter/"+GiV_Settings.serial_number+"/" # key = str(os.getenv('GEAPI')) - key = str(GiV_Settings.GEAPI) + key = str(GiV_Settings.GE_API) # Most users will not need to touch that many of the pre-configured settings below diff --git a/GivTCP/palm_utils.py b/GivTCP/palm_utils.py index 108de077..a587e2eb 100644 --- a/GivTCP/palm_utils.py +++ b/GivTCP/palm_utils.py @@ -227,7 +227,7 @@ def get_load_hist_day(offset: int): i: int = 0 while i < len(stgs.GE.load_hist_weight): if stgs.GE.load_hist_weight[i] > 0: - logger.info("Processing load history for day -"+ str(i + 1)) + logger.debug("Processing load history for day -"+ str(i + 1)) load_hist_array = get_load_hist_day(i) j = 0 while j < 48: @@ -237,7 +237,7 @@ def get_load_hist_day(offset: int): total_weight += stgs.GE.load_hist_weight[i] logger.debug(str(acc_load)+ " total weight: "+ str(total_weight)) else: - logger.info("Skipping load history for day -"+ str(i + 1)+ " (weight <= 0)") + logger.debug("Skipping load history for day -"+ str(i + 1)+ " (weight <= 0)") i += 1 # Avoid DIV/0 if config file contains incorrect weightings @@ -250,7 +250,7 @@ def get_load_hist_day(offset: int): while i < 48: self.base_load[i] = round(acc_load[i]/total_weight, 1) i += 1 - logger.info("Load Calc Summary: "+ str(self.base_load)) + logger.debug("Load Calc Summary: "+ str(self.base_load)) def set_mode(self, cmd: str): """Configures inverter operating mode""" @@ -289,10 +289,10 @@ def set_inverter_register(register: str, value: str): logger.error(error) return if resp.status_code != 201: - logger.info("Invalid response: "+ str(resp.status_code)) + logger.debug("Invalid response: "+ str(resp.status_code)) return - logger.info("Setting Register "+ str(register)+ " ("+ str(cmd_name) + ") to "+ + logger.debug("Setting Register "+ str(register)+ " ("+ str(cmd_name) + ") to "+ str(value)+ " Response: "+ str(resp)) time.sleep(3) # Allow data on GE server to settle @@ -318,7 +318,7 @@ def set_inverter_register(register: str, value: str): returned_cmd = json.loads(resp.content.decode('utf-8'))['data']['value'] if str(returned_cmd) == str(value): - logger.info("Successful register read: "+ str(register)+ " = "+ str(returned_cmd)) + logger.debug("Successful register read: "+ str(register)+ " = "+ str(returned_cmd)) else: logger.error("Readback failed on GivEnergy API... Expected " + str(value) + ", Read: "+ str(returned_cmd)) @@ -396,8 +396,8 @@ def compute_tgt_soc(self, gen_fcast, weight: int, commit: bool) -> str: wgt_50 = weight - 10 wgt_90 = max(0, weight - 50) - logger.info("") - logger.info("{:<20} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}".format("SoC Calc;", + logger.debug("") + logger.debug("{:<20} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}".format("SoC Calc;", "Day", "Hour", "Charge", "Cons", "Gen", "SoC")) # Definitions for export of SoC forecast in chart form @@ -450,7 +450,7 @@ def compute_tgt_soc(self, gen_fcast, weight: int, commit: bool) -> str: elif i > end_charge_period: # Charging after overnight boost max_charge = max(max_charge, batt_charge[i]) - logger.info("{:<20} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}".format("SoC Calc;", + logger.debug("{:<20} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10}".format("SoC Calc;", day, t_to_hrs(i * 30), round(batt_charge[i], 2), round(total_load, 2), round(est_gen, 2), int(100 * batt_charge[i] / batt_max_charge))) @@ -580,15 +580,15 @@ def get_solcast(url) -> Tuple[bool, str]: return if stgs.Solcast.url_sw != "": # Two arrays are specified - logger.info("url_sw = '"+str(stgs.Solcast.url_sw)+"'") + logger.debug("url_sw = '"+str(stgs.Solcast.url_sw)+"'") result, solcast_data_2 = get_solcast(stgs.Solcast.url_sw) if not result: logger.warning("Error; Problem with Solcast data, using previous values (if any)") return else: - logger.info("No second array") + logger.debug("No second array") - logger.info("Successful Solcast download.") + logger.debug("Successful Solcast download.") # Combine forecast for PV arrays & align data with day boundaries pv_est10 = [0] * 10080 @@ -604,7 +604,7 @@ def get_solcast(url) -> Tuple[bool, str]: # Check for BST and convert to local time to align with GivEnergy data if time.strftime("%z", time.localtime()) == "+0100": - logger.info("Applying BST offset to Solcast data") + logger.debug("Applying BST offset to Solcast data") solcast_offset += 60 i = solcast_offset @@ -664,11 +664,11 @@ def get_solcast(url) -> Tuple[bool, str]: i += 1 timestamp = time.strftime("%d-%m-%Y %H:%M:%S", time.localtime()) - logger.info("PV Estimate 10% (hrly, 7 days) / kWh; "+ timestamp+ "; "+ + logger.debug("PV Estimate 10% (hrly, 7 days) / kWh; "+ timestamp+ "; "+ str(self.pv_est10_30[0:47])+ str(self.pv_est10_day[0:6])) - logger.info("PV Estimate 50% (hrly, 7 days) / kWh; "+ timestamp+ "; "+ + logger.debug("PV Estimate 50% (hrly, 7 days) / kWh; "+ timestamp+ "; "+ str(self.pv_est50_30[0:47])+ str(self.pv_est50_day[0:6])) - logger.info("PV Estimate 90% (hrly, 7 days) / kWh; "+ timestamp+ "; "+ + logger.debug("PV Estimate 90% (hrly, 7 days) / kWh; "+ timestamp+ "; "+ str(self.pv_est90_30[0:47])+ str(self.pv_est90_day[0:6])) # End of SolcastObj() class definition diff --git a/GivTCP/read.py b/GivTCP/read.py index 3ac2033b..5276aa09 100644 --- a/GivTCP/read.py +++ b/GivTCP/read.py @@ -174,7 +174,7 @@ def getData(fullrefresh): # Read from Inverter put in cache power_output['PV_Current_String_1'] = GEInv.i_pv1*10 power_output['PV_Current_String_2'] = GEInv.i_pv2*10 power_output['Grid_Voltage'] = GEInv.v_ac1 - power_output['Grid_Current'] = GEInv.i_ac1*10 + power_output['Grid_Current'] = GEInv.i_grid_port # Grid Power logger.debug("Getting Grid Power") @@ -504,7 +504,7 @@ def getData(fullrefresh): # Read from Inverter put in cache timeslots['Charge_start_time_slot_1'] = GEInv.charge_slot_1[0].isoformat() timeslots['Charge_end_time_slot_1'] = GEInv.charge_slot_1[1].isoformat() try: - if inverterModel.generation == "Gen 2" or inverterModel.generation == "Gen 3": + if inverterModel.model == "AIO" or (inverterModel.generation == "Gen 3" and int(GEInv.arm_firmware_version)>302): #10 slots only apply to AIO and new fw on Gen 3 timeslots['Charge_start_time_slot_2'] = GEInv.charge_slot_2[0].isoformat() timeslots['Charge_end_time_slot_2'] = GEInv.charge_slot_2[1].isoformat() timeslots['Charge_start_time_slot_3'] = GEInv.charge_slot_3[0].isoformat() diff --git a/GivTCP/write.py b/GivTCP/write.py index a983a651..9870ecf3 100644 --- a/GivTCP/write.py +++ b/GivTCP/write.py @@ -1167,6 +1167,7 @@ def rebootAddon(): result = requests.post(url, headers={'Content-Type':'application/json', 'Authorization': 'Bearer {}'.format(access_token)}) + return json.dumps(result) def getSavedBatteryReservePercentage(): saved_battery_reserve=4 diff --git a/README.md b/README.md index d1a70e50..40ab4dea 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,11 @@ GivTCP collects all inverter and battery data and creates a nested data structur ## GivEnergy Electric Vehicle Charger -From version 2.4 onwards GivTCP incorporates control and monitoring of the GE charger. Connecting via local modbus it can monitor real-time stats and provide simple control features. +From version 2.4 onwards GivTCP incorporates control and monitoring of the GE charger. Connecting via local modbus it can monitor real-time stats and provide simple control features. With the EVC cloud conrol does not use the modbus protocol, so there is minimal opportunity for changes to reflect in the official app/cloud portal. Particularly the Charging modes, which are merely mimiced in GivEVC ## Configuration -All that is required for config are the IP address and the self run timer. Setting EVC_EABLE to True will turn on the function. +All that is required for config are the IP address and the self run timer. Setting EVC_ENABLE to True will turn on the function. ## Control @@ -98,3 +98,9 @@ Modulates the Charge Limit based on the amount of "excess solar" available after #### Hybrid This will modulate Charge Limit to top up a base 6A grid charge with any excess solar energy. Similar to Solar but uses a constant 6A from Grid plus additional solar energy on top. + +### Max Session Energy +This will cap the maximum energy delivered to the vehicle in a single charge session. Setting this to 0kWh disables this setting. + +### Import Cap +This will monitor the Grid current from the first GivTCP inverter and if it is within 5% of the Import Cap setting it will reduce EVC Charge current to stay 10% under the import Cap. Setting this to 0A disables this setting. diff --git a/battery control dashboard raw config.txt b/battery control dashboard raw config.txt deleted file mode 100644 index 3d18b15e..00000000 --- a/battery control dashboard raw config.txt +++ /dev/null @@ -1,312 +0,0 @@ -title: Home -views: - - title: Energy Now - path: energy-now - badges: [] - cards: - - type: custom:apexcharts-card - header: - title: Load Usage - show_states: true - colorize_states: true - apex_config: - plotOptions: - pie: - donut: - labels: - total: - show: true - chart_type: donut - series: - - entity: sensor.other_power - - entity: sensor.tumble_dryer_electric_consumption_w - - entity: sensor.av_rack_electric_consumption_w - - entity: sensor.boiling_tap_electric_consumption_w - - entity: sensor.car_charge_power - - entity: sensor.server_rack_electric_consumption_w_2 - - entity: sensor.lights_power - - entity: sensor.eden_fan_energy_power_2 - - entity: sensor.hot_tub_power - - entity: sensor.tsmart_power_w - - type: custom:tesla-style-solar-power-card - name: My Flows - grid_consumption_entity: sensor.givtcp_XX1234X567_import_power - house_consumption_entity: sensor.givtcp_XX1234X567_load_power - generation_yield_entity: sensor.givtcp_XX1234X567_pv_power - battery_consumption_entity: sensor.givtcp_XX1234X567_discharge_power - grid_to_house_entity: sensor.givtcp_XX1234X567_grid_to_house - grid_to_battery_entity: sensor.givtcp_XX1234X567_grid_to_battery - generation_to_grid_entity: sensor.givtcp_XX1234X567_solar_to_grid - generation_to_battery_entity: sensor.givtcp_XX1234X567_solar_to_battery - generation_to_house_entity: sensor.givtcp_XX1234X567_solar_to_house - battery_to_house_entity: sensor.givtcp_XX1234X567_battery_to_house - battery_to_grid_entity: sensor.givtcp_XX1234X567_battery_to_grid - battery_extra_entity: sensor.givtcp_XX1234X567_soc - appliance1_consumption_entity: sensor.car_charge_power - appliance1_extra_entity: sensor.etron_state_of_charge - grid_entity: sensor.givtcp_XX1234X567_grid_power - house_entity: sensor.givtcp_XX1234X567_load_power - generation_entity: sensor.givtcp_XX1234X567_pv_power - battery_entity: sensor.givtcp_XX1234X567_battery_power - show_w_not_kw: 1 - hide_inactive_lines: 1 - change_house_bubble_color_with_flow: 1 - - type: gauge - entity: sensor.electric_cost_today - min: 0 - max: 20 - needle: false - - type: gauge - entity: sensor.gas_cost_today - min: 0 - max: 20 - - type: entities - entities: - - entity: sensor.givtcp_XX1234X567_night_cost - - entity: sensor.givtcp_XX1234X567_night_energy_kwh - - entity: sensor.givtcp_XX1234X567_day_cost - - entity: sensor.givtcp_XX1234X567_day_energy_kwh - - entity: sensor.givtcp_XX1234X567_import_ppkwh_today - - type: custom:apexcharts-card - header: - title: Load Usage - show_states: true - colorize_states: true - apex_config: - plotOptions: - pie: - donut: - labels: - total: - show: true - chart_type: donut - series: - - entity: sensor.givtcp_XX1234X567_day_energy_kwh - - entity: sensor.givtcp_XX1234X567_night_energy_kwh - - entity: sensor.givtcp_XX1234X567_pv_energy_today_kwh - - type: history-graph - entities: - - entity: sensor.givtcp_XX1234X567_battery_value - - entity: sensor.givtcp_XX1234X567_battery_ppkwh - hours_to_show: 24 - - panel: false - path: energy - title: Energy - badges: [] - cards: - - type: history-graph - entities: - - entity: sensor.givtcp_XX1234X567_battery_power - - entity: sensor.givtcp_XX1234X567_load_power - - entity: sensor.givtcp_XX1234X567_grid_power - - entity: sensor.givtcp_XX1234X567_pv_power - hours_to_show: 24 - refresh_interval: 0 - title: Energy Usage - - type: history-graph - entities: - - entity: sensor.givtcp_XX1234X567_soc - - entity: number.givtcp_XX1234X567_target_soc - hours_to_show: 24 - refresh_interval: 0 - title: Battery State - - type: custom:apexcharts-card - header: - show: true - title: Import/Export - show_states: true - colorize_states: true - graph_span: 24h - span: - end: hour - experimental: - color_threshold: true - series: - - entity: sensor.givtcp_XX1234X567_grid_power - type: column - group_by: - func: avg - duration: 30min - color_threshold: - - value: -2 - color: red - opacity: 1 - - value: 2 - color: green - - type: custom:apexcharts-card - header: - title: Load Usage - show_states: true - colorize_states: true - graph_span: 24h - span: - end: hour - apex_config: - chart: - stacked: 'true' - series: - - entity: sensor.givtcp_XX1234X567_grid_to_battery - type: column - color: blue - group_by: - func: avg - duration: 30min - - entity: sensor.lights_power - type: column - color: yellow - group_by: - func: avg - duration: 30min - - entity: sensor.server_rack_electric_consumption_w_2 - type: column - group_by: - func: avg - duration: 30min - - entity: sensor.tumble_dryer_electric_consumption_w - type: column - group_by: - func: avg - duration: 30min - - entity: sensor.av_rack_electric_consumption_w - type: column - group_by: - func: avg - duration: 30min - - entity: sensor.boiling_tap_power_stable - type: column - group_by: - func: avg - duration: 30min - - entity: sensor.car_charge_power - type: column - group_by: - func: avg - duration: 30min - - entity: sensor.hot_tub_power - type: column - color: brown - group_by: - func: avg - duration: 30min - - entity: sensor.other_power - type: column - color: grey - group_by: - func: avg - duration: 30min - - entity: sensor.tsmart_power_w - type: column - color: orange - group_by: - func: avg - duration: 30min - - type: history-graph - entities: - - entity: sensor.energy_production_tomorrow - - entity: sensor.givtcp_XX1234X567_pv_energy_today_kwh - refresh_interval: 0 - title: Forecast vs Actual PV - hours_to_show: 72 - - type: history-graph - entities: - - entity: sensor.givtcp_XX1234X567_pv_power - - entity: sensor.givtcp_XX1234X567_pv_power_string_1 - - entity: sensor.givtcp_XX1234X567_pv_power_string_2 - hours_to_show: 12 - - type: custom:apexcharts-card - header: - show: true - title: Daily cost - show_states: true - colorize_states: true - graph_span: 28d - span: - end: day - apex_config: - chart: - stacked: 'true' - series: - - entity: sensor.givtcp_XX1234X567_day_cost - type: column - color: red - group_by: - func: max - duration: 24h - - entity: sensor.givtcp_XX1234X567_night_cost - type: column - color: orange - group_by: - func: max - duration: 24h - - type: custom:apexcharts-card - header: - show: true - title: Daily Load - show_states: true - colorize_states: true - graph_span: 28d - span: - end: hour - apex_config: - chart: - stacked: 'true' - series: - - entity: sensor.givtcp_XX1234X567_day_energy_kwh - type: column - color: red - group_by: - func: last - duration: 24h - - entity: sensor.givtcp_XX1234X567_night_energy_kwh - type: column - color: orange - group_by: - func: last - duration: 24h - - entity: sensor.givtcp_XX1234X567_pv_energy_today_kwh - type: column - color: green - group_by: - func: last - duration: 24h - - type: history-graph - entities: - - entity: sensor.givtcp_XX1234X567_load_energy_today_kwh - - theme: Backend-selected - title: GivEnergy Control - path: givenergy-control - badges: [] - cards: - - type: entities - entities: - - entity: select.givtcp_XX1234X567_mode - - entity: sensor.givtcp_XX1234X567_soc - - entity: number.givtcp_XX1234X567_target_soc - - entity: number.givtcp_XX1234X567_battery_charge_rate - - entity: switch.givtcp_XX1234X567_enable_charge_schedule - - entity: select.givtcp_XX1234X567_charge_start_time_slot_1 - - entity: select.givtcp_XX1234X567_charge_end_time_slot_1 - - entity: switch.givtcp_XX1234X567_enable_discharge - - entity: number.givtcp_XX1234X567_battery_discharge_rate - - entity: number.givtcp_XX1234X567_battery_power_reserve - - entity: switch.givtcp_XX1234X567_enable_discharge_schedule - - entity: select.givtcp_XX1234X567_discharge_start_time_slot_1 - - entity: select.givtcp_XX1234X567_discharge_end_time_slot_1 - - entity: select.givtcp_XX1234X567_discharge_start_time_slot_2 - - entity: select.givtcp_XX1234X567_discharge_end_time_slot_2 - title: Battery Control - show_header_toggle: false - state_color: true - header: - type: graph - entity: sensor.givtcp_XX1234X567_soc - hours_to_show: 24 - - type: entities - entities: - - entity: select.givtcp_XX1234X567_temp_pause_charge - - entity: select.givtcp_XX1234X567_temp_pause_discharge - - entity: select.givtcp_XX1234X567_force_charge - - entity: select.givtcp_XX1234X567_force_export - title: Battery Boost Control - show_header_toggle: false - state_color: true \ No newline at end of file diff --git a/buildx.bat b/buildx.bat index 5312db20..86bbaeec 100644 --- a/buildx.bat +++ b/buildx.bat @@ -1,3 +1,3 @@ -docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -t britkat/giv_tcp-dev:2.3.113 -t britkat/giv_tcp-dev:latest --push . +docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -t britkat/giv_tcp-dev:2.3.151 -t britkat/giv_tcp-dev:latest --push . ::docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -t britkat/giv_tcp-dev:latest --push . ::docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 -t britkat/giv_tcp-ma:latest -t britkat/giv_tcp-ma:2.3.3 --push . diff --git a/givenergy_modbus/client.py b/givenergy_modbus/client.py index 35cdc4a4..db8becc3 100644 --- a/givenergy_modbus/client.py +++ b/givenergy_modbus/client.py @@ -72,13 +72,22 @@ def refresh_plant(self, plant: Plant, isAIO: bool, isAC: bool, full_refresh: boo inverter_registers, plant.inverter_rc, slave_address=0x31, sleep_between_queries=sleep_between_queries ) _logger.debug("Inverter is normal so using the 0x31 slave_address") - for i, battery_rc in enumerate(plant.batteries_rcs): - self.fetch_register_pages( - {InputRegister: [60]}, - battery_rc, - slave_address=0x32 + i, - sleep_between_queries=sleep_between_queries, - ) + if isAIO: + for i, battery_rc in enumerate(plant.batteries_rcs): + self.fetch_register_pages( + {InputRegister: [60]}, + battery_rc, + slave_address=0x32 + i, + sleep_between_queries=sleep_between_queries, + ) + else: + for i, battery_rc in enumerate(plant.batteries_rcs): + self.fetch_register_pages( + {InputRegister: [60]}, + battery_rc, + slave_address=0x32 + i, + sleep_between_queries=sleep_between_queries, + ) def get_inverter_stats(self): try: diff --git a/givenergy_modbus/model/battery.py b/givenergy_modbus/model/battery.py index 0a34abf3..5bbf7c2b 100644 --- a/givenergy_modbus/model/battery.py +++ b/givenergy_modbus/model/battery.py @@ -48,3 +48,94 @@ class Battery(GivEnergyBaseModel): usb_inserted: bool e_battery_charge_total_2: float e_battery_discharge_total_2: float + +class HVBMU(GivEnergyBaseModel): + """Structured format for all inverter attributes.""" + """BMU Register structure Cell Volt and Temp""" + v_battery_cell_01: float + v_battery_cell_02: float + v_battery_cell_03: float + v_battery_cell_04: float + v_battery_cell_05: float + v_battery_cell_06: float + v_battery_cell_07: float + v_battery_cell_08: float + v_battery_cell_09: float + v_battery_cell_10: float + v_battery_cell_11: float + v_battery_cell_12: float + v_battery_cell_13: float + v_battery_cell_14: float + v_battery_cell_15: float + v_battery_cell_16: float + v_battery_cell_17: float + v_battery_cell_18: float + v_battery_cell_19: float + v_battery_cell_20: float + v_battery_cell_21: float + v_battery_cell_22: float + v_battery_cell_23: float + v_battery_cell_24: float + temp_battery_cells_1: float + temp_battery_cells_2: float + temp_battery_cells_3: float + temp_battery_cells_4: float + temp_battery_cells_5: float + temp_battery_cells_6: float + temp_battery_cells_7: float + temp_battery_cells_8: float + temp_battery_cells_9: float + temp_battery_cells_10: float + temp_battery_cells_11: float + temp_battery_cells_12: float + temp_battery_cells_13: float + temp_battery_cells_14: float + temp_battery_cells_15: float + temp_battery_cells_16: float + temp_battery_cells_17: float + temp_battery_cells_18: float + temp_battery_cells_19: float + temp_battery_cells_20: float + temp_battery_cells_21: float + temp_battery_cells_22: float + temp_battery_cells_23: float + temp_battery_cells_24: float + +class HVBCU(GivEnergyBaseModel): + pack_software_version: str + number_of_module: float + cells_per_module: float + cluster_cell_voltage: float + cluster_cell_temperature: float + status: str + battery_voltage: float + load_voltage: float + battery_current: float + battery_power: float + battery_soh: float # should this be SOC? + charge_energy_total: float + discharge_energy_total: float + charge_energy_capacity_total: float + discharge_energy_capacity_total: float + charge_energy_today: float + discharge_energy_today: float + charge_energy_capacity_today: float + discharge_energy_capacity_today: float + battery_capacity: float + number_of_cycles: float + min_discharge_voltage: float + max_discharge_voltage: float + min_discharge_current: float + max_discharge_current: float + max_cell_voltage: float + max_voltage_module: float + max_voltage_cell: float + min_cell_voltage: float + min_voltage_module: float + min_voltage_cell: float + max_cell_temperature: float + max_temperature_module: float + max_temperature_cell: float + min_cell_temperature: float + min_temperature_module: float + min_temperature_cell: float \ No newline at end of file diff --git a/givtcp-vuejs/package.json b/givtcp-vuejs/package.json index f84a3b71..391052ec 100644 --- a/givtcp-vuejs/package.json +++ b/givtcp-vuejs/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "dev": "vite --host", + "dev": "vite --host -l silent", "build": "vite build", "preview": "vite preview", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", diff --git a/givtcp-vuejs/src/components/FormCard.vue b/givtcp-vuejs/src/components/FormCard.vue index 0ec82956..7301dd2d 100644 --- a/givtcp-vuejs/src/components/FormCard.vue +++ b/givtcp-vuejs/src/components/FormCard.vue @@ -22,6 +22,15 @@ :label="input?.options?.label" color='#4fbba9' /> +