diff --git a/access.py b/access.py index e9ebfbf..d8df639 100644 --- a/access.py +++ b/access.py @@ -1,3 +1,5 @@ +"""Cloud access for weather and car data.""" + from requests import ConnectionError import requests import json @@ -95,7 +97,9 @@ def ec_GetCarData(): # noinspection PyPep8 def ec_GetPVData(url=const.C_SOLAR_URL, tout=15): """ - Get relevant data from PV inverter. API doc: https://www.solaredge.com/sites/default/files/se_monitoring_api.pdf + Get relevant data from PV inverter. + + API doc: https://www.solaredge.com/sites/default/files/se_monitoring_api.pdf :param tout: timeout for url access :param url: complete url including api key diff --git a/charger.py b/charger.py index 60844a4..56f0bfd 100644 --- a/charger.py +++ b/charger.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- +""" Basic library for accessing go-e charger API. """ -import time from json import JSONDecodeError import requests from requests import ConnectionError @@ -9,22 +9,6 @@ # API V2 see: https://github.com/goecharger/go-eCharger-API-v2 import const -def search_charger(ip_root, tout = 0.2): - retval = "-1" - command = "/api/status?filter=fna" - for i in range(1, 250): - ip = ip_root + str(i) - print(ip) - try: - response = requests.get(ip + command, timeout=(tout)) - statusCode = response.status_code - if statusCode == 200: - print("IP found:", ip) - retval = ip - break - except: - continue - return retval class Charger: @@ -32,7 +16,7 @@ def __init__(self, url, api_version=2, timeout=5): """ ini function at instantiation of this class - :param url: WiFi or cloud url of charger + :param url: Wi-Fi or cloud url of charger :param api_version: API version (series CM-02: 1, series CM-03: 2) :param timeout: optional seconds. Default = 15 """ @@ -49,38 +33,21 @@ def __init__(self, url, api_version=2, timeout=5): self.chargerData = {'car': 0, 'amp': 0, 'nrg': 15 * [0], 'pha': 0, 'dwo': 0, 'ast': 1, 'err': -1, 'statusCode': -1} - def __search_charger(self, tout=0.5): - retval = "-1" - command = "/api/status?filter=fna" - for i in range(1, 250): - ip = self.url_root + str(i) - try: - response = requests.get(ip + command, timeout = 0.5) - statusCode = response.status_code - if statusCode == 200: - print("IP found:", ip) - retval = ip - break - except: - continue - return retval - - def __search_charger(self, tout=0.5): - retval = "-1" - command = "/api/status?filter=fna" - for i in range(1, 250): - ip = self.url_root + str(i) - try: -# response = requests.get(ip + command, timeout=(tout, 2)) - response = requests.get(ip + command, timeout = 0.5) - statusCode = response.status_code - if statusCode == 200: - print("IP found:", ip) - retval = ip - break - except: - continue - return retval + # def __search_charger(self, tout=0.5): + # retval = "-1" + # command = "/api/status?filter=fna" + # for i in range(1, 250): + # ip = self.url_root + str(i) + # try: + # response = requests.get(ip + command, timeout = 0.5) + # statusCode = response.status_code + # if statusCode == 200: + # print("IP found:", ip) + # retval = ip + # break + # except: + # continue + # return retval def get_charger_data(self): """ diff --git a/const.py b/const.py index cf43bd3..3cbdf71 100644 --- a/const.py +++ b/const.py @@ -1,3 +1,8 @@ +""" Project wide constants. + +project wide constants defined here and in also from evs.cfg file. +""" + import configparser as CP from pathlib import Path from os import path as LogPath @@ -5,7 +10,7 @@ projectRoot = str(Path(__file__).parent) print("Project Root = ", projectRoot) -C_APP_VERSION = '0.9.0rc4' +C_APP_VERSION = '0.9.0rc5' configFile = projectRoot + "/evs.cfg" C_DEFAULT_SETTINGS_FILE = projectRoot + "/PV_Manager.json" C_INI_FILE = projectRoot + "/evsGUI.ini" @@ -47,8 +52,7 @@ C_SYS_LOG_INTERVAL = int(config['SYSTEM']['log_interval']) C_CAR_STATE = ["Car Unpluged", "Car Ready", "Car Charging"] -C_CHARGER_STATUS_TEXT = ("0:Error", "1: Idle", "2: Charging", "3: WaitCar", "4: Completeç, 5: Error") -# C_CHARGER_STATUS_TEXT = ("No Charger access", "Vehicle unplugged", "Charging", "Awaiting command", "Ready") +C_CHARGER_STATUS_TEXT = ("0: imError", "1: Idle", "2: Charging", "3: WaitCar", "4: Completeç, 5: Error") C_MODE_TXT = ["CAR UNPLUGGED", "IDLE, waiting for event ...", "SOLAR CHARGE init ...", @@ -90,5 +94,5 @@ 7wdW741898y57yc2ia4ceOFeLTvnYZfsuesm1Q78WfQL9W3SKY3kyi04NP7BTWnQBTm9xRI24kp3nz9If3278UQPZrKUAAAAASUVORK5CYII=' # ---------------- Weather constants (not used yet) ----------------------------- -# C_WEATHER_URL = "https://api.openweathermap.org/data/2.5/forecast?q=arbon,ch&&units=metric&APPID=" -# C_WEATHER_API_KEY = "e00176e559c6b27d5df2fe21d8cd26e0" +C_WEATHER_URL = "" +C_WEATHER_API_KEY = "" diff --git a/evsGUI.py b/evsGUI.py index 8da5a8d..d9caccc 100644 --- a/evsGUI.py +++ b/evsGUI.py @@ -1,8 +1,9 @@ -# import as sg +"""Graphical user interface based on FreeSimpleGUI( formar PySimpleGUI).""" + import FreeSimpleGUI as sg import const -#todo replace limit text limit sign by graph + #sg.theme('DarkBlue3') #sg.theme('SystemDefault') #sg.theme("SystemDefaultForReal") @@ -33,6 +34,13 @@ def LEDIndicator(key=None, radius=30): + """ + LED widget built as circle. + + :param key: label of selected LED + :param radius: LED radius in pixels + :return: LED + """ return sg.Graph(canvas_size=(radius, radius), graph_bottom_left=(-radius, -radius), graph_top_right=(radius, radius), @@ -40,6 +48,14 @@ def LEDIndicator(key=None, radius=30): def SetLED(win, key, color): + """ + Display Activate LED widget + + :param win: parent window + :param key: label of selected LED + :param color: color of selected LED + :return: none + """ graph = win[key] graph.erase() graph.draw_circle((0, 0), 12, fill_color=color, line_color='black') @@ -58,10 +74,8 @@ def SetLED(win, key, color): sg.Stretch(), LEDIndicator('-LED_CAR-')], [limitSign, limitText, sg.Text('%', pad=0)]]) -# [))]]) ], [sg.Text("")], - [sg.Frame(title='Charging Power', size=(530,80), layout=[ [chargePwrBar, chargeDisp, sg.Text('kW', pad=0), chargeCurrentDisp, sg.Text('A', pad=0), phasesDisp, sg.Text('Phase'), sg.Stretch(), LEDIndicator('-LED_CHARGER-') ], @@ -77,7 +91,6 @@ def SetLED(win, key, color): [sg.Frame(title='Messages', size=(530,60), layout=[ [messageText, sg.Stretch(), LEDIndicator('-LED_MSG-')]])]] -# [sg.Text('Initializing ...', key='-MESSAGE-', size=53, text_color ='grey10', background_color ='light grey')]])]] layout = [[sg.Column(col1)], [sg.Button('Force Charge', disabled=True), sg.Button('Stop Charge', disabled=True), @@ -89,12 +102,14 @@ def SetLED(win, key, color): def testLayout(): + """Displays main window (static).""" + while 1: event, values = window.read(timeout=200) if event == 'Quit' or event == sg.WIN_CLOSED: quit() window['-battBar-'].UpdateBar(50) - SetLED(window,'-LED_SOLAR-', 'grey') + SetLED(window,'-LED_SOLAR-', 'yellow') SetLED(window,'-LED_FORCED-', 'grey') SetLED(window,'-LED_EXTERN-', 'grey') SetLED(window,'-LED_MSG-', 'grey') diff --git a/mainControl.py b/mainControl.py index 94e05f2..7e37d70 100644 --- a/mainControl.py +++ b/mainControl.py @@ -1,3 +1,5 @@ +""" Main control and display module.""" + import configparser as CP import os.path @@ -10,15 +12,6 @@ import charger import utils - -# pwrOnTimer = timers.EcTimer() -# pwrOnTimer.set(2) - -# while(pwrOnTimer.read()): -# x = 1 - - - SIMULATE_PV_TO_GRID = 0 # restore window position @@ -73,8 +66,14 @@ settings = sysSettings.defaultSettings sysSettings.writeSettings(const.C_DEFAULT_SETTINGS_FILE, settings) - def printMsg(item="", value=""): + """ + Write message into message window and return message. + + :param item: First text in message + :param value: optional values to display + :return: composed text + """ text = f'{item} {value}' window['-MESSAGE-'].update(text) return text @@ -83,7 +82,6 @@ def printMsg(item="", value=""): messageTxt = printMsg('Charger on URL', const.C_CHARGER_WIFI_URL) go_e = charger.Charger(url= const.C_CHARGER_WIFI_URL) -utils.charge = go_e #forward object go-e to utils.py utils.writeLog(sysData, strMessage=messageTxt, strMode=chargeMode) diff --git a/popCharge.py b/popCharge.py index 139322c..b4bfa47 100644 --- a/popCharge.py +++ b/popCharge.py @@ -1,4 +1,5 @@ -# import PySimpleGUI as sg +"""Popup window for manual basic charge settings""" + import FreeSimpleGUI as sg import os.path import const diff --git a/popSettings.py b/popSettings.py index 72ae708..27c7b55 100644 --- a/popSettings.py +++ b/popSettings.py @@ -1,4 +1,5 @@ -# import PySimpleGUI as sg +"""Popup window for solar charge settings""" + import FreeSimpleGUI as sg import os.path @@ -50,7 +51,6 @@ def popSettings(batteryLevel=40, file=const.C_DEFAULT_SETTINGS_FILE, pop_locatio # test global padding popWin = sg.Window('Manual Charge Options', layout_popC, element_padding=0) popWin = sg.Window('PV Options', layout_popC, location=pop_location, modal=True, icon=const.C_LOGO) - # p = popWin['battLevel'] while True: ev2, val2 = popWin.read(100) @@ -66,11 +66,6 @@ def popSettings(batteryLevel=40, file=const.C_DEFAULT_SETTINGS_FILE, pop_locatio done = True break - # if ev2 == '-CHARGE LIMIT-': # prevent limit lower than actuaöl battery level - # if val2['-CHARGE LIMIT-'] < batteryLevel: - # popWin['-CHARGE LIMIT-'].update(batteryLevel) - # print(manualSettings) - # noinspection PySimplifyBooleanCheck if val2['-allow_3_phases-'] == True: popWin['-MIN_I_3_PH-'].update(disabled=False) diff --git a/sysSettings.py b/sysSettings.py index 2495f10..7c03639 100644 --- a/sysSettings.py +++ b/sysSettings.py @@ -1,14 +1,18 @@ +"""Read an write charge settings from and to json file.""" import json -# import PySimpleGUI as sg -import FreeSimpleGUI as sg -# default -# defaultSettings = {'manual':{'cancelled': True, 'currentSet': 12, 'chargeLimit': 80, '3_phases': False, 'phaseSet': True}, -# 'pv': {'currentSet': 8, 'chargeLimit': 80, '3_phases': False}} + defaultSettings = {'manual': {'currentSet': 12, 'chargeLimit': 80, '3_phases': False, 'phaseSet': True}, 'pv': {'max_1_Ph_current': 14, 'min_3_Ph_current': 8, 'chargeLimit': 80, 'allow_3_phases': False}} def writeSettings(file="", settingsDict=None): + """ + writes dictionary to json formatted file. + + :param file: name of setting json file + :param settingsDict: name of dictionary containing actual settings + :return: --- + """ if settingsDict is None: settingsDict = {} with open(file, "w") as write_file: @@ -16,5 +20,11 @@ def writeSettings(file="", settingsDict=None): def readSettings(file=""): + """ + reads dictionary from json formatted file. + + :param file: + :return: + """ with open(file) as readFile: return json.load(readFile) diff --git a/timers.py b/timers.py index ef855d9..a70a261 100644 --- a/timers.py +++ b/timers.py @@ -1,27 +1,33 @@ +"""Precision timer routines using 'perf_counter'.""" import time - class TimerError(Exception): """A custom exception used to report errors in use of Timer class.""" class EcTimer: - """Precision counters based on time.perf_counter.""" + """Precision counters based on time.perf_counter. + + Each created instance uses independent timer + """ def __init__(self): self._start_time = None - # classic backcounting timer + # classic back counting timer def set(self, timePeriod): """ - Set timer in seconds - :return: --- + set and start timer. + + :param timePeriod: value in seconds as float + :return: --- """ + self._start_time = timePeriod + time.perf_counter() def read(self): """ - Read remaing time. Zero if time elapsed + Read remaning time. Zero if time elapsed :return: remaining time """ @@ -34,25 +40,23 @@ def read(self): return remain # more functions for time measurement - def start(self): + def elapsed(self): """ - Start up counter + Get time since start in seconds. - :return: --- + :return: elapsed seconds as float """ - if self._start_time is not None: - raise TimerError(f"Timer is running. Use .stop() to stop it") - - self._start_time = time.perf_counter() - - def elapsed(self): if self._start_time is not None: elapsed_time = time.perf_counter() - self._start_time print(f"Elapsed time: {elapsed_time:0.4f} seconds") return elapsed_time def stop(self): - """Stop the timer, and report the elapsed time""" + """Stop the timer and return the elapsed time + + :return: elapsed seconds as float + """ + if self._start_time is None: raise TimerError(f"Timer is not running") diff --git a/utils.py b/utils.py index 1ac63e0..b5369cc 100644 --- a/utils.py +++ b/utils.py @@ -1,3 +1,5 @@ +"""High level API for charging procedures.""" + import os from datetime import datetime import csv @@ -6,7 +8,6 @@ import charger import access -charge = None # window = evsGUI.window # def printMsg(text=''): # window['-MESSAGE-'].update(text) @@ -15,10 +16,8 @@ # todo: PV power must remain for x minutes before decision # todo: night charging solution (charge < x%; manual intervention via remotecontrol ...) -###### charger_ip = charger.search_charger(const.C_CHARGER_WIFI_URL) -###### charger_ip = charger.search_charger(const.C_CHARGER_WIFI_URL) -charger_ip = const.C_CHARGER_WIFI_URL +charger_ip = const.C_CHARGER_WIFI_URL # TODO automatic search charger charge = charger.Charger(charger_ip, const.C_CHARGER_API_VERSION) class SysData: # class variables as kind of (global) C structure @@ -48,7 +47,7 @@ class SysData: # class variables as kind of (global) C structure scanTimer = timers.EcTimer() pvScanTimer = timers.EcTimer() carScanTimer = timers.EcTimer() - carErrorCounter = 0 # increment if eror during car data read + carErrorCounter = 0 # increment if error during car data read pvError = 0 chargerError = 0 @@ -71,7 +70,8 @@ class ChargeModes: # kind of enum def processChargerData(sysData=SysData): """ - Converts charger data into sysData format + Converts charger data into sysData format. + :param sysData: data record similar to C structure :return: updated sysData """ @@ -90,7 +90,7 @@ def processChargerData(sysData=SysData): sysData.carPlugged = True if sysData.chargerAPIversion == 1: sysData.chargePower = chargerData['nrg'][11] / 100 # original value is in 10W - sysData.currentL1 = chargerData['nrg'][4] / 10 # original alue is in 0.1A + sysData.currentL1 = chargerData['nrg'][4] / 10 # original value is in 0.1A if chargerData['nrg'][4] > 10: sysData.chargeActive = True else: @@ -125,9 +125,9 @@ def calcChargeCurrent(sysData, chargeMode, maxCurrent_1P, minCurrent_3P): Calculate optimal charge current depending on solar power :param sysData: data record similar to C structure - :param chargeMode: + :param chargeMode: constant from class ChargeModes :param maxCurrent_1P: [A] upper limit for 1 phase - :param minCurrent_3P: [A] minimum used for switch overr to 3 phases + :param minCurrent_3P: [A] minimum used for switch over to 3 phases :return sysData: updatad data record """ @@ -231,7 +231,7 @@ def evalChargeMode(chargeMode, sysData, settings): else: new_chargeMode = ChargeModes.UNPLUGGED - #### charge end ctriteria + #### charge end criteria if sysData.batteryLevel >= sysData.batteryLimit \ or sysData.calcPvCurrent_1P < const.C_CHARGER_MIN_CURRENT \ or new_chargeMode == ChargeModes.UNPLUGGED: @@ -307,7 +307,7 @@ def evalChargeMode(chargeMode, sysData, settings): elif chargeMode == ChargeModes.EXTERN: if sysData.chargePower < 1: - charge.stop_charging(authenticate=1) # dispite external stop, restore authenticate + charge.stop_charging(authenticate=1) # despite external stop, restore authenticate new_chargeMode = ChargeModes.IDLE elif chargeMode == ChargeModes.UNPLUGGED: @@ -335,12 +335,12 @@ def evalChargeMode(chargeMode, sysData, settings): def writeLog(sysData, strMessage="", strMode="", logpath=const.C_LOG_PATH): """ - Write logfile on event or mode change + Write logfile on event or mode change. :param strMode: - :param strMessage: - :param sysData: object of class SysData - :param logpath: full path including filew nane + :param strMessage: free text + :param sysData: instance of class SysData + :param logpath: full path including file nane :return: characters written """ diff --git a/zozo.py b/zozo.py index 0737ad4..f2d138e 100644 --- a/zozo.py +++ b/zozo.py @@ -1,3 +1,8 @@ +"""High level API based on 'Renault API' + + Uses Renault https://renault-api.readthedocs.io/en/latest/endpoints.html +""" + import requests import urllib.parse import json @@ -6,7 +11,7 @@ # 'FR' replaced by 'CH' (Switzerland # File Operations partly removed -# API Referemce https://renault-api.readthedocs.io/en/latest/endpoints.html + def encodeURIComponent(s): return urllib.parse.quote(s) @@ -45,16 +50,6 @@ def saveToFile(self, data, filename): with open(filename, "w") as f: f.write(data) - # def cleanPersonnalInfo(self): - # if os.path.exists("firststep.dta"): - # os.remove("firststep.dta") - # if os.path.exists("secondstep.dta"): - # os.remove("secondstep.dta") - # if os.path.exists("thirdstep.dta"): - # os.remove("thirdstep.dta") - # if os.path.exists("fourstep.dta"): - # os.remove("fourstep.dta") - def getPersonalInfo(self): # Save the result to a file, to avoid being annoyed by renault server quota limits. data = None @@ -92,15 +87,6 @@ def getPersonalInfo(self): #Save the result to a file, to avoid being annoyed by renault server quota limits. data = None -# data = self.loadFromFile("fourstep.dta") -# if data is None: -# url = self.kamareonURL + '/commerce/v1/accounts/' + self.account_id + '/vehicles?country=CH' -# headers = {"x-gigya-id_token": self.gigyaJWTToken, "apikey": self.kamareonAPI} -# response = requests.get(url, headers=headers, timeout=10) -# print("response step4", response) -# data = response.text -# self.saveToFile(data, "fourstep.dta") - # self.VIN = json.loads(data)["vehicleLinks"][0]["vin"] self.VIN = const.C_RENAULT_VIN def batteryStatus(self):