Skip to content

Commit

Permalink
Fix cli output and add JSON-only flag
Browse files Browse the repository at this point in the history
  • Loading branch information
rikroe committed Nov 14, 2021
1 parent 969f4fe commit a4db0f6
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 97 deletions.
40 changes: 21 additions & 19 deletions bimmer_connected/charging_profile.py
Expand Up @@ -4,34 +4,34 @@
from typing import TYPE_CHECKING, List
from enum import Enum

from bimmer_connected.const import SERVICE_STATUS
from bimmer_connected.utils import SerializableBaseClass

if TYPE_CHECKING:
from bimmer_connected.vehicle_status import VehicleStatus

_LOGGER = logging.getLogger(__name__)


class ChargingMode(Enum):
class ChargingMode(str, Enum):
"""Charging mode of electric vehicle."""
IMMEDIATE_CHARGING = 'immediateCharging'
DELAYED_CHARGING = 'delayedCharging'


class ChargingPreferences(Enum):
class ChargingPreferences(str, Enum):
"""Charging preferences of electric vehicle."""
NO_PRESELECTION = 'noPreselection'
NO_PRESELECTION = 'noPreSelection'
CHARGING_WINDOW = 'chargingWindow'


class TimerTypes(Enum):
class TimerTypes(str, Enum):
"""Different Timer-Types."""
TWO_WEEKS = 'twoWeeksTimer'
ONE_WEEK = 'weeklyPlanner'
OVERRIDE_TIMER = 'overrideTimer'


class ChargingWindow:
class ChargingWindow(SerializableBaseClass):
"""
This class provides a nicer API than parsing the JSON format directly.
"""
Expand All @@ -58,7 +58,7 @@ def end_time(self) -> str:
)


class DepartureTimer:
class DepartureTimer(SerializableBaseClass):
"""
This class provides a nicer API than parsing the JSON format directly.
"""
Expand All @@ -69,11 +69,13 @@ def __init__(self, timer_dict: dict):
@property
def timer_id(self) -> int:
"""ID of this timer."""
return self._timer_dict["id"]
return self._timer_dict.get("id")

