Skip to content
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
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
59 changes: 57 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ ruff = "*"
types-requests-oauthlib = "1.3"
types-pytz = "^2024.1.0.20240203"
types-click = "^7.1.8"
pytest-freezegun = "^0.4.2"

[tool.poetry.extras]
docs = ["sphinx", "sphinx-rtd-theme", "sphinx-github-changelog"]
Expand Down
16 changes: 2 additions & 14 deletions ring_doorbell/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from __future__ import annotations

import logging
from datetime import datetime
from typing import TYPE_CHECKING, Any

import pytz

from ring_doorbell.const import URL_DOORBELL_HISTORY, RingCapability
from ring_doorbell.util import parse_datetime

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -204,23 +204,11 @@ def history( # noqa: C901, PLR0912, PLR0913

if convert_timezone:
# convert for specific timezone
utc = pytz.utc
if timezone:
mytz = pytz.timezone(timezone)

for entry in response:
dt_at = datetime.strptime(
entry["created_at"], "%Y-%m-%dT%H:%M:%S.%f%z"
)
utc_dt = datetime(
dt_at.year,
dt_at.month,
dt_at.day,
dt_at.hour,
dt_at.minute,
dt_at.second,
tzinfo=utc,
)
utc_dt = parse_datetime(entry["created_at"])
if timezone:
tz_dt = utc_dt.astimezone(mytz)
entry["created_at"] = tz_dt
Expand Down
6 changes: 2 additions & 4 deletions ring_doorbell/listen/eventlistener.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import json
import logging
import time
from datetime import datetime
from typing import TYPE_CHECKING, Any, Callable, Dict

from firebase_messaging import FcmPushClient
Expand All @@ -25,6 +24,7 @@
)
from ring_doorbell.event import RingEvent
from ring_doorbell.exceptions import RingError
from ring_doorbell.util import parse_datetime

from .listenerconfig import RingEventListenerConfig

Expand Down Expand Up @@ -203,9 +203,7 @@ def _get_ding_event(self, gcm_data: dict[str, Any]) -> RingEvent:
state = subtype

created_at = ding["created_at"]
create_seconds = (
datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%f%z")
).timestamp()
create_seconds = parse_datetime(created_at).timestamp()
return RingEvent(
id=ding["id"],
kind=kind,
Expand Down
32 changes: 32 additions & 0 deletions ring_doorbell/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Module for common utility functions."""

import datetime
import logging

_logger = logging.getLogger(__name__)


def parse_datetime(datetime_str: str) -> datetime.datetime:
"""Parse a datetime string into a datetime object.

Ring api has inconsistent datetime string patterns.
"""
# Check if the datetime string contains a period which precedes 'Z',
# indicating microseconds
if "." in datetime_str and datetime_str.endswith("Z"):
# String contains microseconds and ends with 'Z'
format_str = "%Y-%m-%dT%H:%M:%S.%fZ"
else:
# String does not contain microseconds, should end with 'Z'
# Could be updated to handle other formats
format_str = "%Y-%m-%dT%H:%M:%SZ"
try:
res = datetime.datetime.strptime(datetime_str, format_str).replace(
tzinfo=datetime.timezone.utc
)
except ValueError:
_logger.exception(
"Unable to parse datetime string %s, defaulting to now time", datetime_str
)
res = datetime.datetime.now(datetime.timezone.utc)
return res
47 changes: 47 additions & 0 deletions tests/test_ring.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"""The tests for the Ring platform."""

from datetime import datetime, timezone

import pytest
from freezegun import freeze_time
from ring_doorbell import RingError
from ring_doorbell.util import parse_datetime


def test_basic_attributes(ring):
Expand Down Expand Up @@ -165,3 +169,46 @@ def test_motion_detection_enable(ring, requests_mock):

assert len(active_dings) == 3
assert len(ring.active_alerts()) == 3


@pytest.mark.parametrize(
("datetime_string", "expected", "error_in_log"),
[
pytest.param(
"2012-01-15T06:01:01",
datetime(2012, 1, 14, 5 - 4, 5, 5, 123 * 1_000, tzinfo=timezone.utc),
True,
id="No timezone",
),
pytest.param(
"2012-01-15T06:01:01.12Z",
datetime(2012, 1, 15, 6, 1, 1, 120 * 1_000, tzinfo=timezone.utc),
False,
id="Millis",
),
pytest.param(
"2012-01-15T06:01:01.123456Z",
datetime(2012, 1, 15, 6, 1, 1, 123456, tzinfo=timezone.utc),
False,
id="Micros",
),
pytest.param(
"2012-01-15T06:01:01Z",
datetime(2012, 1, 15, 6, 1, 1, 0, tzinfo=timezone.utc),
False,
id="No millis",
),
],
)
@freeze_time("2012-01-14T05:05:05.123", tz_offset=-4)
def test_datetime_parse(
caplog: pytest.LogCaptureFixture, datetime_string, expected, error_in_log
):
"""Test the datetime parsing."""
dt = parse_datetime(datetime_string)
is_error_in_log = (
f"Unable to parse datetime string {datetime_string}, defaulting to now time"
in caplog.text
)
assert dt == expected
assert is_error_in_log is error_in_log