In bacpypes3, the BACnet Time primitive is defined as class Time(Atomic, tuple), so a Time value is a Python tuple of four integers: (hour, minute, second, hundredth). Construct one by passing a single 4-tuple — Time((9, 30, 0, 0)) for 09:30:00.00 — and anything that isn't exactly four elements raises ValueError("4-tuple expected"). Each field uses 255 to mean "unspecified," matching the four-octet wildcard encoding in ASHRAE 135. Because the class subclasses tuple, you index and unpack it like any tuple: t[0] is the hour.
The Scenario: Why Is a Time a Tuple?
You're reading bacpypes3 source — or stepping through a Schedule object — and you hit the Time class. You print a value you got back from a ReadProperty and instead of a familiar datetime.time, you get something that looks like (9, 30, 0, 0). Or you try to build one with Time(9, 30, 0, 0) and the field doesn't hold what you expect. Drop one element and you get a blunt ValueError: 4-tuple expected.
None of this is a bug. The bacpypes3 Time class is deliberately a tuple, and once you know why, the construction rules and the wildcard behavior stop being surprises.
How bacpypes3 Defines Time
In the bacpypes3.primitivedata module, the class is declared with two base classes:
class Time(Atomic, tuple):
...Each base class does one job. Atomic contributes the BACnet primitive-datatype machinery — tag handling and the encode/decode path that turns the value into application tag 11 on the wire. tuple is the storage: the four time fields live directly in the tuple itself, in order.
That design tracks the protocol exactly. In ASHRAE Standard 135, a Time value is encoded as four octets — hour, minute, second, and hundredths of a second. bacpypes3 stores those same four values as a 4-tuple, so the in-memory object mirrors the on-the-wire encoding with no translation layer in between.
This is the same pattern bacpypes3 uses elsewhere for atomic types. The REAL datatype is declared as class Real(Atomic, float) — a Real is a float — for the same reason a Time is a tuple. If that inheritance style is new to you, the bacpypes3 Real class explainer walks through what each base class contributes and when to pass a native Python value versus a wrapped instance.
The Four Fields
The tuple is ordered (hour, minute, second, hundredth). The valid ranges follow the BACnet encoding:
- hour — 0 through 23.
- minute — 0 through 59.
- second — 0 through 59.
- hundredth — 0 through 99. This is hundredths of a second, not milliseconds.
Any field can be set to 255 to mark it "unspecified." That is the BACnet wildcard (octet value X'FF'), and it's how you express patterns like "every hour on the half hour" or "9 o'clock, any minute." A Time with one or more 255 fields is a valid, intentional value — not an error.
Constructing and Reading a Time
The class doesn't define its own constructor signature with named hour/minute/second arguments. You pass a single 4-tuple as the first positional argument, and the tuple flows through the class's cast() validation:
from bacpypes3.primitivedata import Time
# A Time IS a 4-tuple: (hour, minute, second, hundredth)
t = Time((9, 30, 0, 0)) # 09:30:00.00
# Because Time subclasses tuple, index and unpack it like any tuple
t[0] # 9 (the hour)
hour, minute, second, hundredth = t
# 255 marks a field as "unspecified" (the BACnet wildcard)
any_minute = Time((9, 255, 255, 255)) # 9 o'clock, rest unspecified
# Passing the wrong number of elements is the most common mistake
Time((9, 30, 0)) # ValueError: 4-tuple expectedRecent bacpypes3 releases also let cast() accept a datetime.time object or a time string and convert it for you, but the canonical, version-stable form is the explicit 4-tuple. If you depend on the datetime.time or string path, confirm it against your installed release rather than assuming it — see the escalation note below.
Where you'll most often meet a Time in practice is inside a Schedule. A Weekly_Schedule is built from DailySchedule entries, and each entry is a list of TimeValue pairs — a Time and the value to apply at that time. If you're assembling those, the bacpypes3 DailySchedule and TimeValue reference shows the full construction without the AnyAtomic type errors that trip up first attempts.
Common Pitfalls
- Passing four positional arguments instead of one tuple.
Time(9, 30, 0, 0)is not the same asTime((9, 30, 0, 0)). The atomic base captures the first positional argument as the value, so the loose form does not populate all four fields the way you intend. Always pass a single 4-tuple. - Forgetting the hundredth field. The
4-tuple expectederror almost always comes from passing(hour, minute, second)and omitting hundredths. If you mean "on the second," pass0for hundredths explicitly —(9, 30, 0, 0). - Treating hundredths as milliseconds. The fourth field is 0–99, not 0–999. A value derived from milliseconds needs to be divided by 10 before it goes into the tuple.
- Using
Noneto mean "any." The BACnet wildcard is the integer 255 in a field, not PythonNone. Build wildcard times with 255, not by leaving fields out or setting them toNone. - Copying constructor signatures from the old bacpypes. The original bacpypes (the pre-3 package) used keyword arguments like
Time(hour=255, minute=255, ...). bacpypes3 is a separate package with the(Atomic, tuple)design described here. Documentation and Stack Overflow answers for the older library do not carry over.
When to Verify Against Your Installed Release
bacpypes3 is pre-1.0, and primitivedata details — accessor names, what cast() accepts, debug hooks — have shifted between releases. The (Atomic, tuple) shape and the 4-tuple of (hour, minute, second, hundredth) are stable and match the protocol, but before you build anything on a specific accessor or coercion behavior, read the Time class in the primitivedata.py that ships with your pinned version. Pin the version in your project, and when behavior doesn't match a tutorial, trust the installed source over the article.
Source Attribution
The technical guidance in this entry is grounded in the following public sources:
- bacpypes3 primitivedata.py source — JoelBender/BACpypes3 on GitHub. The
Timeclass definition (class Time(Atomic, tuple)), thecast()4-tuple validation, and the 255 wildcard handling. - bacpypes3 on PyPI — Release listing for pinning and verifying the version installed in your environment.
- ASHRAE Standard 135 — BACnet—A Data Communication Protocol for Building Automation and Control Networks. The normative reference for the four-octet Time encoding (hour, minute, second, hundredths) and the X'FF' unspecified value.
Additional testing and field validation by SiteConduit.
Was this article helpful?
Related Articles
Need to do this remotely? SiteConduit provides Layer 2 access that preserves BACnet broadcasts — no BBMD needed for remote sessions. Join the waitlist.
SiteConduit Technical Team
Idea Networks Inc.
SiteConduit builds managed remote access for building automation. Our knowledge base is maintained by BAS professionals with hands-on experience deploying and troubleshooting BACnet, Niagara, Modbus, and Facility Explorer systems.