@property
def start_time(self) -> str:
"""Deperture time for this timer."""
if "timeStamp" not in self._timer_dict:
return None
return "{}:{}".format(
str(self._timer_dict["timeStamp"]["hour"]).zfill(2),
str(self._timer_dict["timeStamp"]["minute"]).zfill(2),
Expand All @@ -82,12 +84,12 @@ def start_time(self) -> str:
@property
def action(self) -> bool:
"""What does the timer do."""
return self._timer_dict["action"]
return self._timer_dict.get("action")

@property
def weekdays(self) -> List[str]:
"""Active weekdays for this timer."""
return self._timer_dict["timerWeekDays"]
return self._timer_dict.get("timerWeekDays")


def backend_parameter(func):
Expand All @@ -97,7 +99,7 @@ def backend_parameter(func):
"""
def _func_wrapper(self: 'ChargingProfile', *args, **kwargs):
# pylint: disable=protected-access
if self._charging_profile is None:
if self.charging_profile is None:
raise ValueError('No data available for vehicles charging profile!')
try:
return func(self, *args, **kwargs)
Expand All @@ -109,29 +111,29 @@ def _func_wrapper(self: 'ChargingProfile', *args, **kwargs):
return _func_wrapper


class ChargingProfile: # pylint: disable=too-many-public-methods
class ChargingProfile(SerializableBaseClass): # pylint: disable=too-many-public-methods
"""Models the charging profile of a vehicle."""

def __init__(self, status: "VehicleStatus"):
"""Constructor."""
self._charging_profile = status._state[SERVICE_STATUS]["chargingProfile"]
self.charging_profile = status.status["chargingProfile"]

def __getattr__(self, item):
"""Generic get function for all backend attributes."""
return self._charging_profile[item]
return self.charging_profile[item]

@property
@backend_parameter
def is_pre_entry_climatization_enabled(self) -> bool:
"""Get status of pre-entry climatization."""
return bool(self._charging_profile['climatisationOn'])
return bool(self.charging_profile['climatisationOn'])

@property
@backend_parameter
def timer(self) -> dict:
"""List of timer messages."""
timer_list = {}
for timer_dict in self._charging_profile["departureTimes"]:
for timer_dict in self.charging_profile["departureTimes"]:
curr_timer = DepartureTimer(timer_dict)
timer_list[curr_timer.timer_id] = curr_timer
return timer_list
Expand All @@ -140,16 +142,16 @@ def timer(self) -> dict:
@backend_parameter
def preferred_charging_window(self) -> ChargingWindow:
"""Returns the preferred charging window."""
return ChargingWindow(self._charging_profile['reductionOfChargeCurrent'])
return ChargingWindow(self.charging_profile['reductionOfChargeCurrent'])

@property
@backend_parameter
def charging_preferences(self) -> str:
"""Returns the prefered charging preferences."""
return ChargingPreferences(self._charging_profile['chargingPreference'])
return ChargingPreferences(self.charging_profile['chargingPreference'])

@property
@backend_parameter
def charging_mode(self) -> str:
"""Returns the prefered charging mode."""
return ChargingMode(self._charging_profile['chargingMode'])
return ChargingMode(self.charging_profile['chargingMode'])
31 changes: 20 additions & 11 deletions bimmer_connected/cli.py
Expand Up @@ -3,7 +3,6 @@

import argparse
import logging
import json
import time
import sys
from datetime import datetime
Expand All @@ -15,6 +14,7 @@
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name, valid_regions, get_server_url
from bimmer_connected.vehicle import VehicleViewDirection, HV_BATTERY_DRIVE_TRAINS
from bimmer_connected.utils import to_json

TEXT_VIN = 'Vehicle Identification Number'

Expand All @@ -28,6 +28,10 @@ def main_parser() -> argparse.ArgumentParser:
subparsers.required = True

status_parser = subparsers.add_parser('status', description='Get the current status of the vehicle.')
status_parser.add_argument('-j', '--json',
help='Output as JSON only. Removes all other output.',
action='store_true'
)
_add_default_arguments(status_parser)
_add_position_arguments(status_parser)

Expand Down Expand Up @@ -89,23 +93,28 @@ def main_parser() -> argparse.ArgumentParser:

def get_status(args) -> None:
"""Get the vehicle status."""
if args.json:
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)

account = ConnectedDriveAccount(args.username, args.password, get_region_from_name(args.region))
if args.lat and args.lng:
for vehicle in account.vehicles:
vehicle.set_observer_position(args.lat, args.lng)
account.update_vehicle_states()

print('Found {} vehicles: {}'.format(
len(account.vehicles),
','.join([v.name for v in account.vehicles])))
if args.json:
print(to_json(account.vehicles))
else:
print('Found {} vehicles: {}'.format(
len(account.vehicles),
','.join([v.name for v in account.vehicles])))

for vehicle in account.vehicles:
print('VIN: {}'.format(vehicle.vin))
print('Mileage: {}'.format(vehicle.state.vehicle_status.mileage))
print('Vehicle properties:')
print(json.dumps(vehicle.attributes, indent=4))
print('Vehicle status:')
print(json.dumps(vehicle.state.vehicle_status.attributes, indent=4))
for vehicle in account.vehicles:
print('VIN: {}'.format(vehicle.vin))
print('Mileage: {}'.format(vehicle.state.vehicle_status.mileage))
print('Vehicle data:')
print(to_json(vehicle, indent=4))


def fingerprint(args) -> None:
Expand Down
4 changes: 2 additions & 2 deletions bimmer_connected/remote_services.py
Expand Up @@ -30,7 +30,7 @@
_UPDATE_AFTER_REMOTE_SERVICE_DELAY = 10


class ExecutionState(Enum):
class ExecutionState(str, Enum):
"""Enumeration of possible states of the execution of a remote service."""
INITIATED = 'INITIATED'
PENDING = 'PENDING'
Expand All @@ -39,7 +39,7 @@ class ExecutionState(Enum):
UNKNOWN = 'UNKNOWN'


class _Services(Enum):
class _Services(str, Enum):
"""Enumeration of possible services to be executed."""
REMOTE_LIGHT_FLASH = 'LIGHT_FLASH'
REMOTE_VEHICLE_FINDER = 'VEHICLE_FINDER'
Expand Down
46 changes: 46 additions & 0 deletions bimmer_connected/utils.py
@@ -0,0 +1,46 @@
"""General utils and base classes used in the library."""

from abc import ABC
import inspect
import json


def serialize_for_json(obj: object, excluded: list = None, exclude_hidden: bool = True) -> dict:
"""
Returns all object attributes and properties as dictionary.
:param excluded list: attributes and parameters NOT to export
:param exclude_hidden bool: if true, do not export attributes or parameters starting with '_'
"""
excluded = excluded if excluded else []
return dict(
{
k: v for k, v in obj.__dict__.items()
if k not in excluded and ((exclude_hidden and not str(k).startswith("_")) or not exclude_hidden)
},
**{a: getattr(obj, a) for a in get_class_property_names(obj) if a not in excluded + ["to_json"]}
)


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("_")
]


def to_json(obj: object, *args, **kwargs):
"""Serialize a nested object to json. Tries to call `to_json` attribute on object first."""
def serialize(obj: object):
return getattr(obj, 'to_json', getattr(obj, '__dict__') if hasattr(obj, '__dict__') else str(obj))
return json.dumps(obj, default=serialize, *args, **kwargs)


class SerializableBaseClass(ABC): # pylint: disable=too-few-public-methods
"""Base class to enable json-compatible serialization."""

@property
def to_json(self) -> dict:
"""Return all attributes and parameters."""
return serialize_for_json(self)

0 comments on commit a4db0f6

Please sign in to comment.