Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update timezone handling for time only data #617

Merged
merged 5 commits into from
May 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
pip install -r requirements.txt -r requirements-test.txt
- name: Test with flake8
run: |
ruff --config=pyproject.toml bimmer_connected
ruff check --config=pyproject.toml bimmer_connected

black:
runs-on: ubuntu-latest
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.1.14
rev: v0.4.5
hooks:
- id: ruff
args:
- --fix
- repo: https://github.com/psf/black
rev: 23.12.1
rev: 24.4.2
hooks:
- id: black
args:
- --safe
- --quiet
files: ^(bimmer_connected/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.3.0
hooks:
- id: codespell
args:
Expand All @@ -24,7 +24,7 @@ repos:
exclude_types: [csv, json]
exclude: ^test/responses/
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
rev: v1.10.0
hooks:
- id: mypy
name: mypy
Expand Down
9 changes: 8 additions & 1 deletion bimmer_connected/tests/test_vehicle_status.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Test for VehicleState."""

import datetime
import os
import time

import pytest
import respx
Expand Down Expand Up @@ -180,13 +182,18 @@ async def test_charging_end_time(caplog, bmw_fixture: respx.Router):
@pytest.mark.asyncio
async def test_plugged_in_waiting_for_charge_window(caplog, bmw_fixture: respx.Router):
"""I01_REX is plugged in but not charging, as its waiting for charging window."""

# Make sure that local timezone for test is UTC
os.environ["TZ"] = "Europe/Berlin"
time.tzset()

account = await prepare_account_with_vehicles()
vehicle = account.get_vehicle(VIN_I01_REX)

assert vehicle.fuel_and_battery.charging_end_time is None
assert vehicle.fuel_and_battery.charging_status == ChargingState.WAITING_FOR_CHARGING
assert vehicle.fuel_and_battery.is_charger_connected is True
assert vehicle.fuel_and_battery.charging_start_time == datetime.datetime(2021, 11, 28, 18, 1, tzinfo=UTC)
assert vehicle.fuel_and_battery.charging_start_time == datetime.datetime(2021, 11, 29, 18, 1)
assert vehicle.fuel_and_battery.charging_target == 100

assert len(get_deprecation_warning_count(caplog)) == 0
Expand Down
23 changes: 15 additions & 8 deletions bimmer_connected/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import json
import logging
import pathlib
import time
from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Union

Expand Down Expand Up @@ -37,20 +36,28 @@ def parse_datetime(date_str: str) -> Optional[datetime.datetime]:
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)
parsed = datetime.datetime.strptime(date_str, date_format)
parsed = parsed.replace(microsecond=0)
return parsed
except ValueError:
pass
_LOGGER.error("unable to parse '%s' using %s", date_str, date_formats)
return None


def get_next_occurrence(now: datetime.datetime, time: datetime.time) -> datetime.datetime:
"""Get the next occurrence of a given time."""

# If current time is past the given time, add one day to the current date
if now.time() > time:
next_date = now.date() + datetime.timedelta(days=1)
# If current time is before the given time, use the current date
else:
next_date = now.date()
next_occurrence = datetime.datetime.combine(next_date, time)
return next_occurrence


class MyBMWJSONEncoder(json.JSONEncoder):
"""JSON Encoder that handles data classes, properties and additional data types."""

Expand Down
2 changes: 1 addition & 1 deletion bimmer_connected/vehicle/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def _parse_vehicle_data(cls, vehicle_data: Dict) -> Dict:
retval["activity"] = ClimateActivityState(climate_control_state["activity"])
retval["activity_end_time"] = (
(
datetime.datetime.now(datetime.timezone.utc)
vehicle_data["fetched_at"]
+ datetime.timedelta(seconds=int(climate_control_state["remainingSeconds"]))
)
if "remainingSeconds" in climate_control_state
Expand Down
8 changes: 4 additions & 4 deletions bimmer_connected/vehicle/fuel_and_battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from bimmer_connected.const import ATTR_ATTRIBUTES, ATTR_STATE
from bimmer_connected.models import StrEnum, ValueWithUnit, VehicleDataBase
from bimmer_connected.utils import get_next_occurrence
from bimmer_connected.vehicle.const import COMBUSTION_ENGINE_DRIVE_TRAINS, HV_BATTERY_DRIVE_TRAINS, DriveTrainType

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -56,7 +57,7 @@ class FuelAndBattery(VehicleDataBase):
"""Charging state of the vehicle."""

charging_start_time: Optional[datetime.datetime] = None
"""The planned time the vehicle will start charging in UTC."""
"""The planned time the vehicle will start charging in local time."""

charging_end_time: Optional[datetime.datetime] = None
"""The estimated time the vehicle will have finished charging."""
Expand Down Expand Up @@ -165,10 +166,9 @@ def _parse_electric_data(
retval["charging_target"] = int(electric_data["chargingTarget"])

if retval["charging_status"] == ChargingState.WAITING_FOR_CHARGING and isinstance(charging_window, Dict):
retval["charging_start_time"] = datetime.datetime.combine(
datetime.datetime.now(datetime.timezone.utc).date(),
retval["charging_start_time"] = get_next_occurrence(
datetime.datetime.now(),
datetime.time(int(charging_window["start"]["hour"]), int(charging_window["start"]["minute"])),
tzinfo=datetime.timezone.utc,
)

return retval
4 changes: 2 additions & 2 deletions bimmer_connected/vehicle/vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def update_state(
vehicle_data = self.combine_data(data, fetched_at)
self.data = vehicle_data

update_entities: List[Tuple[Type["VehicleDataBase"], str]] = [
update_entities: List[Tuple[Type[VehicleDataBase], str]] = [
(FuelAndBattery, "fuel_and_battery"),
(VehicleLocation, "vehicle_location"),
(DoorsAndWindows, "doors_and_windows"),
Expand All @@ -162,7 +162,7 @@ def update_state(
if getattr(self, vehicle_attribute) is None:
setattr(self, vehicle_attribute, cls.from_vehicle_data(vehicle_data))
else:
curr_attr: "VehicleDataBase" = getattr(self, vehicle_attribute)
curr_attr: VehicleDataBase = getattr(self, vehicle_attribute)
curr_attr.update_from_vehicle_data(vehicle_data)
except (KeyError, TypeError) as ex:
_LOGGER.warning("Unable to update %s - (%s) %s", vehicle_attribute, type(ex).__name__, ex)
Expand Down
11 changes: 6 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ enable_error_code = "ignore-without-code"
target-version = "py38"
line-length = 120

exclude = [
"bimmer_connected/coord_convert.py",
]

[tool.ruff.lint]
select = [
"C", # complexity
"D", # docstrings
Expand All @@ -37,13 +42,9 @@ ignore = [
"D107", # Missing docstring in `__init__`
]

exclude = [
"bimmer_connected/coord_convert.py",
]

[tool.ruff.lint.per-file-ignores]
"docs/source/conf.py" = ["D100"]
"bimmer_connected/api/authentication.py" = ["D102", "D107"]

[tool.ruff.mccabe]
[tool.ruff.lint.mccabe]
max-complexity = 25
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Python package description."""

from setuptools import setup

setup(
Expand Down
Loading