wwvb 5.0.2__tar.gz → 6.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wwvb-5.0.2 → wwvb-6.0.0}/.github/workflows/test.yml +31 -5
- {wwvb-5.0.2 → wwvb-6.0.0}/.pre-commit-config.yaml +1 -1
- wwvb-6.0.0/.readthedocs.yaml +17 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/Makefile +1 -1
- {wwvb-5.0.2/src/wwvb.egg-info → wwvb-6.0.0}/PKG-INFO +6 -8
- {wwvb-5.0.2 → wwvb-6.0.0}/README.md +5 -7
- {wwvb-5.0.2 → wwvb-6.0.0}/doc/conf.py +5 -0
- wwvb-6.0.0/doc/index.rst +24 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/pyproject.toml +3 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/requirements-dev.txt +2 -1
- {wwvb-5.0.2 → wwvb-6.0.0}/src/uwwvb.py +5 -2
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/__init__.py +91 -36
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/__version__.py +2 -2
- wwvb-6.0.0/src/wwvb/iersdata.json +1 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/wwvbtk.py +16 -18
- {wwvb-5.0.2 → wwvb-6.0.0/src/wwvb.egg-info}/PKG-INFO +6 -8
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/SOURCES.txt +1 -3
- {wwvb-5.0.2 → wwvb-6.0.0}/test/testuwwvb.py +1 -1
- {wwvb-5.0.2 → wwvb-6.0.0}/test/testwwvb.py +32 -1
- wwvb-5.0.2/.github/workflows/codeql.yml +0 -49
- wwvb-5.0.2/LICENSES/Apache-2.0.txt +0 -73
- wwvb-5.0.2/adafruit_datetime.pyi +0 -416
- wwvb-5.0.2/doc/index.rst +0 -31
- wwvb-5.0.2/src/wwvb/iersdata.json +0 -1
- {wwvb-5.0.2 → wwvb-6.0.0}/.coveragerc +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/.github/workflows/cron.yml +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/.github/workflows/release.yml +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/.gitignore +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/LICENSES/CC0-1.0.txt +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/LICENSES/GPL-3.0-only.txt +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/LICENSES/Unlicense.txt +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/codecov.yml +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/doc/_static/.empty +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/requirements.txt +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/setup.cfg +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/decode.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/dut1table.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/gen.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/iersdata.json.license +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/iersdata.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/py.typed +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/tz.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb/updateiers.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/dependency_links.txt +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/entry_points.txt +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/requires.txt +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/src/wwvb.egg-info/top_level.txt +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/testcli.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/testdaylight.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/testls.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/testpm.py +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/1998leapsecond +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/2012leapsecond +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/all-headers +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/bar +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/both +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/cradek +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/duration +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/enddst-phase +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/enddst-phase-2 +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/endleapyear +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/leapday1 +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/leapday28 +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/leapday29 +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/negleapsecond +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/nextdst +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/nextst +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/nonleapday1 +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/nonleapday28 +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/phase +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startdst +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startdst-phase +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startdst-phase-2 +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startleapyear +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/startst +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/y2k +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/y2k-1 +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/y2k1 +0 -0
- {wwvb-5.0.2 → wwvb-6.0.0}/test/wwvbgen_testcases/y2k1-1 +0 -0
@@ -31,6 +31,35 @@ jobs:
|
|
31
31
|
- name: Build HTML docs
|
32
32
|
run: make html
|
33
33
|
|
34
|
+
typing:
|
35
|
+
strategy:
|
36
|
+
fail-fast: false
|
37
|
+
matrix:
|
38
|
+
python-version:
|
39
|
+
- '3.13'
|
40
|
+
os-version:
|
41
|
+
- 'ubuntu-latest'
|
42
|
+
runs-on: ${{ matrix.os-version }}
|
43
|
+
steps:
|
44
|
+
- uses: actions/checkout@v4
|
45
|
+
with:
|
46
|
+
persist-credentials: false
|
47
|
+
|
48
|
+
- name: Set up Python
|
49
|
+
uses: actions/setup-python@v5
|
50
|
+
with:
|
51
|
+
python-version: ${{ matrix.python-version }}
|
52
|
+
|
53
|
+
- name: Install deps
|
54
|
+
run: |
|
55
|
+
python -mpip install wheel
|
56
|
+
python -mpip install -r requirements-dev.txt
|
57
|
+
|
58
|
+
- name: Check stubs
|
59
|
+
if: (! startsWith(matrix.python-version, 'pypy-'))
|
60
|
+
run: make mypy PYTHON=python
|
61
|
+
|
62
|
+
|
34
63
|
test:
|
35
64
|
strategy:
|
36
65
|
fail-fast: false
|
@@ -40,7 +69,8 @@ jobs:
|
|
40
69
|
- '3.10'
|
41
70
|
- '3.11'
|
42
71
|
- '3.12'
|
43
|
-
- '3.13
|
72
|
+
- '3.13'
|
73
|
+
- '3.14.0-alpha.0 - 3.14'
|
44
74
|
os-version:
|
45
75
|
- 'ubuntu-latest'
|
46
76
|
include:
|
@@ -67,10 +97,6 @@ jobs:
|
|
67
97
|
python -mpip install wheel
|
68
98
|
python -mpip install -r requirements-dev.txt
|
69
99
|
|
70
|
-
- name: Check stubs
|
71
|
-
if: (! startsWith(matrix.python-version, 'pypy-'))
|
72
|
-
run: make mypy PYTHON=python
|
73
|
-
|
74
100
|
- name: Coverage
|
75
101
|
run: make coverage PYTHON=python
|
76
102
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Jeff Epler
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
4
|
+
|
5
|
+
version: 2
|
6
|
+
|
7
|
+
build:
|
8
|
+
os: ubuntu-lts-latest
|
9
|
+
tools:
|
10
|
+
python: "3"
|
11
|
+
|
12
|
+
sphinx:
|
13
|
+
configuration: doc/conf.py
|
14
|
+
|
15
|
+
python:
|
16
|
+
install:
|
17
|
+
- requirements: requirements-dev.txt
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: wwvb
|
3
|
-
Version:
|
3
|
+
Version: 6.0.0
|
4
4
|
Summary: Generate WWVB timecodes for any desired time
|
5
5
|
Author-email: Jeff Epler <jepler@gmail.com>
|
6
6
|
Project-URL: Source, https://github.com/jepler/wwvbpy
|
@@ -33,13 +33,11 @@ SPDX-License-Identifier: GPL-3.0-only
|
|
33
33
|
[](https://codecov.io/gh/jepler/wwvbpy)
|
34
34
|
[](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
|
35
35
|
[](https://pypi.org/project/wwvb)
|
36
|
-
[](https://github.com/jepler/wwvbpy/actions/workflows/codeql.yml)
|
37
36
|
[](https://results.pre-commit.ci/latest/github/jepler/wwvbpy/main)
|
38
37
|
|
39
38
|
# Purpose
|
40
39
|
|
41
|
-
|
42
|
-
may be useful in testing WWVB decoder software.
|
40
|
+
Python package and command line programs for interacting with WWVB timecodes.
|
43
41
|
|
44
42
|
Where possible, wwvbpy uses existing facilities for calendar and time
|
45
43
|
manipulation (datetime and dateutil).
|
@@ -65,7 +63,7 @@ The package includes:
|
|
65
63
|
|
66
64
|
# Development status
|
67
65
|
|
68
|
-
The author (@jepler) occasionally develops and maintains this project, but
|
66
|
+
The author ([@jepler](https://github.com/jepler)) occasionally develops and maintains this project, but
|
69
67
|
issues are not likely to be acted on. They would be interested in adding
|
70
68
|
co-maintainer(s).
|
71
69
|
|
@@ -93,7 +91,7 @@ channel.
|
|
93
91
|
# Usage
|
94
92
|
|
95
93
|
~~~~
|
96
|
-
Usage:
|
94
|
+
Usage: wwvbgen [OPTIONS] [TIMESPEC]...
|
97
95
|
|
98
96
|
Generate WWVB timecodes
|
99
97
|
|
@@ -123,7 +121,7 @@ Options:
|
|
123
121
|
|
124
122
|
For example, to display the leap second that occurred at the end of 1998,
|
125
123
|
~~~~
|
126
|
-
$
|
124
|
+
$ wwvbgen -m 7 1998 365 23 56
|
127
125
|
WWVB timecode: year=98 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1
|
128
126
|
'98+365 23:56 210100110200100001120011001102010100010200110100121000001002
|
129
127
|
'98+365 23:57 210100111200100001120011001102010100010200110100121000001002
|
@@ -145,7 +143,7 @@ The letters `a` through `u` represent offsets of -1.0s through +1.0s
|
|
145
143
|
in 0.1s increments; `k` represents 0s. (In practice, only a smaller range
|
146
144
|
of values, typically -0.7s to +0.8s, is seen)
|
147
145
|
|
148
|
-
For 2001 through
|
146
|
+
For 2001 through 2024, NIST has published the actual DUT1 values broadcast,
|
149
147
|
and the date of each change, though it in the format of an HTML
|
150
148
|
table and not designed for machine readability:
|
151
149
|
|
@@ -7,13 +7,11 @@ SPDX-License-Identifier: GPL-3.0-only
|
|
7
7
|
[](https://codecov.io/gh/jepler/wwvbpy)
|
8
8
|
[](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
|
9
9
|
[](https://pypi.org/project/wwvb)
|
10
|
-
[](https://github.com/jepler/wwvbpy/actions/workflows/codeql.yml)
|
11
10
|
[](https://results.pre-commit.ci/latest/github/jepler/wwvbpy/main)
|
12
11
|
|
13
12
|
# Purpose
|
14
13
|
|
15
|
-
|
16
|
-
may be useful in testing WWVB decoder software.
|
14
|
+
Python package and command line programs for interacting with WWVB timecodes.
|
17
15
|
|
18
16
|
Where possible, wwvbpy uses existing facilities for calendar and time
|
19
17
|
manipulation (datetime and dateutil).
|
@@ -39,7 +37,7 @@ The package includes:
|
|
39
37
|
|
40
38
|
# Development status
|
41
39
|
|
42
|
-
The author (@jepler) occasionally develops and maintains this project, but
|
40
|
+
The author ([@jepler](https://github.com/jepler)) occasionally develops and maintains this project, but
|
43
41
|
issues are not likely to be acted on. They would be interested in adding
|
44
42
|
co-maintainer(s).
|
45
43
|
|
@@ -67,7 +65,7 @@ channel.
|
|
67
65
|
# Usage
|
68
66
|
|
69
67
|
~~~~
|
70
|
-
Usage:
|
68
|
+
Usage: wwvbgen [OPTIONS] [TIMESPEC]...
|
71
69
|
|
72
70
|
Generate WWVB timecodes
|
73
71
|
|
@@ -97,7 +95,7 @@ Options:
|
|
97
95
|
|
98
96
|
For example, to display the leap second that occurred at the end of 1998,
|
99
97
|
~~~~
|
100
|
-
$
|
98
|
+
$ wwvbgen -m 7 1998 365 23 56
|
101
99
|
WWVB timecode: year=98 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1
|
102
100
|
'98+365 23:56 210100110200100001120011001102010100010200110100121000001002
|
103
101
|
'98+365 23:57 210100111200100001120011001102010100010200110100121000001002
|
@@ -119,7 +117,7 @@ The letters `a` through `u` represent offsets of -1.0s through +1.0s
|
|
119
117
|
in 0.1s increments; `k` represents 0s. (In practice, only a smaller range
|
120
118
|
of values, typically -0.7s to +0.8s, is seen)
|
121
119
|
|
122
|
-
For 2001 through
|
120
|
+
For 2001 through 2024, NIST has published the actual DUT1 values broadcast,
|
123
121
|
and the date of each change, though it in the format of an HTML
|
124
122
|
table and not designed for machine readability:
|
125
123
|
|
@@ -56,6 +56,7 @@ version = release = final_version
|
|
56
56
|
# ones.
|
57
57
|
extensions = [
|
58
58
|
"sphinx.ext.autodoc",
|
59
|
+
"sphinx_mdinclude",
|
59
60
|
]
|
60
61
|
|
61
62
|
# Add any paths that contain templates here, relative to this directory.
|
@@ -82,6 +83,10 @@ html_static_path = ["_static"]
|
|
82
83
|
autodoc_typehints = "description"
|
83
84
|
autodoc_class_signature = "separated"
|
84
85
|
|
86
|
+
default_role = "any"
|
87
|
+
|
88
|
+
intersphinx_mapping = {'py': ('https://docs.python.org/3', None)}
|
89
|
+
|
85
90
|
# SPDX-FileCopyrightText: 2021-2024 Jeff Epler
|
86
91
|
#
|
87
92
|
# SPDX-License-Identifier: GPL-3.0-only
|
wwvb-6.0.0/doc/index.rst
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
.. SPDX-FileCopyrightText: 2021-2024 Jeff Epler
|
2
|
+
..
|
3
|
+
.. SPDX-License-Identifier: GPL-3.0-only
|
4
|
+
|
5
|
+
wwvbpy |version|
|
6
|
+
================
|
7
|
+
|
8
|
+
.. mdinclude:: ../README.md
|
9
|
+
|
10
|
+
.. toctree::
|
11
|
+
:maxdepth: 2
|
12
|
+
:caption: Contents:
|
13
|
+
|
14
|
+
wwvb module
|
15
|
+
===========
|
16
|
+
|
17
|
+
.. automodule:: wwvb
|
18
|
+
:members:
|
19
|
+
|
20
|
+
uwwvb module
|
21
|
+
============
|
22
|
+
|
23
|
+
.. automodule:: uwwvb
|
24
|
+
:members:
|
@@ -14,9 +14,10 @@ pre-commit
|
|
14
14
|
python-dateutil
|
15
15
|
requests; implementation_name=="cpython"
|
16
16
|
setuptools>=68; implementation_name=="cpython"
|
17
|
-
sphinx
|
17
|
+
sphinx
|
18
18
|
sphinx-autodoc-typehints
|
19
19
|
sphinx-rtd-theme
|
20
|
+
sphinx-mdinclude
|
20
21
|
twine; implementation_name=="cpython"
|
21
22
|
types-beautifulsoup4; implementation_name=="cpython"
|
22
23
|
types-python-dateutil; implementation_name=="cpython"
|
@@ -2,9 +2,12 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-only
|
4
4
|
|
5
|
-
# ruff: noqa: C405 PYI024
|
5
|
+
# ruff: noqa: C405, PYI024, FBT001, FBT002
|
6
6
|
|
7
|
-
"""Implementation of a WWVB state machine & decoder for resource-constrained systems
|
7
|
+
"""Implementation of a WWVB state machine & decoder for resource-constrained systems
|
8
|
+
|
9
|
+
This version is intended for use with MicroPython & CircuitPython.
|
10
|
+
"""
|
8
11
|
|
9
12
|
from __future__ import annotations
|
10
13
|
|
@@ -1,5 +1,13 @@
|
|
1
1
|
#!/usr/bin/python3
|
2
|
-
"""A
|
2
|
+
"""A package and CLI for WWVB timecodes
|
3
|
+
|
4
|
+
This is the full featured library suitable for use on 'real computers'.
|
5
|
+
For a reduced version suitable for use on MicroPython & CircuitPython,
|
6
|
+
see `uwwvb`.
|
7
|
+
|
8
|
+
This package also includes the commandline programs listed above,
|
9
|
+
perhaps most importantly ``wwvbgen`` for generating WWVB timecodes.
|
10
|
+
"""
|
3
11
|
|
4
12
|
# SPDX-FileCopyrightText: 2011-2024 Jeff Epler
|
5
13
|
#
|
@@ -24,12 +32,6 @@ SECOND = datetime.timedelta(seconds=1)
|
|
24
32
|
T = TypeVar("T")
|
25
33
|
|
26
34
|
|
27
|
-
def _require(x: T | None) -> T:
|
28
|
-
"""Check an Optional item is not None."""
|
29
|
-
assert x is not None
|
30
|
-
return x
|
31
|
-
|
32
|
-
|
33
35
|
def _removeprefix(s: str, p: str) -> str:
|
34
36
|
if s.startswith(p):
|
35
37
|
return s[len(p) :]
|
@@ -323,11 +325,22 @@ _dst_ls_lut = [
|
|
323
325
|
]
|
324
326
|
|
325
327
|
|
326
|
-
|
327
|
-
|
328
|
+
@enum.unique
|
329
|
+
class DstStatus(enum.IntEnum):
|
330
|
+
"""Constants that describe the DST status of a minute"""
|
328
331
|
|
329
|
-
|
330
|
-
"""
|
332
|
+
DST_NOT_IN_EFFECT = 0b00
|
333
|
+
"""DST not in effect today"""
|
334
|
+
DST_STARTS_TODAY = 0b01
|
335
|
+
"""DST starts today at 0200 local standard time"""
|
336
|
+
DST_ENDS_TODAY = 0b10
|
337
|
+
"""DST ends today at 0200 local standard time"""
|
338
|
+
DST_IN_EFFECT = 0b11
|
339
|
+
"""DST in effect all day today"""
|
340
|
+
|
341
|
+
|
342
|
+
class _WWVBMinute(NamedTuple):
|
343
|
+
"""(implementation detail)"""
|
331
344
|
|
332
345
|
year: int
|
333
346
|
"""2-digit year within the WWVB epoch"""
|
@@ -341,7 +354,7 @@ class _WWVBMinute(NamedTuple):
|
|
341
354
|
min: int
|
342
355
|
"""Minute of hour"""
|
343
356
|
|
344
|
-
dst:
|
357
|
+
dst: DstStatus
|
345
358
|
"""2-bit DST code """
|
346
359
|
|
347
360
|
ut1: int
|
@@ -357,7 +370,8 @@ class _WWVBMinute(NamedTuple):
|
|
357
370
|
class WWVBMinute(_WWVBMinute):
|
358
371
|
"""Uniquely identifies a minute of time in the WWVB system.
|
359
372
|
|
360
|
-
To use ut1 and ls information from IERS, create a WWVBMinuteIERS
|
373
|
+
To use ``ut1`` and ``ls`` information from IERS, create a `WWVBMinuteIERS`
|
374
|
+
object instead.
|
361
375
|
"""
|
362
376
|
|
363
377
|
epoch: int = 1970
|
@@ -368,16 +382,26 @@ class WWVBMinute(_WWVBMinute):
|
|
368
382
|
days: int,
|
369
383
|
hour: int,
|
370
384
|
minute: int,
|
371
|
-
dst: int | None = None,
|
385
|
+
dst: DstStatus | int | None = None,
|
372
386
|
ut1: int | None = None,
|
387
|
+
*,
|
373
388
|
ls: bool | None = None,
|
374
389
|
ly: bool | None = None,
|
375
390
|
) -> WWVBMinute:
|
376
|
-
"""Construct a WWVBMinute
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
391
|
+
"""Construct a WWVBMinute
|
392
|
+
|
393
|
+
:param year: The 2- or 4-digit year. This parameter is converted by the `full_year` method.
|
394
|
+
:param days: 1-based day of year
|
395
|
+
|
396
|
+
:param hour: UTC hour of day
|
397
|
+
|
398
|
+
:param minute: Minute of hour
|
399
|
+
:param dst: 2-bit DST code
|
400
|
+
:param ut1: UT1 offset in units of 100ms, range -900 to +900ms
|
401
|
+
:param ls: Leap second warning flag
|
402
|
+
:param ly: Leap year flag
|
403
|
+
"""
|
404
|
+
dst = cls.get_dst(year, days) if dst is None else DstStatus(dst)
|
381
405
|
if ut1 is None and ls is None:
|
382
406
|
ut1, ls = cls._get_dut1_info(year, days)
|
383
407
|
elif ut1 is None or ls is None:
|
@@ -407,13 +431,13 @@ class WWVBMinute(_WWVBMinute):
|
|
407
431
|
return year
|
408
432
|
|
409
433
|
@staticmethod
|
410
|
-
def get_dst(year: int, days: int) ->
|
434
|
+
def get_dst(year: int, days: int) -> DstStatus:
|
411
435
|
"""Get the 2-bit WWVB DST value for the given day"""
|
412
436
|
d0 = datetime.datetime(year, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(days - 1)
|
413
437
|
d1 = d0 + datetime.timedelta(1)
|
414
438
|
dst0 = isdst(d0)
|
415
439
|
dst1 = isdst(d1)
|
416
|
-
return dst1 * 2 + dst0
|
440
|
+
return DstStatus(dst1 * 2 + dst0)
|
417
441
|
|
418
442
|
def __str__(self) -> str:
|
419
443
|
"""Implement str()"""
|
@@ -424,7 +448,10 @@ class WWVBMinute(_WWVBMinute):
|
|
424
448
|
)
|
425
449
|
|
426
450
|
def as_datetime_utc(self) -> datetime.datetime:
|
427
|
-
"""Convert to a UTC datetime
|
451
|
+
"""Convert to a UTC datetime
|
452
|
+
|
453
|
+
The returned object has ``tzinfo=datetime.timezone.utc``.
|
454
|
+
"""
|
428
455
|
d = datetime.datetime(self.year, 1, 1, tzinfo=datetime.timezone.utc)
|
429
456
|
d += datetime.timedelta(self.days - 1, self.hour * 3600 + self.min * 60)
|
430
457
|
return d
|
@@ -437,7 +464,18 @@ class WWVBMinute(_WWVBMinute):
|
|
437
464
|
*,
|
438
465
|
dst_observed: bool = True,
|
439
466
|
) -> datetime.datetime:
|
440
|
-
"""Convert to a local datetime according to the DST bits
|
467
|
+
"""Convert to a local datetime according to the DST bits
|
468
|
+
|
469
|
+
The returned object has ``tz=datetime.timezone(computed_offset)``.
|
470
|
+
|
471
|
+
:param standard_time_offset: The UTC offset of local standard time, in seconds west of UTC.
|
472
|
+
The default value, ``7 * 3600``, is for Colorado, the source of the WWVB broadcast.
|
473
|
+
|
474
|
+
:param dst_observed: If ``True`` then the locale observes DST, and a
|
475
|
+
one hour offset is applied according to WWVB rules. If ``False``, then
|
476
|
+
the standard time offset is used at all times.
|
477
|
+
|
478
|
+
"""
|
441
479
|
u = self.as_datetime_utc()
|
442
480
|
offset = datetime.timedelta(seconds=-standard_time_offset)
|
443
481
|
d = u - datetime.timedelta(seconds=standard_time_offset)
|
@@ -622,15 +660,15 @@ class WWVBMinute(_WWVBMinute):
|
|
622
660
|
else:
|
623
661
|
self._fill_pm_timecode_regular(t)
|
624
662
|
|
625
|
-
def next_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
663
|
+
def next_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
626
664
|
"""Return an object representing the next minute"""
|
627
665
|
d = self.as_datetime() + datetime.timedelta(minutes=1)
|
628
|
-
return self.from_datetime(d, newut1, newls, self)
|
666
|
+
return self.from_datetime(d, newut1=newut1, newls=newls, old_time=self)
|
629
667
|
|
630
|
-
def previous_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
668
|
+
def previous_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
631
669
|
"""Return an object representing the previous minute"""
|
632
670
|
d = self.as_datetime() - datetime.timedelta(minutes=1)
|
633
|
-
return self.from_datetime(d, newut1, newls, self)
|
671
|
+
return self.from_datetime(d, newut1=newut1, newls=newls, old_time=self)
|
634
672
|
|
635
673
|
@classmethod
|
636
674
|
def _get_dut1_info(cls: type, year: int, days: int, old_time: WWVBMinute | None = None) -> tuple[int, bool]: # noqa: ARG003
|
@@ -659,18 +697,19 @@ class WWVBMinute(_WWVBMinute):
|
|
659
697
|
days = d.pop("days")
|
660
698
|
hour = d.pop("hour")
|
661
699
|
minute = d.pop("minute")
|
662
|
-
dst
|
663
|
-
ut1
|
700
|
+
dst = d.pop("dst", None)
|
701
|
+
ut1 = d.pop("ut1", None)
|
664
702
|
ls = d.pop("ls", None)
|
665
703
|
d.pop("ly", None)
|
666
704
|
if d:
|
667
705
|
raise ValueError(f"Invalid options: {d}")
|
668
|
-
return cls(year, days, hour, minute, dst, ut1, None if ls is None else bool(ls))
|
706
|
+
return cls(year, days, hour, minute, dst, ut1=ut1, ls=None if ls is None else bool(ls))
|
669
707
|
|
670
708
|
@classmethod
|
671
709
|
def from_datetime(
|
672
710
|
cls,
|
673
711
|
d: datetime.datetime,
|
712
|
+
*,
|
674
713
|
newut1: int | None = None,
|
675
714
|
newls: bool | None = None,
|
676
715
|
old_time: WWVBMinute | None = None,
|
@@ -682,7 +721,7 @@ class WWVBMinute(_WWVBMinute):
|
|
682
721
|
return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
|
683
722
|
|
684
723
|
@classmethod
|
685
|
-
def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None:
|
724
|
+
def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None: # noqa: PLR0912
|
686
725
|
"""Construct a WWVBMinute from a WWVBTimecode"""
|
687
726
|
for i in (0, 9, 19, 29, 39, 49, 59):
|
688
727
|
if t.am[i] != AmplitudeModulation.MARK:
|
@@ -697,9 +736,13 @@ class WWVBMinute(_WWVBMinute):
|
|
697
736
|
minute = t._get_am_bcd(1, 2, 3, 5, 6, 7, 8)
|
698
737
|
if minute is None:
|
699
738
|
return None
|
739
|
+
if minute >= 60:
|
740
|
+
return None
|
700
741
|
hour = t._get_am_bcd(12, 13, 15, 16, 17, 18)
|
701
742
|
if hour is None:
|
702
743
|
return None
|
744
|
+
if hour >= 24:
|
745
|
+
return None
|
703
746
|
days = t._get_am_bcd(22, 23, 25, 26, 27, 28, 30, 31, 32, 33)
|
704
747
|
if days is None:
|
705
748
|
return None
|
@@ -716,8 +759,10 @@ class WWVBMinute(_WWVBMinute):
|
|
716
759
|
if days > 366 or (not ly and days > 365):
|
717
760
|
return None
|
718
761
|
ls = bool(t.am[56])
|
719
|
-
dst =
|
720
|
-
|
762
|
+
dst = t._get_am_bcd(57, 58)
|
763
|
+
if dst is None:
|
764
|
+
return None
|
765
|
+
return cls(year, days, hour, minute, dst, ut1, ls=ls, ly=ly)
|
721
766
|
|
722
767
|
|
723
768
|
class WWVBMinuteIERS(WWVBMinute):
|
@@ -729,7 +774,7 @@ class WWVBMinuteIERS(WWVBMinute):
|
|
729
774
|
return round(get_dut1(d) * 10) * 100, isls(d)
|
730
775
|
|
731
776
|
|
732
|
-
def _bcd_bits(n: int) -> Generator[bool
|
777
|
+
def _bcd_bits(n: int) -> Generator[bool]:
|
733
778
|
"""Return the bcd representation of n, starting with the least significant bit"""
|
734
779
|
while True:
|
735
780
|
d = n % 10
|
@@ -743,9 +788,13 @@ class AmplitudeModulation(enum.IntEnum):
|
|
743
788
|
"""Constants that describe an Amplitude Modulation value"""
|
744
789
|
|
745
790
|
ZERO = 0
|
791
|
+
"""A zero bit (reduced carrier during the first 200ms of the second)"""
|
746
792
|
ONE = 1
|
793
|
+
"""A one bit (reduced carrier during the first 500ms of the second)"""
|
747
794
|
MARK = 2
|
795
|
+
"""A mark bit (reduced carrier during the first 800ms of the second)"""
|
748
796
|
UNSET = -1
|
797
|
+
"""An unset or unknown amplitude modulation value"""
|
749
798
|
|
750
799
|
|
751
800
|
@enum.unique
|
@@ -753,8 +802,11 @@ class PhaseModulation(enum.IntEnum):
|
|
753
802
|
"""Constants that describe a Phase Modulation value"""
|
754
803
|
|
755
804
|
ZERO = 0
|
805
|
+
"""A one bit (180° phase shift during the second)"""
|
756
806
|
ONE = 1
|
807
|
+
"""A zero bit (No phase shift during the second)"""
|
757
808
|
UNSET = -1
|
809
|
+
"""An unset or unknown phase modulation value"""
|
758
810
|
|
759
811
|
|
760
812
|
class WWVBTimecode:
|
@@ -777,7 +829,10 @@ class WWVBTimecode:
|
|
777
829
|
The the bits ``self.am[poslist[i]]`` in MSB order are converted from
|
778
830
|
BCD to integer
|
779
831
|
"""
|
780
|
-
pos =
|
832
|
+
pos = list(poslist)[::-1]
|
833
|
+
for p in pos:
|
834
|
+
if self.am[p] not in {AmplitudeModulation.ZERO, AmplitudeModulation.ONE}:
|
835
|
+
return None
|
781
836
|
val = [bool(self.am[p]) for p in pos]
|
782
837
|
result = 0
|
783
838
|
base = 1
|
@@ -805,7 +860,7 @@ class WWVBTimecode:
|
|
805
860
|
else:
|
806
861
|
self.am[p] = AmplitudeModulation.ZERO
|
807
862
|
|
808
|
-
def _put_pm_bit(self, i: int, v: PhaseModulation | int
|
863
|
+
def _put_pm_bit(self, i: int, v: PhaseModulation | int) -> None:
|
809
864
|
"""Update a bit of the Phase Modulation signal"""
|
810
865
|
self.phase[i] = PhaseModulation(v)
|
811
866
|
|
@@ -0,0 +1 @@
|
|
1
|
+
{"START": "1972-01-01", "OFFSETS_GZ": "H4sIAJF3PWgC/+2aa3LDMAiEL5uHLDuxnN5/pn/aTmfSSiAWhGR9J8gsywJylqVHPtqxZuH/7leeI0fKsGd5EngQ2WisJWKegrThDa6aJFnL0u4wYZkCE2UmSF0U+13vCveStC6JTfQyW3O86HLJf0SvDgy5u4FCI+WVKRuy0KMjJeXoULIvMDmEWgeRxAJtwXquPCIBqbLh/gbfv0mcxk3mHV9tYiATZP8W/zgw2wd5LpJnY+WErI8abJ3opaIW6592+YMbjSsNWQFlNVVtuhjhtQzSUh4MEpOdDrSW6qsUv+O+Dt+XkIONSrUwvWmTsmq5LO9xsZ+EgcDK+MIESDaYmxSxGlgbGOFjBXMjbV7lc6zlmQ0i48oH5P4+vK7i/AHc7tfTXDtffqFi3m6WhApPSTyDvArU5vUDhm7YaNQYGASVbbwLUBtI2PrhSiZNbvCRrtGUGu0GbjDhJ3aLCx5dQFjt0LFovmWB96e6tktqMenoULXajVS3asBibP3kYXrpmZxnsS2Yf2xRPrHbvQ2D9wjfL4C6b4PWV4otW0vWUYkeWE5M8M594oLbxP77xcl4NuBkG0dfM3xOUf/T0GF+ur+J5pljcODEUZkXg6vIdLYy7g3oZU3bPNDnc8qwGdJZMmAurUsRj6tOo95zP6fb9YPWp5OuZ5X7q2DrmsG/VCyTyaREnDRhnUxOzfzzh3/NRuYTMxwhU6lNAAA="}
|
@@ -6,8 +6,8 @@
|
|
6
6
|
# SPDX-License-Identifier: GPL-3.0-only
|
7
7
|
from __future__ import annotations
|
8
8
|
|
9
|
+
import datetime
|
9
10
|
import functools
|
10
|
-
import time
|
11
11
|
from tkinter import Canvas, TclError, Tk
|
12
12
|
from typing import TYPE_CHECKING, Any
|
13
13
|
|
@@ -59,31 +59,29 @@ DEFAULT_COLORS = "#3c3c3c #3c3c3c #3c3c3c #cc3c3c #88883c #3ccc3c"
|
|
59
59
|
metavar="COLORS",
|
60
60
|
help="2, 3, 4, or 6 Tk color values",
|
61
61
|
)
|
62
|
-
@click.option("--size", default=48)
|
63
|
-
@click.option("--min-size", default=None)
|
62
|
+
@click.option("--size", default=48, help="initial size in pixels")
|
63
|
+
@click.option("--min-size", default=None, type=int, help="minimum size in pixels (default: same as initial size)")
|
64
64
|
def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: PLR0915
|
65
65
|
"""Visualize the WWVB signal in realtime"""
|
66
66
|
if min_size is None:
|
67
67
|
min_size = size
|
68
68
|
|
69
|
-
def deadline_ms(deadline:
|
69
|
+
def deadline_ms(deadline: datetime.datetime) -> int:
|
70
70
|
"""Compute the number of ms until a deadline"""
|
71
|
-
now =
|
72
|
-
return int(max(0, deadline - now) * 1000)
|
71
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
72
|
+
return int(max(0, (deadline - now).total_seconds()) * 1000)
|
73
73
|
|
74
|
-
def wwvbtick() -> Generator[tuple[
|
74
|
+
def wwvbtick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
|
75
75
|
"""Yield consecutive values of the WWVB amplitude signal, going from minute to minute"""
|
76
|
-
timestamp =
|
76
|
+
timestamp = datetime.datetime.now(datetime.timezone.utc).replace(second=0, microsecond=0)
|
77
77
|
|
78
78
|
while True:
|
79
|
-
|
80
|
-
key = tt.tm_year, tt.tm_yday, tt.tm_hour, tt.tm_min
|
81
|
-
timecode = wwvb.WWVBMinuteIERS(*key).as_timecode()
|
79
|
+
timecode = wwvb.WWVBMinuteIERS.from_datetime(timestamp).as_timecode()
|
82
80
|
for i, code in enumerate(timecode.am):
|
83
|
-
yield timestamp + i, code
|
84
|
-
timestamp = timestamp + 60
|
81
|
+
yield timestamp + datetime.timedelta(seconds=i), code
|
82
|
+
timestamp = timestamp + datetime.timedelta(seconds=60)
|
85
83
|
|
86
|
-
def wwvbsmarttick() -> Generator[tuple[
|
84
|
+
def wwvbsmarttick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
|
87
85
|
"""Yield consecutive values of the WWVB amplitude signal
|
88
86
|
|
89
87
|
.. but deal with time progressing unexpectedly, such as when the
|
@@ -94,10 +92,10 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
|
|
94
92
|
"""
|
95
93
|
while True:
|
96
94
|
for stamp, code in wwvbtick():
|
97
|
-
now =
|
98
|
-
if stamp < now - 60:
|
95
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
96
|
+
if stamp < now - datetime.timedelta(seconds=60):
|
99
97
|
break
|
100
|
-
if stamp < now - 1:
|
98
|
+
if stamp < now - datetime.timedelta(seconds=1):
|
101
99
|
continue
|
102
100
|
yield stamp, code
|
103
101
|
|
@@ -137,7 +135,7 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
|
|
137
135
|
yield deadline_ms(stamp)
|
138
136
|
led_on(code)
|
139
137
|
app.update()
|
140
|
-
yield deadline_ms(stamp + 0.2 + 0.3 * int(code))
|
138
|
+
yield deadline_ms(stamp + datetime.timedelta(seconds=0.2 + 0.3 * int(code)))
|
141
139
|
led_off(code)
|
142
140
|
app.update()
|
143
141
|
|