diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 816d5fc..648a09c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,4 +41,5 @@ jobs: echo Test source .venv/bin/activate coverage run -m unittest || exit 1 + coverage report [ -n "$CODACY_PROJECT_TOKEN" ] && coverage combine && coverage xml -o cobertura.xml && bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r cobertura.xml diff --git a/config.ini b/config.ini index 2f21f22..1b16fcd 100644 --- a/config.ini +++ b/config.ini @@ -3,15 +3,17 @@ currency = € # minimum trip length in km so it's added to stats and map in website minimum trip length = 10 +length unit = km [Electricity config] # price by kw/h day price = 0.15 -night price = +night price # ex: 22h30 -night hour start = +night hour start # ex: 6h00 -night hour end = -dc charge price = 0.4 -high speed dc charge price = 0.6 +night hour end +dc charge price +high speed dc charge price # minimum power in kW that should be delivered during a charge so it can be considered as a high speed charger -high speed dc charge threshold = 60 \ No newline at end of file +high speed dc charge threshold +charger efficiency = 0.8942 diff --git a/poetry.lock b/poetry.lock index 53c65fc..39da348 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1103,6 +1103,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "single-source" +version = "0.3.0" +description = "Access to the project version in Python code for PEP 621-style projects" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +importlib_metadata = {version = ">=3.0,<5", markers = "python_version < \"3.8\""} + [[package]] name = "six" version = "1.16.0" @@ -1247,7 +1258,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = ">=3.7.0, <4.0.0" -content-hash = "3e21b3d9a8d196a0e896b4dec41624bf71cc8967fbd42f094afe871639d3f748" +content-hash = "237337e04594d877cea819e9ff6b56d18fa1c4583396f1ec3eca53d98378add7" [metadata.files] androguard = [ @@ -2196,6 +2207,10 @@ setuptools-scm = [ simplegeneric = [ {file = "simplegeneric-0.8.1.zip", hash = "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173"}, ] +single-source = [ + {file = "single-source-0.3.0.tar.gz", hash = "sha256:b12705af958ca99d56ea9ce40bd9cc749378f4fe7ad03b1f9067e29daceef27d"}, + {file = "single_source-0.3.0-py3-none-any.whl", hash = "sha256:7bc87168ced50f638b6ab0cda4cc1ce9e80ee0e1220014397050d336d021a597"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/psa_car_controller/__init__.py b/psa_car_controller/__init__.py index e69de29..51e090d 100644 --- a/psa_car_controller/__init__.py +++ b/psa_car_controller/__init__.py @@ -0,0 +1,4 @@ +from pathlib import Path +from single_source import get_version + +__version__ = get_version(__name__, Path(__file__), default_return="dev") diff --git a/psa_car_controller/psacc/application/car_controller.py b/psa_car_controller/psacc/application/car_controller.py index 0d2978a..f17191f 100644 --- a/psa_car_controller/psacc/application/car_controller.py +++ b/psa_car_controller/psacc/application/car_controller.py @@ -4,6 +4,7 @@ import logging import threading from os import environ, path +import psa_car_controller from oauth2_client.credentials_manager import OAuthError from .charge_control import ChargeControls @@ -36,6 +37,8 @@ def parse_args(): parser.add_argument("--offline", help="offline limited mode", action='store_true') parser.add_argument("--web-conf", help="ignore if config files not existing yet", action='store_true') parser.add_argument("-b", "--base-path", help="base path for web app", default="/") + parser.add_argument('--version', action='version', version='PSACC {}'.format(psa_car_controller.__version__)) + return parser.parse_args() diff --git a/psa_car_controller/psacc/application/charge_control.py b/psa_car_controller/psacc/application/charge_control.py index 0ad8193..da288cf 100644 --- a/psa_car_controller/psacc/application/charge_control.py +++ b/psa_car_controller/psacc/application/charge_control.py @@ -85,7 +85,7 @@ class ChargeControl: level = vehicle_status.get_energy('Electric').level has_threshold = self.percentage_threshold < 100 hit_threshold = level >= self.percentage_threshold - if status == INPROGRESS and has_threshold: + if status == INPROGRESS: logger.info("charging status of %s is %s, battery level: %d", self.vin, status, level) self.force_update(vehicle_status) if hit_threshold and self.retry_count < 2: diff --git a/psa_car_controller/psacc/repository/config_repository.py b/psa_car_controller/psacc/repository/config_repository.py index 31fe3fd..6f0b83e 100644 --- a/psa_car_controller/psacc/repository/config_repository.py +++ b/psa_car_controller/psacc/repository/config_repository.py @@ -29,6 +29,7 @@ dc charge price = high speed dc charge price = # minimum power in kW that should be delivered during a charge so it can be considered as a high speed charger high speed dc charge threshold = +charger efficiency = """ @@ -91,6 +92,7 @@ class ElectricityPriceConfig(BaseModel): dc_charge_price: float = None high_speed_dc_charge_price: float = None high_speed_dc_charge_threshold: float = None + charger_efficiency: float = 0.8942 @staticmethod def compare_hour(date: datetime, hour, minute): @@ -128,7 +130,7 @@ class ElectricityPriceConfig(BaseModel): prices.append(self.get_instant_price(date)) date = date + timedelta(minutes=30) try: - res = round(consumption * mean(prices), 2) + res = round(consumption * mean(prices) / self.charger_efficiency, 2) except (TypeError, StatisticsError): logger.error("Can't get_price of charge, check config") return res @@ -157,15 +159,17 @@ class ConfigRepository(BaseModel): def read_config(name=None) -> 'ConfigRepository': try: config_str = ConfigRepository._read_file(name) - config = ConfigUpdater() + config = ConfigUpdater(allow_no_value=True) config.read_string(config_str) + return ConfigRepository.config_file_to_dto(config) except FileNotFoundError: - config = ConfigRepository.get_default_config() - return ConfigRepository.config_file_to_dto(config) + config = ConfigRepository.config_file_to_dto(ConfigRepository.get_default_config()) + config.write_config() + return config @staticmethod def get_default_config(): - config = ConfigUpdater() + config = ConfigUpdater(allow_no_value=True) config.read_string(DEFAULT_CONFIG) return config @@ -198,7 +202,7 @@ class ConfigRepository(BaseModel): for option in config[section]: new_option = option.replace(" ", "_") value = config[section][option].value - if len(value) > 0: + if value and len(value) > 0: new_dict[new_section][new_option] = value config_obj = ConfigRepository(**new_dict) diff --git a/psa_car_controller/web/assets/clientside.js b/psa_car_controller/web/assets/clientside.js index 8a1d155..ff47b23 100644 --- a/psa_car_controller/web/assets/clientside.js +++ b/psa_car_controller/web/assets/clientside.js @@ -70,9 +70,9 @@ function filterDataset (data, range) { return res } -function filterShortTrip (data) { +function filterShortTrip (data, minimumLength) { const longTrips = { - trips: data.trips.filter(line => line.distance > 10), + trips: data.trips.filter(line => line.distance > minimumLength), chargings: data.chargings } console.log('long trips:', longTrips) @@ -87,31 +87,41 @@ function updateFigures (data, oldFigure, x, y) { const xLabel = x[i] const figure = Object.assign({}, oldFigure[i]) i++ - if ('mapbox' in figure.layout) { - figure.data[0].lat = [] - figure.data[0].lon = [] - figure.data[0].hovertext = [] - let trip = null - for (trip of trips) { - const xPos = trip.positions[xLabel] - figure.data[0].lat.push(...xPos, null) - figure.data[0].lon.push(...trip.positions[yLabel[0]]) - figure.data[0].hovertext.push(...Array(xPos.length).fill(trip[yLabel[1]]), null) - } - if (trip) { - const lastPos = trip.positions[yLabel[0]].length - 1 - figure.layout.mapbox.center.lat = trip.positions[xLabel][lastPos] - figure.layout.mapbox.center.lon = trip.positions[yLabel[0]][lastPos] - figure.data[1].lat = [figure.layout.mapbox.center.lat] - figure.data[1].lon = [figure.layout.mapbox.center.lon] - } - } else { - const xValues = trips.map(a => a[xLabel]) - // for each y label - for (let j = 0; j < yLabel.length; j++) { - figure.data[j].y = trips.map(a => a[yLabel[j]]) - figure.data[j].x = xValues - } + const xValues = trips.map(a => a[xLabel]) + // for each y label + for (let j = 0; j < yLabel.length; j++) { + figure.data[j].y = trips.map(a => a[yLabel[j]]) + figure.data[j].x = xValues + } + console.log(xLabel, figure) + figures.push(figure) + }) + return figures +} + +function updateMap (data, oldFigure, x, y, lastPos) { + const trips = data.trips + const figures = [] + let i = 0 + y.forEach(function (yLabel) { + const xLabel = x[i] + const figure = Object.assign({}, oldFigure[i]) + i++ + figure.data[0].lat = [] + figure.data[0].lon = [] + figure.data[0].hovertext = [] + let trip = null + for (trip of trips) { + const xPos = trip.positions[xLabel] + figure.data[0].lat.push(...xPos, null) + figure.data[0].lon.push(...trip.positions[yLabel[0]]) + figure.data[0].hovertext.push(...Array(xPos.length).fill(trip[yLabel[1]]), null) + } + if (trip) { + figure.layout.mapbox.center.lat = lastPos.lat + figure.layout.mapbox.center.lon = lastPos.lon + figure.data[1].lat = [lastPos.lat] + figure.data[1].lon = [lastPos.lon] } console.log(xLabel, figure) figures.push(figure) @@ -215,7 +225,14 @@ function sortMultipleTable (sortParams, data, tables) { } } -function filterAndSort (data, range, figures, p, log, sort) { // eslint-disable-line no-unused-vars +function getLastPosition (trips) { + const lastPos = {} + lastPos.lat = trips.at(-1).positions.lat[0] + lastPos.lon = trips.at(-1).positions.long[0] + return lastPos +} + +function filterAndSort (data, range, figures, p, log, sort, config) { // eslint-disable-line no-unused-vars if (log > 10) { logger.disableLogger() } @@ -236,12 +253,11 @@ function filterAndSort (data, range, figures, p, log, sort) { // eslint-disable- dataFiltered = filterDataset(data, range) sortMultipleTable(sort, dataFiltered, p.table_src) outFigures.push(...updateTables(dataFiltered, p.table_src)) - console.log(dataFiltered.trips.length) - const longTrips = filterShortTrip(dataFiltered) - console.log('trips', dataFiltered.trips.length) + const longTrips = filterShortTrip(dataFiltered, config.minimumLength) + console.log('trips', dataFiltered.trips) console.log('longTrips', longTrips.trips.length) outFigures.push(...updateFigures(longTrips, figures.graph, p.graph_x_label, p.graph_y_label)) - outFigures.push(...updateFigures(dataFiltered, figures.maps, p.map_x_label, p.map_y_label)) + outFigures.push(...updateMap(dataFiltered, figures.maps, p.map_x_label, p.map_y_label, getLastPosition(dataFiltered.trips))) updateCardsValue(longTrips) } return outFigures diff --git a/psa_car_controller/web/tools/figurefilter.py b/psa_car_controller/web/tools/figurefilter.py index 83cb585..63231c4 100644 --- a/psa_car_controller/web/tools/figurefilter.py +++ b/psa_car_controller/web/tools/figurefilter.py @@ -112,9 +112,10 @@ class FigureFilter: }, indent=4) return params - def set_clientside_callback(self, dash_app): + def set_clientside_callback(self, dash_app, config: dict): callback_id = create_callback_id(self.__get_output()) if callback_id not in dash_app.callback_map: + config_str = json.dumps(config) if logger.isEnabledFor(DEBUG): log_level = 10 else: @@ -122,7 +123,8 @@ class FigureFilter: fct_def = f"""function(data,range, figures, {self.gen_sort_variable()}) {{ const params={self.get_params()} const logLevel={log_level} - return filterAndSort(data, range, figures, params, logLevel, {self.__gen_sort_dict()}) + return filterAndSort(data, range, figures, params, logLevel, + {self.__gen_sort_dict()}, {config_str}) }}""" dash_app.clientside_callback(fct_def, *self.__get_output(), diff --git a/psa_car_controller/web/view/views.py b/psa_car_controller/web/view/views.py index 5bd4841..ac344a0 100644 --- a/psa_car_controller/web/view/views.py +++ b/psa_car_controller/web/view/views.py @@ -327,7 +327,7 @@ def serve_layout(): fig_filter.add_table("trips", figures.table_fig) fig_filter.add_table("chargings", figures.battery_table) fig_filter.src = {"trips": trips.get_trips_as_dict(), "chargings": chargings} - fig_filter.set_clientside_callback(dash_app) + fig_filter.set_clientside_callback(dash_app, {"minimumLength": APP.config.General.minimum_trip_length}) create_callback() except (IndexError, TypeError, NameError, AssertionError, NameError, AttributeError): summary_tab = figures.ERROR_DIV @@ -398,6 +398,7 @@ try: if APP.is_good: Charging.set_default_price(APP.myp.vehicles_list) Database.set_db_callback(update_trips) + figures.CURRENCY = APP.config.General.currency update_trips() except (IndexError, TypeError): logger.debug("Failed to get trips, there is probably not enough data yet:", exc_info=True) diff --git a/pyproject.toml b/pyproject.toml index 4c53810..fe04c17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ certifi = ">=14.05.14" six = ">=1.10" python-dateutil = ">=2.5.3" urllib3 = ">=1.15.1" +single-source = "^0.3.0" [tool.poetry.dev-dependencies] prospector = ">=1.3.0" diff --git a/tests/test_unit.py b/tests/test_unit.py index f16684c..fc52e6a 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -203,7 +203,7 @@ class TestUnit(unittest.TestCase): 'end_level': 85, 'co2': co2, 'kw': 20.7, - 'price': 3.84, + 'price': 4.29, 'charging_mode': 'slow'}]) assert get_figures(car) row = {"start_at": date0.strftime('%Y-%m-%dT%H:%M:%S.000Z'),