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