bacpypes3 Time Class: Why It's a Tuple of (Hour, Minute, Second, Hundredth)

Tools & Librariesbacpypes3TimeAtomicprimitivedata
June 19, 2026|8 min read

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:

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 expected

Recent 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

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:

Additional testing and field validation by SiteConduit.

bacpypes3TimeAtomicprimitivedataBACnet types

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.

SC

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.