Source code for bimmer_connected.utils

"""General utils and base classes used in the library."""

import datetime
import inspect
import json
import logging
import time
import traceback
from enum import Enum
from typing import TYPE_CHECKING, Dict, Optional

if TYPE_CHECKING:
    from typing import Callable, TypeVar

    from typing_extensions import ParamSpec

    _T = TypeVar("_T")
    _R = TypeVar("_R")
    _P = ParamSpec("_P")
_LOGGER = logging.getLogger(__name__)


JSON_IGNORED_KEYS = ["account", "_account", "vehicle", "_vehicle", "status", "remote_services"]
JSON_DEPRECATED_KEYS = [
    "has_hv_battery",
    "has_range_extender",
    "has_internal_combustion_engine",
    "has_weekly_planner_service",
]


[docs]def get_class_property_names(obj: object): """Returns the names of all properties of a class.""" return [p[0] for p in inspect.getmembers(type(obj), inspect.isdatadescriptor) if not p[0].startswith("_")]
[docs]def parse_datetime(date_str: str) -> Optional[datetime.datetime]: """Convert a time string into datetime.""" if not date_str: return None date_formats = ["%Y-%m-%dT%H:%M:%S.%f%z", "%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"] for date_format in date_formats: try: # Parse datetimes using `time.strptime` to allow running in some embedded python interpreters. # https://bugs.python.org/issue27400 time_struct = time.strptime(date_str, date_format) parsed = datetime.datetime(*(time_struct[0:6])) if time_struct.tm_gmtoff and time_struct.tm_gmtoff != 0: parsed = parsed - datetime.timedelta(seconds=time_struct.tm_gmtoff) parsed = parsed.replace(tzinfo=datetime.timezone.utc) return parsed except ValueError: pass _LOGGER.error("unable to parse '%s' using %s", date_str, date_formats) return None
[docs]class MyBMWJSONEncoder(json.JSONEncoder): """JSON Encoder that handles data classes, properties and additional data types."""
[docs] def default(self, o): if isinstance(o, (datetime.datetime, datetime.date, datetime.time)): return o.isoformat() if not isinstance(o, Enum) and hasattr(o, "__dict__") and isinstance(o.__dict__, Dict): retval: Dict = o.__dict__ retval.update({p: getattr(o, p) for p in get_class_property_names(o) if p not in JSON_DEPRECATED_KEYS}) return {k: v for k, v in retval.items() if k not in JSON_IGNORED_KEYS} return str(o)
[docs]def deprecated(replacement: Optional[str] = None) -> "Callable[[Callable[_P, _R]], Callable[_P, _R | None]]": """Mark a function or property as deprecated.""" def decorator(func: "Callable[_P, _R]") -> "Callable[_P, _R | None]": def _func_wrapper(*args: "_P.args", **kwargs: "_P.kwargs") -> "_R | None": replacement_text = f" Please change to '{replacement}'." if replacement else "" stack = traceback.extract_stack()[-2] _LOGGER.warning( "DeprecationWarning:%s:%s: '%s' is deprecated.%s", stack.filename, stack.lineno, func.__qualname__, replacement_text, ) return func(*args, **kwargs) return _func_wrapper return decorator
[docs]def to_camel_case(input_str: str) -> str: """Converts SNAKE_CASE or snake_case to camelCase.""" retval = "" flag_upper = False for curr in input_str.lower(): if not curr.isalnum(): if curr == "_": flag_upper = True continue retval = retval + (curr.upper() if flag_upper else curr) flag_upper = False return retval