diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 2a5a6852c1..29e9a48941 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -57,7 +57,7 @@ class UpdateConfig: - DATASTORE_VERSION = 111 + DATASTORE_VERSION = 112 valid_topic = [ "^openWB/bat/config/bat_control_permitted$", @@ -2839,3 +2839,23 @@ def upgrade(topic: str, payload) -> Optional[dict]: return {topic: configuration_payload} self._loop_all_received_topics(upgrade) self._append_datastore_version(111) + + def upgrade_datastore_112(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + if re.search("openWB/vehicle/[0-9]+/soc_module/config", topic) is not None: + payload = decode_payload(payload) + if payload.get("type") == "bmwbc": + pub_system_message( + {}, + "Die Schnittstelle des bisherigen BMW-Moduls wurde eingestellt und in openWB entfernt. Bitte " + "beachte, dass Du ohne die Konfiguration eines anderen Fahrzeug-Moduls kein SoC-basiertes " + "Laden nutzen kannst.
Unsere Fahrzeug-Module werden von der Community entwickelt. Wenn du " + "also ein BMW-Fahrer bist und gerne ein neues BMW-Modul in openWB programmieren möchtest, " + "findest Du im Forum " + "weitere Informationen.", + MessageType.INFO, + ) + return {topic: NO_MODULE} + run_command(['pip', 'uninstall', 'bimmer_connected', '-y'], process_exception=True) + self._loop_all_received_topics(upgrade) + self._append_datastore_version(112) diff --git a/packages/modules/vehicles/bmwbc/__init__.py b/packages/modules/vehicles/bmwbc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/modules/vehicles/bmwbc/api.py b/packages/modules/vehicles/bmwbc/api.py deleted file mode 100755 index d117e74972..0000000000 --- a/packages/modules/vehicles/bmwbc/api.py +++ /dev/null @@ -1,407 +0,0 @@ -#!/usr/bin/env python3 - -# references: -# https://github.com/bimmerconnected -# https://bimmer-connected.readthedocs.io/en/latest/ - -from typing import Union -from logging import getLogger, WARNING -from json import load, dump, loads, dumps -from asyncio import new_event_loop, set_event_loop -from datetime import datetime -from copy import deepcopy -from threading import Lock -# from uuid import uuid4 -import os.path -import shutil -import time - -from helpermodules.utils.error_handling import ImportErrorContext -with ImportErrorContext(): - from bimmer_connected.api.client import MyBMWClientConfiguration - from bimmer_connected.api.authentication import MyBMWAuthentication - from bimmer_connected.account import MyBMWAccount - from bimmer_connected.api.regions import Regions - from bimmer_connected.utils import MyBMWJSONEncoder - from bimmer_connected.models import MyBMWAPIError - -from modules.common.component_state import CarState -from modules.common.store import RAMDISK_PATH -from pathlib import Path - -DATA_PATH = Path(__file__).resolve().parents[4] / "data" / "modules" / "bmwbc" -ts_fmt = '%Y-%m-%dT%H:%M:%S' -dt_fmt = '%Y-%m-%d %H:%M:%S' -storeFilePrefix = '/soc_bmwbc_' -storeFileTypeVehicle = 'vh_' -storeFileTypeUserid = 'usr_' -jsonFilePostfix = '.json' -replyFilePrefix = '/soc_bmwbc_reply_vehicle_' - -log = getLogger(__name__) - - -# ------------ Helper functions ------------------------------------- -# send store content n debug level to soc log -def log_store(store: dict, txt: str): - st = deepcopy(store) # create a copy of the store - # st['rt'] = st['refresh_token'] # copy key to avoid REDACTED - # st['at'] = st['access_token'] # copy key to avoid REDACTED - log.debug("store file action:" + txt) - log.debug(txt + ":\n" + dumps(st, indent=4)) - - -# initialize store structures when no store is available -def init_store(): - store = {} - store['refresh_token'] = None - store['access_token'] = None - store['expires_at'] = None - store['gcid'] = None - store['session_id'] = None - store['captcha_token'] = None - log_store(store, "store initialized") - return store - - -# load store from file, if no store file exists initialize store structure -def load_store(user_id: str, vehicle_id: str) -> dict: - try: - # check for user_id storefile - # if not check for vehicle_id storefile - storeFileUserid = str(DATA_PATH) + storeFilePrefix + storeFileTypeUserid + user_id + jsonFilePostfix - storeFileVehicle = str(DATA_PATH) + storeFilePrefix + storeFileTypeVehicle + vehicle_id + jsonFilePostfix - if os.path.isfile(storeFileUserid): - log.debug("load_store: storeFileUserid found") - storeFile = storeFileUserid - elif os.path.isfile(storeFileVehicle): - log.debug("load_store: storeFileUserid not found, storeFileVehicle found") - shutil.copy(storeFileVehicle, storeFileUserid) - storeFile = storeFileUserid - else: - log.warning("no store file found, init store") - store = init_store() - return store - except Exception as e: - log.error("init: loading stored data failed, file: " + - storeFile + ", error=" + str(e)) - store = init_store() - return store - - try: - with open(storeFile, 'r', encoding='utf-8') as tf: - store = load(tf) - log_store(store, "store loaded") - if 'refresh_token' not in store: - store = init_store() - except FileNotFoundError: - log.warning("load_store: store file not found, " + - "full authentication required") - store = init_store() - - except Exception as e: - log.error("init: loading stored data failed, file: " + - storeFile + ", error=" + str(e)) - store = init_store() - - return store - - -# write store file -def write_store(store: dict, user_id: str, vehicle_id: str): - storeFile = str(DATA_PATH) + storeFilePrefix + storeFileTypeUserid + user_id + jsonFilePostfix - with open(storeFile, 'w', encoding='utf-8') as tf: - dump(store, tf, indent=4) - log_store(store, "store written") - - -# write a dict as json file - useful for problem analysis -def dump_json(data: dict, fout: str): - replyFile = str(RAMDISK_PATH) + fout + jsonFilePostfix - with open(replyFile, 'w', encoding='utf-8') as f: - dump(data, f, ensure_ascii=False, indent=4) - - -# Exception class - used in raise -class RequestFailed(Exception): - def __init__(self, m): - self.message = m - - def __str__(self): - return self.message - - -class Api: - # initialize class variables - _instance = None - _store = {} - _auth = {} - _clconf = {} - _account = {} - _last_reload = {} - _login_required = {} - _primary_vehicle_id = {} - _lock = Lock() - - def __new__(self, *args, **kwargs): - if self._instance is None: - with self._lock: - if self._instance is None: - log.debug('# Instantiate api object') - self._instance = object.__new__(self) - self.user_id = None - self.password = None - else: - log.debug('# Reuse api _instance') - else: - log.debug('# Reuse api _instance') - return self._instance - - # --------------- async fetch Function called by fetch_soc ------------------------------------ - async def _fetch_soc(self, - user_id: str, - password: str, - vin: str, - captcha_token: str, - vehicle_id: str) -> Union[int, float]: - - # make sure the path for the store fie exists - try: - log.debug("dataPath=" + str(DATA_PATH)) - DATA_PATH.mkdir(parents=True, exist_ok=True) - except Exception as e: - if user_id not in self._store: - self._store[user_id] = init_store() - raise Exception("init: dataPath creation failed, dataPath: " + - str(DATA_PATH) + ", error=" + str(e)) - - # if self._storeFile is None: - if user_id not in self._primary_vehicle_id: - if captcha_token != "SECONDARY": - self._primary_vehicle_id[user_id] = vehicle_id - else: - log.warning("sequence problem: secondary starts without preceding primary") - raise RequestFailed("sequence problem: secondary starts without preceding primary") - - try: - self._login_required[user_id] = False - - if captcha_token != "SECONDARY": - self._mode = "PRIMARY " - # set logging in httpx to WARNING to prevent unwanted log messages from bimmeer connected - getLogger("httpx").setLevel(WARNING) - - if user_id not in self._store: - self._store[user_id] = load_store(user_id, self._primary_vehicle_id[user_id]) - - # check for captcha_token in store - if 'captcha_token' not in self._store[user_id]: - # captcha token not in self._store - add current captcha_token - log.debug("initialize captcha token in store") - self._store[user_id]['captcha_token'] = captcha_token - write_store(self._store[user_id], user_id, self._primary_vehicle_id[user_id]) - else: - # last used captcha token in store, compare with captcha_token in configuration - if self._store[user_id]['captcha_token'] != captcha_token: - # invalidate current refresh and access token to force new login - self._login_required[user_id] = True - log.info("new captcha token configured - invalidate stored token set") - self._new_captcha = True - self._store[user_id]['expires_at'] = None - self._store[user_id]['access_token'] = None - self._store[user_id]['refresh_token'] = None - self._store[user_id]['session_id'] = None - self._store[user_id]['gcid'] = None - if user_id in self._auth: - self._auth.pop(user_id) - if user_id in self._clconf: - self._clconf.pop(user_id) - if user_id in self._account: - self._account.pop(user_id) - else: - log.debug("captcha token unchanged") - self._new_captcha = False - - if user_id not in self._auth: - if self._store[user_id]['expires_at'] is not None and \ - self._store[user_id]['refresh_token'] is not None and \ - self._store[user_id]['access_token'] is not None: - # authenticate via refresh and access token - # user_id, password are provided in case these are required - log.info("authenticate via current token set") - expires_at = datetime.fromisoformat(self._store[user_id]['expires_at']) - self._auth[user_id] = MyBMWAuthentication( - user_id, - password, - Regions.REST_OF_WORLD, - refresh_token=self._store[user_id]['refresh_token'], - access_token=self._store[user_id]['access_token'], - expires_at=expires_at, - hcaptcha_token=captcha_token) - else: - # no token, authenticate via user_id, password and captcha_token - log.info("authenticate via userid, password, captcha token") - log.debug("# Create _auth instance") - self._auth[user_id] = MyBMWAuthentication(user_id, - password, - Regions.REST_OF_WORLD, - hcaptcha_token=captcha_token) - else: - log.debug("# Reuse _auth instance") - - # set session_id and gcid in _auth to store values - if self._login_required[user_id] is False: - if self._store[user_id]['session_id'] is not None: - self._auth[user_id].session_id = self._store[user_id]['session_id'] - if self._store[user_id]['gcid'] is not None: - self._auth[user_id].gcid = self._store[user_id]['gcid'] - - # instantiate client configuration object is not existent yet - if user_id not in self._clconf: - log.debug("# Create _clconf instance") - self._clconf[user_id] = MyBMWClientConfiguration(self._auth[user_id]) - else: - log.debug("# Reuse _clconf instance") - - # instantiate account object if not existent yet - if user_id not in self._account: - log.debug("# Create _account instance") - # user, password and region already set in BMWAuthentication/ClientConfiguration! - self._account[user_id] = MyBMWAccount(None, None, None, - config=self._clconf[user_id], - hcaptcha_token=captcha_token) - if self._login_required[user_id] is False: - self._account[user_id].set_refresh_token(refresh_token=self._store[user_id]['refresh_token'], - gcid=self._store[user_id]['gcid'], - access_token=self._store[user_id]['access_token'], - session_id=self._store[user_id]['session_id']) - else: - log.debug("# Reuse _account instance") - else: - self._mode = "SECONDARY" - - if self._login_required[user_id]: - log.debug("# before initial login:" + str(self._auth[user_id].expires_at)) - await self._auth[user_id].login() - log.debug("# after initial login:" + str(self._auth[user_id].expires_at)) - self._login_required[user_id] = False - self._last_reload[user_id] = 0 - - # get vehicle list - if last reload is more than 5 min ago - self._now = datetime.timestamp(datetime.now()) - if user_id not in self._last_reload: - self._last_reload[user_id] = 0 - if self._now > self._last_reload[user_id] + 5 * 60: - # self._auth[user_id].session_id = str(uuid4()) # experimental to avoid error 408: reset session_id - # experimental: login when expires_at is reached to force token refresh - if self._store[user_id]['expires_at'] is not None: - expires_at = datetime.fromisoformat(self._store[user_id]['expires_at']) - nowdt = datetime.now(expires_at.tzinfo) - - if nowdt > expires_at: - log.debug("# Proactive login to force refresh token before get_vehicles") - log.debug("# before proactive login:" + str(self._auth[user_id].expires_at)) - await self._auth[user_id].login() - log.debug("# after proactive login:" + str(self._auth[user_id].expires_at)) - - # get vehicle list - needs to be called async - _loop = 5 # 5 retries - while _loop > 0: - _err = 0 - log.info(self._mode + ": reload vehicles data, _loop=" + str(_loop)) - try: - await self._account[user_id].get_vehicles() - except MyBMWAPIError as err: - _err = -1 - if 'Internal Server Error' in str(err): - log.info(self._mode + ": get_vehicles : Internal Server Error (500)") - _err = 500 - elif 'Request Timeout' in str(err): - log.info(self._mode + ": get_vehicles : Request Timeout (408)") - _err = 408 - else: - log.info(self._mode + ": get_vehicles unknown err=" + str(err)) - log.info(self._mode + ": get_vehicles : MyBMWAPIError, _loop/_err=" + - str(_loop) + "/" + str(_err)) - time.sleep(15) # sleep for 15 secs before retry - _loop = _loop - 1 - except Exception as err: - log.error("bmwbc.fetch_soc: get_vehicles Error, vehicle_id: " + - vehicle_id + f" {err=}, {type(err)=}") - raise err - self._last_reload[user_id] = datetime.timestamp(datetime.now()) - if _err == 0: - _loop = 0 - - # get vehicle data for vin - vehicle = self._account[user_id].get_vehicle(vin) - - # get json of vehicle data - resp = dumps(vehicle, cls=MyBMWJSONEncoder, indent=4) - log.debug("bmwbc.fetch_soc: resp=" + resp) - - # vehicle data - json to dict - respd = loads(resp) - state = respd['data']['state'] - - # get soc, range, lastUpdated from vehicle data - soc = int(state['electricChargingState']['chargingLevelPercent']) - range = float(state['electricChargingState']['range']) - lastUpdatedAt = state['lastUpdatedAt'] - if self._new_captcha: - # after new captcha use system timestamp - # soc_tsX = datetime.timestamp(datetime.now()) - # after new captcha use timestamp 0 (19700101) - soc_tsX = 0 - else: - # convert lastUpdatedAt to soc_timestamp - soc_tsdtZ = datetime.strptime(lastUpdatedAt, ts_fmt + "Z") - soc_tsX = datetime.timestamp(soc_tsdtZ) - - # save the vehicle data for further analysis if required - dump_json(respd, replyFilePrefix + vehicle_id) - - log.info(self._mode + " SOC/Range: " + str(soc) + '%/' + str(range) + 'KM@' + lastUpdatedAt) - - # store token and expires_at if changed - expires_at = datetime.isoformat(self._auth[user_id].expires_at) - if self._store[user_id]['expires_at'] != expires_at or \ - self._store[user_id]['session_id'] != self._auth[user_id].session_id or \ - self._store[user_id]['gcid'] != self._auth[user_id].gcid or \ - self._store[user_id]['access_token'] != self._auth[user_id].access_token or \ - self._store[user_id]['refresh_token'] != self._auth[user_id].refresh_token: - self._store[user_id]['refresh_token'] = self._auth[user_id].refresh_token - self._store[user_id]['access_token'] = self._auth[user_id].access_token - if captcha_token != "SECONDARY": - self._store[user_id]['captcha_token'] = captcha_token - self._store[user_id]['session_id'] = self._auth[user_id].session_id - self._store[user_id]['gcid'] = self._auth[user_id].gcid - self._store[user_id]['expires_at'] = datetime.isoformat(self._auth[user_id].expires_at) - write_store(self._store[user_id], user_id, self._primary_vehicle_id[user_id]) - log.debug("# after write_store :" + str(self._auth[user_id].expires_at) + - "/" + self._auth[user_id].refresh_token) - - except Exception as err: - # log.error("bmwbc.fetch_soc: requestData Error, vehicle_id: " + vehicle_id + f" {err=}, {type(err)=}") - log.error("bmwbc.fetch_soc: requestData Error, vehicle_id: " + str(vehicle_id)) - # self._auth = None - self._auth.pop(user_id, None) - self._clconf.pop(user_id, None) - self._account.pop(user_id, None) - raise Exception("SoC Request failed") from err - return soc, range, soc_tsX - - -# main entry - _fetch needs to be run async -def fetch_soc(user_id: str, password: str, vin: str, captcha_token: str, vehicle_id: str) -> CarState: - - # prepare and call async method - loop = new_event_loop() - set_event_loop(loop) - - api = Api() - - # get soc, range, soc_timestamp from server - soc, range, soc_tsX = loop.run_until_complete(api._fetch_soc(user_id, password, vin, captcha_token, vehicle_id)) - - return CarState(soc=soc, range=range, soc_timestamp=soc_tsX) diff --git a/packages/modules/vehicles/bmwbc/config.py b/packages/modules/vehicles/bmwbc/config.py deleted file mode 100755 index db8c4cf5e3..0000000000 --- a/packages/modules/vehicles/bmwbc/config.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Optional - - -class BMWbcConfiguration: - def __init__(self, - user_id: Optional[str] = None, - password: Optional[str] = None, - vin: Optional[str] = None, - captcha_token: Optional[str] = None, - calculate_soc: bool = False): - self.user_id = user_id - self.password = password - self.vin = vin - self.captcha_token = captcha_token - self.calculate_soc = calculate_soc - - -class BMWbc: - def __init__(self, - name: str = "BMW & Mini ", - type: str = "bmwbc", - official: bool = False, - configuration: BMWbcConfiguration = None) -> None: - self.name = name - self.type = type - self.official = official - self.configuration = configuration or BMWbcConfiguration() diff --git a/packages/modules/vehicles/bmwbc/soc.py b/packages/modules/vehicles/bmwbc/soc.py deleted file mode 100755 index 796582f471..0000000000 --- a/packages/modules/vehicles/bmwbc/soc.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import List - -import logging - -from helpermodules.cli import run_using_positional_cli_args -from modules.common import store -from modules.common.abstract_device import DeviceDescriptor -from modules.common.abstract_vehicle import VehicleUpdateData -from modules.common.component_state import CarState -from modules.common.configurable_vehicle import ConfigurableVehicle -from modules.vehicles.bmwbc import api -from modules.vehicles.bmwbc.config import BMWbc, BMWbcConfiguration - - -log = logging.getLogger(__name__) - - -def create_vehicle(vehicle_config: BMWbc, vehicle: int): - def updater(vehicle_update_data: VehicleUpdateData) -> CarState: - return api.fetch_soc( - vehicle_config.configuration.user_id, - vehicle_config.configuration.password, - vehicle_config.configuration.vin, - vehicle_config.configuration.captcha_token, - vehicle) - return ConfigurableVehicle(vehicle_config=vehicle_config, - component_updater=updater, - vehicle=vehicle, - calc_while_charging=vehicle_config.configuration.calculate_soc) - - -def bmwbc_update(user_id: str, password: str, vin: str, captcha_token: str, charge_point: int): - log.debug("bmwbc: user_id="+user_id+"vin="+vin+"charge_point="+str(charge_point)) - log.debug("bmwbc: captcha_token="+captcha_token) - vehicle_config = BMWbc(configuration=BMWbcConfiguration(charge_point, - user_id, - password, - vin, - captcha_token)) - store.get_car_value_store(charge_point).store.set(api.fetch_soc( - vehicle_config.configuration.user_id, - vehicle_config.configuration.password, - vehicle_config.configuration.vin, - vehicle_config.configuration.captcha_token, - charge_point)) - - -def main(argv: List[str]): - run_using_positional_cli_args(bmwbc_update, argv) - - -device_descriptor = DeviceDescriptor(configuration_factory=BMWbc) diff --git a/requirements.txt b/requirements.txt index 140a92330c..9d76bc33ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,6 @@ pysmb==1.2.9.1 pytz==2023.3.post1 grpcio==1.60.1 protobuf==5.29.6 -bimmer_connected==0.17.3 ocpp==1.0.0 websockets==12.0 pycarwings3==0.7.14