diff --git a/README.md b/README.md index 5103b807..376b1a6c 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,19 @@ With this app you will be able to: - lock and unlock doors - get consumption statistics - visualize your trips on a map or in a table - - get the list of car charging + - get the list of car charging - visualize battery charging curve - visualize altitude trip curve - get car charging CO2 emission - get car charging price - send live data to ABetterRoutePlanner -The official API is documented [here](https://developer.groupe-psa.io/webapi/b2c/quickstart/connect/#article) but it is not totally up to date, and contains some errors. +The official API is documented [here](https://developer.groupe-psa.io/webapi/b2c/quickstart/connect/#article) but it is not totally up to date, and contains some errors. ## I. Installation - [Installation on Linux or Windows](docs/Install.md) -- [Installation as Home Assistant addon](https://github.com/flobz/psacc-ha/blob/main/psacc-ha/README.md) +- [Installation as Home Assistant addon](https://github.com/flobz/psacc-ha/blob/main/psacc-ha/README.md) - [Installation in Docker](docs/Docker.md) - [Installation on Raspberry Pi with docker-compose (external Tutorial)](https://return2.net/opel-peugeot-electric-vehicle-set-charging-threshold-limit/) ## II. Use the API @@ -40,13 +40,13 @@ Look at [API documentation](./docs/psacc_api.md) You can add the -r argument to record the position of the vehicle and retrieve this information in a dashboard. ``python3 psa-car-controller -f config.json -c charge_config.json -r`` - + You will be able to visualize your trips, your consumption and some statistics: - - + + ![Screenshot_20210128_104519](https://user-images.githubusercontent.com/48728684/106119895-01c98d80-6156-11eb-8969-9e8bc24f3677.png) - You have to add an API key from https://home.openweathermap.org/ in your config file, to be able to see your consumption vs exterior temperature. -- You have to add an API key from https://co2signal.com/ to have your CO2 emission by KM (in France the key isn't needed). +- You have to add an API key from https://co2signal.com/ to have your CO2 emission by KM (in France the key isn't needed). ### IV. Charge price calculation The dashboard can give you the price by kilometer and price by kw that you pay. You just have to set the price in the config file. @@ -54,7 +54,7 @@ You just have to set the price in the config file. After a successful launch of the app, a config.ini file will be created. In this file you can set the price you pay for electricity in the following format "0.15". -If you have a special price during the night you can set "night price", "night hour start" and "night hour end". +If you have a special price during the night you can set "night price", "night hour start" and "night hour end". Hours need to be in the following format "23h12". You can modify a price manually in the dashboard. It can be useful if you use public charge point. @@ -62,11 +62,12 @@ You can modify a price manually in the dashboard. It can be useful if you use pu - [Domoticz](docs/domoticz/Domoticz.md) - [HomeAssistant](https://github.com/Flodu31/HomeAssistant-PeugeotIntegration) - Jeedom (Anyone can share the procedure ?) -- You can send live car status to ABRP (A better Route Planner), see [this page](docs/abrp.md) +- You can send live car status to ABRP (A better Route Planner), see [this page](docs/osmandapi.md) +- You can send live car status to OsmAndApi (A better Route Planner), see [this page](docs/abrp.md) - [Grafana](https://github.com/flobz/psa_car_controller/issues/161) ## FAQ -If you have a problem or a question, please check if the answer isn't already in the [FAQ](FAQ.md). +If you have a problem or a question, please check if the answer isn't already in the [FAQ](FAQ.md). ## Contribute If you need information to contribute or edit this program go [here](docs/Develop.md). diff --git a/docs/osmandapi.md b/docs/osmandapi.md new file mode 100644 index 00000000..63603811 --- /dev/null +++ b/docs/osmandapi.md @@ -0,0 +1,27 @@ +## Connect to OsmAnd API recipient + +Use OsmAnd APR recipient - e.g. [Traccar](https://www.traccar.org) - to track your device + +### Prerequisite +1. A working last version of psa_car_controller +2. Access to an OsmAnd API compatible receiver + +### Procedure +1. Go to your OsmAnd receiver and setup a car which matches your VIN or use a self defined identifier +2. Enable OsmAndAPI for your car in "Control" section +3. stop psa_car_controller +4. edit config.json and check the following: + 4.1: + "osmandapi": { + "osmand_enable_vin": [ + "" + ], + "server_uri": "https://" + }, +5. If you want to use a self defined identifier (you can skip this if you id should be your VIN): Open cars.json + 3.1: set osmand_id to you your chosen device id + + 11.4 save the file + + 11.5 restart psa_car_controller +6. Enjoy \ No newline at end of file diff --git a/psa_car_controller/psacc/application/osmand.py b/psa_car_controller/psacc/application/osmand.py new file mode 100644 index 00000000..0c8b6d8f --- /dev/null +++ b/psa_car_controller/psacc/application/osmand.py @@ -0,0 +1,79 @@ +import logging +from datetime import datetime + +import requests + +from psa_car_controller.psacc.model.car import Car + +logger = logging.getLogger(__name__) +TIMEOUT_IN_S = 10 + + +class OsmAndApi: + def __init__(self, server_uri: str = None, osmand_enable_vin=None): + if osmand_enable_vin is None: + osmand_enable_vin = [] + self.__server_uri = server_uri + self.osmand_enable_vin = set(osmand_enable_vin) + self.proxies = None + + def enable_osmand(self, vin, enable): + if enable: + self.osmand_enable_vin.add(vin) + else: + self.osmand_enable_vin.discard(vin) + + def call(self, car: Car, ext_temp: float = None): + try: + if car.vin in self.osmand_enable_vin: + if self.__server_uri is None: + logger.debug("osmandapi: No Server URI set") + return False + + if not car.has_fuel() and not car.has_battery(): + logger.debug("Neither fuel nor battery available") + return False + + data = { + "id": car.get_osmand_id(), + "timestamp": int(datetime.timestamp(car.status.last_position.properties.updated_at)), + "is_parked": not bool(car.status.is_moving()), + "odometer": car.status.timed_odometer.mileage * 1000, + "speed": getattr(car.status.kinetic, "speed", 0.0), + "lat": car.status.last_position.geometry.coordinates[1], + "lon": car.status.last_position.geometry.coordinates[0], + "altitude": car.status.last_position.geometry.coordinates[2] + } + if car.has_fuel: + fuel = car.status.get_energy('Fuel') + data["fuel"] = fuel.level + if car.has_battery(): + energy = car.status.get_energy('Electric') + if energy.battery and energy.battery.health: + data["soh"] = energy.battery.health.resistance + data["soc"] = energy.level + data["batt"] = energy.level + data["power"] = energy.consumption + data["current"] = car.status.battery.current + data["voltage"] = car.status.battery.voltage + data["is_charging"] = energy.charging.status == "InProgress" + data['est_battery_range'] = energy.autonomy + + if ext_temp is not None: + data["ext_temp"] = ext_temp + + response = requests.request("POST", self.__server_uri, params=data, proxies=self.proxies, + verify=self.proxies is None, timeout=TIMEOUT_IN_S) + logger.debug(response.text) + try: + return response.status_code == 200 + except (KeyError): + logger.error("Bad response from OsmAnd API: %s", response.text) + return False + except (AttributeError, IndexError, ValueError): + logger.exception("osmandapi:") + return False + + def __iter__(self): + yield "osmand_enable_vin", list(self.osmand_enable_vin) + yield "server_uri", self.__server_uri diff --git a/psa_car_controller/psacc/application/psa_client.py b/psa_car_controller/psacc/application/psa_client.py index 1dc34588..e4d9bade 100644 --- a/psa_car_controller/psacc/application/psa_client.py +++ b/psa_car_controller/psacc/application/psa_client.py @@ -20,6 +20,7 @@ from psa_car_controller.psa.constants import realm_info, AUTHORIZE_SERVICE from .abrp import Abrp +from .osmand import OsmAndApi from psa_car_controller.psacc.repository.db import Database from psa_car_controller.common.mylogger import CustomLogger @@ -36,7 +37,7 @@ def connect(self, code: str): # pylint: disable=too-many-arguments def __init__(self, refresh_token, client_id, client_secret, remote_refresh_token, customer_id, realm, country_code, - brand=None, proxies=None, weather_api=None, abrp=None, co2_signal_api=None): + brand=None, proxies=None, weather_api=None, abrp=None, osmandapi=None, co2_signal_api=None): self.realm = realm self.service_information = ServiceInformation(AUTHORIZE_SERVICE[self.realm], realm_info[self.realm]['oauth_url'], @@ -68,6 +69,10 @@ def __init__(self, refresh_token, client_id, client_secret, remote_refresh_token self.abrp = Abrp() else: self.abrp: Abrp = Abrp(**abrp) + if osmandapi is None: + self.osmandapi = OsmAndApi() + else: + self.osmandapi: OsmAndApi = OsmAndApi(**osmandapi) self.set_proxies(proxies) self.config_file = DEFAULT_CONFIG_FILENAME Ecomix.co2_signal_key = co2_signal_api @@ -94,6 +99,7 @@ def set_proxies(self, proxies): else: self.api_config.proxy = proxies['http'] self.abrp.proxies = proxies + self.osmandapi.proxies = proxies self.manager.proxies = proxies def get_vehicle_info(self, vin, cache=False): @@ -168,7 +174,7 @@ def load_config(name="config.json"): config = {**json.loads(config_str)} if "country_code" not in config: config["country_code"] = input("What is your country code ? (ex: FR, GB, DE, ES...)\n") - for new_el in ["abrp", "co2_signal_api"]: + for new_el in ["abrp", "osmandapi", "co2_signal_api"]: if new_el not in config: config[new_el] = None psacc = PSAClient(**config) @@ -196,7 +202,9 @@ def record_info(self, car: Car): # pylint: disable=too-many-locals moving) Database.record_position(self.weather_api, car.vin, mileage, latitude, longitude, altitude, date, level, level_fuel, moving) - self.abrp.call(car, Database.get_last_temp(car.vin)) + last_temp = Database.get_last_temp(car.vin) + self.abrp.call(car, last_temp) + self.osmandapi.call(car, last_temp) if car.has_battery(): electric_energy_status = car.status.get_energy('Electric') try: @@ -231,6 +239,7 @@ def default(self, mp: PSAClient): # pylint: disable=arguments-renamed "refresh_token": mp.manager.refresh_token, "client_secret": mp.service_information.client_secret, "abrp": dict(mp.abrp), + "osmandapi": dict(mp.osmandapi), "remote_refresh_token": mp.remote_client.remoteCredentials.refresh_token, "customer_id": mp.account_info.customer_id, "client_id": mp.account_info.client_id, diff --git a/psa_car_controller/psacc/model/car.py b/psa_car_controller/psacc/model/car.py index efbe379a..27e7fa54 100644 --- a/psa_car_controller/psacc/model/car.py +++ b/psa_car_controller/psacc/model/car.py @@ -10,7 +10,7 @@ # pylint: disable=too-many-arguments class Car: def __init__(self, vin, vehicle_id, brand, label=None, battery_power=None, fuel_capacity=None, - max_elec_consumption=None, max_fuel_consumption=None, abrp_name=None): + max_elec_consumption=None, max_fuel_consumption=None, abrp_name=None, osmand_id=None): self.vin = vin model = None if label is not None: @@ -23,6 +23,7 @@ def __init__(self, vin, vehicle_id, brand, label=None, battery_power=None, fuel_ self.brand = brand self._status = None self.abrp_name = abrp_name or model.abrp_name + self.osmand_id = osmand_id self.battery_power = battery_power or model.battery_power self.fuel_capacity = fuel_capacity or model.fuel_capacity self.max_elec_consumption = max_elec_consumption or model.max_elec_consumption # kwh/100Km @@ -46,7 +47,7 @@ def has_battery(self): def has_fuel(self): return self.fuel_capacity > 0 - def get_status(self): + def get_status(self) -> CarStatus: if self.status is not None: return self.status logger.error("status of %s is None", self.vin) @@ -69,6 +70,11 @@ def get_abrp_name(self): return self.abrp_name raise ValueError("ABRP model is not set") + def get_osmand_id(self): + if self.osmand_id is not None: + return self.osmand_id + return self.vin + @property def status(self) -> CarStatus: return self._status diff --git a/psa_car_controller/web/view/control.py b/psa_car_controller/web/view/control.py index df1ef1ed..b9822cb4 100644 --- a/psa_car_controller/web/view/control.py +++ b/psa_car_controller/web/view/control.py @@ -14,6 +14,7 @@ REFRESH_SWITCH = "refresh-switch" ABRP_SWITCH = 'abrp-switch' +OSMANDAPI_SWITCH = 'osmandapi-switch' CHARGE_SWITCH = "charge-switch" PRECONDITIONING_SWITCH = "preconditioning-switch" @@ -72,5 +73,7 @@ def get_control_tabs(config): if not config.offline: buttons_row.append(Switch(ABRP_SWITCH, car.vin, "Send data to ABRP", myp.abrp.enable_abrp, car.vin in config.myp.abrp.abrp_enable_vin).get_html()) + buttons_row.append(Switch(OSMANDAPI_SWITCH, car.vin, "Send data to OsmAndApi", myp.osmandapi.enable_osmand, + car.vin in config.myp.osmandapi.osmand_enable_vin).get_html()) tabs.append(dbc.Tab(label=label, id="tab-" + car.vin, children=[dbc.Row(buttons_row), *el])) return dbc.Tabs(id="control-tabs", children=tabs)