mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-26 10:17:18 +00:00
Merge branch 'master' into develop
This commit is contained in:
@@ -33,4 +33,4 @@ jobs:
|
||||
run: |
|
||||
echo Test
|
||||
coverage run -m unittest || exit 1
|
||||
coverage xml -o cobertura.xml && bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r cobertura.xml
|
||||
[ -n "$CODACY_PROJECT_TOKEN" ] && coverage xml -o cobertura.xml && bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r cobertura.xml
|
||||
|
||||
@@ -33,3 +33,8 @@ To fix this go to the application directory and execute this command :
|
||||
# if the user is launched by pi user do
|
||||
sudo chown pi: -R .
|
||||
```
|
||||
|
||||
### 5. I doesn't receive SMS
|
||||
The SMS authentication is used to be able to remote control your car.
|
||||
If your car doesn't have this functionality you should disable remote control when you start psa-car-controller
|
||||
by using `--remote-disable` argument.
|
||||
|
||||
@@ -57,6 +57,9 @@ For information on configuring the psa_car_controller Docker container [see this
|
||||
|
||||
2.7 Start/Stop preconditioning
|
||||
http://localhost:5000/preconditioning/YOURVIN/1 or 0
|
||||
|
||||
2.8 Change charge hour (for example: set it to 22h30)
|
||||
http://127.0.0.1:5000/charge_hour?vin=YOURVIN&hour=22&min=30
|
||||
|
||||
|
||||
## III. Use the dashboard
|
||||
|
||||
+1
-1
@@ -17,4 +17,4 @@ docker run -d -ti --name psa_car_controller1 \
|
||||
psa_car_controller
|
||||
```
|
||||
|
||||
Go to http://127.0.0.1:5000 and follow instruction
|
||||
Go to http://127.0.0.1:5000 and follow instruction
|
||||
|
||||
@@ -5,6 +5,7 @@ import numbers
|
||||
|
||||
import requests
|
||||
import reverse_geocode
|
||||
from pytz import UTC
|
||||
|
||||
from mylogger import logger
|
||||
|
||||
@@ -51,12 +52,13 @@ class Ecomix:
|
||||
def get_data_from_co2_signal(latitude, longitude, country_code_default):
|
||||
if Ecomix.co2_signal_key is not None:
|
||||
try:
|
||||
now = datetime.utcnow().replace(tzinfo=UTC)
|
||||
country_code = Ecomix.get_country(latitude, longitude, country_code_default)
|
||||
assert country_code is not None
|
||||
if country_code not in Ecomix._cache:
|
||||
Ecomix._cache[country_code] = []
|
||||
elif len(Ecomix._cache[country_code]) > 0 and \
|
||||
(datetime.now() - Ecomix._cache[country_code][-1][0]).total_seconds() < CO2_SIGNAL_REQ_INTERVAL:
|
||||
(now - Ecomix._cache[country_code][-1][0]).total_seconds() < CO2_SIGNAL_REQ_INTERVAL:
|
||||
return False
|
||||
res = requests.get(CO2_SIGNAL_URL + "/v1/latest",
|
||||
headers={"auth-token": Ecomix.co2_signal_key},
|
||||
@@ -64,7 +66,7 @@ class Ecomix:
|
||||
data = res.json()
|
||||
value = data["data"]["carbonIntensity"]
|
||||
assert isinstance(value, numbers.Number)
|
||||
Ecomix._cache[country_code].append([datetime.now(), value])
|
||||
Ecomix._cache[country_code].append([now, value])
|
||||
return data["status"] == "ok"
|
||||
except (AssertionError, NameError, KeyError):
|
||||
logger.debug("ecomix:", exc_info=True)
|
||||
|
||||
+8
-1
@@ -55,10 +55,17 @@ carmodels = [
|
||||
ElecModel("e-2008", 46, "peugeot:e2008:20:48", r"VR3UKZKX.*"),
|
||||
ElecModel("e-Spacetourer", 46, "peugeot:etraveler:21:50:citroen", r"VF7VZZKX.*"),
|
||||
ElecModel("corsa-e", 46, "opel:corsae:20:50", r"VXKUHZKX.*"),
|
||||
# Use corsa in abrp because Mokka isn't available
|
||||
ElecModel("Mokka-e", 46, "opel:corsae:20:50", r"VXKUKZKX.*"), # VXKUKZKXZM
|
||||
ElecModel("Zaphira-e", 68, "peugeot:etraveler:21:75:opel", r"VXEVZZKX.*"), # VXEVZZKXZMZ
|
||||
ElecModel("E-C4", 46, "citroen:ec4:21:50", r"VR7BCZKX.*"), # VR7BCZKXCM
|
||||
CarModel("SUV 3008", 10.8, 43),
|
||||
CarModel("308", 0, 56, reg=r"VF3L35GG.*"),
|
||||
CarModel("2008", 0, 44, reg=r"VR3USHNS.*"), # VR3USHNSKM
|
||||
CarModel("SUV 5008 II", 0, 56, reg=r"VF3MRHNS.*"), # vf3mrhnsum
|
||||
CarModel("SUV 5008 II 2018", 0, 56, reg=r"VF3MRHNY.*"), # VF3MRHNYHH
|
||||
CarModel("C5 Aircross", 10.8, 43),
|
||||
CarModel("DS7 Crossback E-Tense", 13.2, 43, reg="VR1J45GBUK.*")
|
||||
CarModel("DS7 Crossback E-Tense", 11.5, 43, reg="VR1J45GBUK.*"),
|
||||
CarModel("DS7 Crossback E-Tense 300 4x4", 11.5, 43, reg=" VR1J45GBUL.*"),
|
||||
CarModel("508 SW Hybrid", 11.5, 45, reg=r"VR3F4DGZ.*") # VR3F4DGZTL
|
||||
]
|
||||
|
||||
@@ -3,6 +3,7 @@ from threading import Semaphore, Timer
|
||||
import socket
|
||||
|
||||
import requests
|
||||
from typing import List
|
||||
|
||||
from mylogger import logger
|
||||
|
||||
@@ -47,3 +48,21 @@ def rate_limit(limit, every):
|
||||
def is_port_in_use(ip, port):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
return s.connect_ex((ip, port)) == 0
|
||||
|
||||
|
||||
def parse_hour(s):
|
||||
s = s[2:]
|
||||
separators = ("H", "M", "S")
|
||||
res: List[int] = []
|
||||
for sep in separators:
|
||||
if sep in s:
|
||||
n, s = s.split(sep)
|
||||
else:
|
||||
n = 0
|
||||
res.append(int(n))
|
||||
if s.isnumeric():
|
||||
res.append(int(s))
|
||||
break
|
||||
if len(res) == 2:
|
||||
res.append(0)
|
||||
return res
|
||||
|
||||
+20
-19
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
@@ -22,7 +21,7 @@ from otp.otp import load_otp, new_otp_session, save_otp, ConfigException, Otp
|
||||
from psa_connectedcar.rest import ApiException
|
||||
from mylogger import logger
|
||||
|
||||
from libs.utils import rate_limit
|
||||
from libs.utils import rate_limit, parse_hour
|
||||
from web.abrp import Abrp
|
||||
from web.db import Database
|
||||
|
||||
@@ -38,6 +37,12 @@ realm_info = {
|
||||
"app_name": "MyVauxhall"}
|
||||
}
|
||||
|
||||
MQTT_BRANDCODE = {"AP": "AP",
|
||||
"AC": "AC",
|
||||
"DS": "AC",
|
||||
"VX": "OV",
|
||||
"OP": "OV"
|
||||
}
|
||||
|
||||
AUTHORIZE_SERVICE = "https://api.mpsa.com/api/connectedcar/v2/oauth/authorize"
|
||||
REMOTE_URL = "https://api.groupe-psa.com/connectedcar/v4/virtualkey/remoteaccess/token?client_id="
|
||||
@@ -263,10 +268,14 @@ class MyPSACC:
|
||||
sleep(60)
|
||||
return None
|
||||
|
||||
def __get_mqtt_customer_id(self):
|
||||
brand_code = self.customer_id[:2]
|
||||
return MQTT_BRANDCODE[brand_code]+self.customer_id[2:]
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def __on_mqtt_connect(self, client, userdata, result_code, _):
|
||||
logger.info("Connected with result code %s", result_code)
|
||||
topics = [MQTT_RESP_TOPIC + self.customer_id + "/#"]
|
||||
topics = [MQTT_RESP_TOPIC + self.__get_mqtt_customer_id() + "/#"]
|
||||
for car in self.vehicles_list:
|
||||
topics.append(MQTT_EVENT_TOPIC + car.vin)
|
||||
for topic in topics:
|
||||
@@ -333,24 +342,17 @@ class MyPSACC:
|
||||
self.refresh_token()
|
||||
date = datetime.utcnow()
|
||||
date_str = date.strftime(PSA_DATE_FORMAT)
|
||||
data = {"access_token": self.remote_access_token, "customer_id": self.customer_id,
|
||||
data = {"access_token": self.remote_access_token, "customer_id": self.__get_mqtt_customer_id(),
|
||||
"correlation_id": gen_correlation_id(date), "req_date": date_str, "vin": vin,
|
||||
"req_parameters": req_parameters}
|
||||
|
||||
return json.dumps(data)
|
||||
|
||||
def __get_charge_hour(self, vin):
|
||||
reg = r"PT([0-9]{1,2})H([0-9]{1,2})?"
|
||||
data = self.get_vehicle_info(vin)
|
||||
hour_str = data.get_energy('Electric').charging.next_delayed_time
|
||||
try:
|
||||
hour_minute = re.findall(reg, hour_str)[0]
|
||||
hour = int(hour_minute[0])
|
||||
if hour_minute[1] == '':
|
||||
minute = 0
|
||||
else:
|
||||
minute = hour_minute[1]
|
||||
return hour, minute
|
||||
return parse_hour(hour_str)[:2]
|
||||
except IndexError:
|
||||
logger.exception("Can't get charge hour: %s", hour_str)
|
||||
return None
|
||||
@@ -363,7 +365,7 @@ class MyPSACC:
|
||||
def __veh_charge_request(self, vin, hour, minute, charge_type):
|
||||
msg = self.mqtt_request(vin, {"program": {"hour": hour, "minute": minute}, "type": charge_type})
|
||||
logger.info(msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.customer_id + "/VehCharge", msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.__get_mqtt_customer_id() + "/VehCharge", msg)
|
||||
|
||||
def change_charge_hour(self, vin, hour, miinute):
|
||||
self.__veh_charge_request(vin, hour, miinute, "delayed")
|
||||
@@ -381,19 +383,18 @@ class MyPSACC:
|
||||
def horn(self, vin, count):
|
||||
msg = self.mqtt_request(vin, {"nb_horn": count, "action": "activate"})
|
||||
logger.info(msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.customer_id + "/Horn", msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.__get_mqtt_customer_id() + "/Horn", msg)
|
||||
|
||||
def lights(self, vin, duration: int):
|
||||
msg = self.mqtt_request(vin, {"action": "activate", "duration": duration})
|
||||
logger.info(msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.customer_id + "/Lights", msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.__get_mqtt_customer_id() + "/Lights", msg)
|
||||
|
||||
@rate_limit(3, 60 * 20)
|
||||
def wakeup(self, vin):
|
||||
logger.info("ask wakeup to %s", vin)
|
||||
msg = self.mqtt_request(vin, {"action": "state"})
|
||||
logger.info(msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.customer_id + "/VehCharge/state", msg)
|
||||
return True
|
||||
|
||||
def lock_door(self, vin, lock: bool):
|
||||
@@ -404,7 +405,7 @@ class MyPSACC:
|
||||
|
||||
msg = self.mqtt_request(vin, {"action": value})
|
||||
logger.info(msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.customer_id + "/Doors", msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.__get_mqtt_customer_id() + "/Doors", msg)
|
||||
return True
|
||||
|
||||
def preconditioning(self, vin, activate: bool):
|
||||
@@ -423,7 +424,7 @@ class MyPSACC:
|
||||
}
|
||||
msg = self.mqtt_request(vin, {"asap": value, "programs": programs})
|
||||
logger.info(msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.customer_id + "/ThermalPrecond", msg)
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.__get_mqtt_customer_id() + "/ThermalPrecond", msg)
|
||||
return True
|
||||
|
||||
def save_config(self, name=None, force=False):
|
||||
@@ -436,7 +437,7 @@ class MyPSACC:
|
||||
f.write(config_str)
|
||||
self._config_hash = new_hash
|
||||
logger.info("save config change")
|
||||
|
||||
# disconnect
|
||||
@staticmethod
|
||||
def load_config(name="config.json"):
|
||||
with open(name, "r") as f:
|
||||
|
||||
+6
-1
@@ -20,7 +20,7 @@ from charge_control import ChargeControls
|
||||
from test.utils import DATA_DIR, record_position, latitude, longitude, date0, date1, date2, date3, record_charging, \
|
||||
vehicule_list, get_new_test_db
|
||||
from trip import Trips
|
||||
from libs.utils import get_temp
|
||||
from libs.utils import get_temp, parse_hour
|
||||
from web.db import Database
|
||||
from web.figures import get_figures, get_battery_curve_fig, get_altitude_fig
|
||||
from deepdiff import DeepDiff
|
||||
@@ -217,6 +217,11 @@ class TestUnit(unittest.TestCase):
|
||||
Database.record_position(None, "xx", 11, latitude, longitude - 0.05, None, date0, 40, None, False)
|
||||
assert old_dummy_value != dummy_value
|
||||
|
||||
def test_parse_hour(self):
|
||||
expected_res = [[2, 0, 0], [3, 14, 0], [0, 0, 2], [0, 30, 0]]
|
||||
assert expected_res == [parse_hour(h) for h in ["PT2H", "PT3H14", "PT2S", "PT30M"]]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
my_logger(handler_level=os.environ.get("DEBUG_LEVEL", 20))
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user