mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-22 01:16:14 +00:00
Merge remote-tracking branch 'origin/master' into multi-day-charge-control
# Conflicts: # psa_car_controller/psacc/application/charge_control.py
This commit is contained in:
@@ -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
|
||||
|
||||
+8
-6
@@ -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
|
||||
high speed dc charge threshold
|
||||
charger efficiency = 0.8942
|
||||
|
||||
Generated
+16
-1
@@ -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"},
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from pathlib import Path
|
||||
from single_source import get_version
|
||||
|
||||
__version__ = get_version(__name__, Path(__file__), default_return="dev")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
-1
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user