Show a sensor's identity in both bases, and take either
Reported as bandsaunter using "completely different" identities from a script
that had been watching the same sensors for years. They are the same
identities. Their list against this program's:
14645 = 3935 8677 = 21E5 14717 = 397D
3542 = 0DD6 10011 = 271B
Five of their eight, matching exactly; the other three simply were not
transmitting during the three seconds of capture I had. One base is
hexadecimal and the other decimal, and nothing anywhere said so.
The hexadecimal is not arbitrary -- the identity is a bit field with the
channel packed into the top two bits, and that shape is visible in hex and
invisible in decimal, which is why the decoder carries it that way. But
rtl_433 and everything built on it prints these in decimal, so anybody who
comes to this with their own sensors already written down has the other form,
and being handed a list that looks unrelated to theirs is a poor welcome.
So both are shown wherever a person reads: the live display when there is room
for the column, the report, the sensor list and the spreadsheet. `bandsaunter
sensors` is the table for correlating two lists and now has them side by side,
with a line saying which is which and why.
Either may be typed at --name. One case needs care rather than cleverness:
"3935" is a valid identity in both bases and they are different sensors, so
when both are out there it says the identity is ambiguous and asks for the
whole key instead of picking whichever the code reaches first.
A first attempt at the lookup converted a decimal identity to hexadecimal and
matched on that as well as comparing decimals directly. Both worked, so
neither could be tested apart from the other; the conversion was removed
rather than given a test written backwards from it.
Full suite 2391 passed, checked against five deliberately broken builds.
Built as 2026-09-20_01.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
parent
03d3600ecc
commit
0f7e47e55e
10 changed files with 246 additions and 15 deletions
21
README.md
21
README.md
|
|
@ -1842,6 +1842,26 @@ of a hat in a factory — or a different number out of the same hat the next
|
||||||
time the batteries were changed. It is enough to tell one sensor from another
|
time the batteries were changed. It is enough to tell one sensor from another
|
||||||
and it is no use at all for telling which is which. `1A2B` is not a place.
|
and it is no use at all for telling which is which. `1A2B` is not a place.
|
||||||
|
|
||||||
|
**The identity is shown in both bases**, because the same number gets written
|
||||||
|
two ways. These identities are bit fields with the channel packed above them,
|
||||||
|
so this program prints them in hexadecimal where that shape shows; rtl_433 and
|
||||||
|
everything built on it prints them in decimal. `3935` and `14645` are the same
|
||||||
|
sensor. Anyone arriving with a list of their own sensors already has it in
|
||||||
|
decimal and has no reason to convert it, so `bandsaunter sensors` puts the two
|
||||||
|
side by side and either can be typed at `--name`:
|
||||||
|
|
||||||
|
```
|
||||||
|
name id decimal key model ch msgs last heard
|
||||||
|
back fence 3935 14645 tower/3935 Tower 592TXR A 412 2026-09-20 14:17
|
||||||
|
— 271B 10011 tower/271B Tower 592TXR B 408 2026-09-20 14:17
|
||||||
|
— 21E5 8677 tower/21E5 Tower 592TXR C 405 2026-09-20 14:16
|
||||||
|
```
|
||||||
|
|
||||||
|
The one case that needs care is an identity that is a valid number in both
|
||||||
|
bases — `3935` is hexadecimal 3935 and also decimal 3935, which are different
|
||||||
|
sensors. If both are out there it says so and asks for the whole key rather
|
||||||
|
than picking one.
|
||||||
|
|
||||||
So **press `n` while listening**. The display comes down, the sensors are
|
So **press `n` while listening**. The display comes down, the sensors are
|
||||||
listed with numbers, you pick one and type a name, and it goes back up. The
|
listed with numbers, you pick one and type a name, and it goes back up. The
|
||||||
receiver keeps running throughout: a slow typist loses a few seconds of
|
receiver keeps running throughout: a slow typist loses a few seconds of
|
||||||
|
|
@ -1856,6 +1876,7 @@ been heard:
|
||||||
|
|
||||||
```
|
```
|
||||||
bandsaunter weather --name 1A2B="back fence" --name 5C=shed
|
bandsaunter weather --name 1A2B="back fence" --name 5C=shed
|
||||||
|
bandsaunter sensors --name 14645="back fence" # decimal works too
|
||||||
bandsaunter sensors --name 0311="lightning detector" --note 0311="south gable"
|
bandsaunter sensors --name 0311="lightning detector" --note 0311="south gable"
|
||||||
bandsaunter sensors --forget 93
|
bandsaunter sensors --forget 93
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ and transcribing speech.
|
||||||
# Versions are the release date and a revision within that day, so
|
# Versions are the release date and a revision within that day, so
|
||||||
# 2026-08-21_02 is the second build made on the 21st. The revision is padded
|
# 2026-08-21_02 is the second build made on the 21st. The revision is padded
|
||||||
# to two digits so versions sort as text.
|
# to two digits so versions sort as text.
|
||||||
VERSION_DATE = "2026-09-07"
|
VERSION_DATE = "2026-09-20"
|
||||||
VERSION_REVISION = 6
|
VERSION_REVISION = 1
|
||||||
|
|
||||||
__version__ = f"{VERSION_DATE}_{VERSION_REVISION:02d}"
|
__version__ = f"{VERSION_DATE}_{VERSION_REVISION:02d}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1610,6 +1610,7 @@ def cmd_sensors(args) -> int:
|
||||||
title="[bold]sensors[/bold]", title_justify="left")
|
title="[bold]sensors[/bold]", title_justify="left")
|
||||||
t.add_column("name", overflow="fold")
|
t.add_column("name", overflow="fold")
|
||||||
t.add_column("id", style="grey62", no_wrap=True)
|
t.add_column("id", style="grey62", no_wrap=True)
|
||||||
|
t.add_column("decimal", style="grey62", no_wrap=True, justify="right")
|
||||||
t.add_column("key", style="grey62", no_wrap=True)
|
t.add_column("key", style="grey62", no_wrap=True)
|
||||||
t.add_column("model", style="grey62", overflow="fold")
|
t.add_column("model", style="grey62", overflow="fold")
|
||||||
t.add_column("ch", style="grey62", justify="center")
|
t.add_column("ch", style="grey62", justify="center")
|
||||||
|
|
@ -1621,12 +1622,13 @@ def cmd_sensors(args) -> int:
|
||||||
time.localtime(sensor.last_heard)) \
|
time.localtime(sensor.last_heard)) \
|
||||||
if sensor.last_heard else ""
|
if sensor.last_heard else ""
|
||||||
t.add_row(sensor.name or "[yellow]unnamed[/yellow]", sensor.sensor,
|
t.add_row(sensor.name or "[yellow]unnamed[/yellow]", sensor.sensor,
|
||||||
sensor.key, sensor.model, sensor.channel,
|
sensor.number, sensor.key, sensor.model, sensor.channel,
|
||||||
f"{sensor.messages:,}", last, sensor.note)
|
f"{sensor.messages:,}", last, sensor.note)
|
||||||
console.print(t)
|
console.print(t)
|
||||||
console.print(f"[grey62]kept in {book.path} — "
|
console.print(f"[grey62]kept in {book.path} — name one with "
|
||||||
f"name one with `bandsaunter sensors --name ID=NAME`"
|
f"`bandsaunter sensors --name ID=NAME`, in either column: "
|
||||||
f"[/grey62]")
|
f"the decimal is the same identity, and is what rtl_433 "
|
||||||
|
f"and anything built on it prints[/grey62]")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,24 @@ class Sensor:
|
||||||
def family(self) -> str:
|
def family(self) -> str:
|
||||||
return self.key.split("/", 1)[0] if "/" in self.key else ""
|
return self.key.split("/", 1)[0] if "/" in self.key else ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def number(self) -> str:
|
||||||
|
"""The same identity written in decimal, or "" if it is not a number.
|
||||||
|
|
||||||
|
Which is what rtl_433 and everything built on it prints, so it is
|
||||||
|
what anybody arriving here with a list of their own sensors already
|
||||||
|
has written down. This program shows hexadecimal because the
|
||||||
|
identity is a bit field with a channel packed above it and the shape
|
||||||
|
shows in hex -- and then presents a person holding a list of decimal
|
||||||
|
numbers with a list of hexadecimal ones and no hint that they are the
|
||||||
|
same sensors. They are: 0x3935 is 14645. Both are shown everywhere
|
||||||
|
a person reads, and either can be typed.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return str(int(self.sensor, 16))
|
||||||
|
except ValueError:
|
||||||
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def named(self) -> bool:
|
def named(self) -> bool:
|
||||||
return bool(self.name.strip())
|
return bool(self.name.strip())
|
||||||
|
|
@ -197,16 +215,25 @@ class SensorBook:
|
||||||
def find(self, text: str) -> list[Sensor]:
|
def find(self, text: str) -> list[Sensor]:
|
||||||
"""Sensors matching what somebody typed: a key, a name, or an id.
|
"""Sensors matching what somebody typed: a key, a name, or an id.
|
||||||
|
|
||||||
An exact match on the key or the identity wins outright, so that
|
An identity may be given in either base. Somebody who has been
|
||||||
naming a sensor whose identity happens to read like a word does not
|
watching these sensors with another tool has a list of decimal
|
||||||
turn into a list of everything else in the garden.
|
numbers and no reason to convert it, and somebody reading this
|
||||||
|
program's own output has hexadecimal; both work.
|
||||||
|
|
||||||
|
The two can collide -- "3935" is a hexadecimal identity and also a
|
||||||
|
decimal one -- so both readings are looked for, and if they land on
|
||||||
|
two different sensors both are returned and the caller says it is
|
||||||
|
ambiguous rather than picking one. An exact match otherwise wins
|
||||||
|
outright, so naming a sensor whose identity reads like a word does
|
||||||
|
not turn into a list of everything in the garden.
|
||||||
"""
|
"""
|
||||||
wanted = (text or "").strip().lower()
|
wanted = (text or "").strip().lower()
|
||||||
if not wanted:
|
if not wanted:
|
||||||
return []
|
return []
|
||||||
exact = [s for s in self.ordered()
|
exact = [s for s in self.ordered()
|
||||||
if wanted in (s.key.lower(), s.sensor.lower(),
|
if wanted in (s.key.lower(), s.sensor.lower(),
|
||||||
s.name.strip().lower())]
|
s.name.strip().lower())
|
||||||
|
or (wanted.isdigit() and wanted == s.number)]
|
||||||
if exact:
|
if exact:
|
||||||
return exact
|
return exact
|
||||||
return [s for s in self.ordered()
|
return [s for s in self.ordered()
|
||||||
|
|
|
||||||
|
|
@ -883,6 +883,11 @@ class WeatherDisplay:
|
||||||
t.add_column("#", style="grey62", width=2, justify="right")
|
t.add_column("#", style="grey62", width=2, justify="right")
|
||||||
t.add_column("name", width=14, no_wrap=True)
|
t.add_column("name", width=14, no_wrap=True)
|
||||||
t.add_column("id", width=5, style="grey62", no_wrap=True)
|
t.add_column("id", width=5, style="grey62", no_wrap=True)
|
||||||
|
if width >= 100:
|
||||||
|
# The same identity in decimal, which is the form other tools
|
||||||
|
# for this band print and the form anybody's own list is in.
|
||||||
|
t.add_column("dec", width=6, style="grey62", no_wrap=True,
|
||||||
|
justify="right")
|
||||||
if width >= 92:
|
if width >= 92:
|
||||||
t.add_column("model", width=16, style="grey62", no_wrap=True)
|
t.add_column("model", width=16, style="grey62", no_wrap=True)
|
||||||
t.add_column("readings", overflow="fold")
|
t.add_column("readings", overflow="fold")
|
||||||
|
|
@ -901,6 +906,8 @@ class WeatherDisplay:
|
||||||
Text(name, style="bold") if name
|
Text(name, style="bold") if name
|
||||||
else Text("unnamed", style="yellow"),
|
else Text("unnamed", style="yellow"),
|
||||||
station.sensor]
|
station.sensor]
|
||||||
|
if width >= 100:
|
||||||
|
row.append(station.number)
|
||||||
if width >= 92:
|
if width >= 92:
|
||||||
row.append(station.model or "")
|
row.append(station.model or "")
|
||||||
row.append(self._readings(station, now))
|
row.append(self._readings(station, now))
|
||||||
|
|
|
||||||
|
|
@ -418,6 +418,14 @@ class Station:
|
||||||
def family(self) -> str:
|
def family(self) -> str:
|
||||||
return self.key.split("/", 1)[0] if "/" in self.key else ""
|
return self.key.split("/", 1)[0] if "/" in self.key else ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def number(self) -> str:
|
||||||
|
"""The identity in decimal, which is what other tools print."""
|
||||||
|
try:
|
||||||
|
return str(int(self.sensor, 16))
|
||||||
|
except ValueError:
|
||||||
|
return ""
|
||||||
|
|
||||||
def add(self, reading) -> None:
|
def add(self, reading) -> None:
|
||||||
self.model = reading.model or self.model
|
self.model = reading.model or self.model
|
||||||
self.channel = reading.channel or self.channel
|
self.channel = reading.channel or self.channel
|
||||||
|
|
@ -1163,6 +1171,10 @@ def report(console, garden: Garden, book=None, imperial: bool = False) -> None:
|
||||||
title="[bold]sensors heard[/bold]", title_justify="left")
|
title="[bold]sensors heard[/bold]", title_justify="left")
|
||||||
t.add_column("name", overflow="fold")
|
t.add_column("name", overflow="fold")
|
||||||
t.add_column("id", style="grey62", no_wrap=True)
|
t.add_column("id", style="grey62", no_wrap=True)
|
||||||
|
# The same identity in decimal, because that is what rtl_433 and
|
||||||
|
# everything built on it prints, and somebody arriving with a list of
|
||||||
|
# their own sensors has it written down that way.
|
||||||
|
t.add_column("decimal", style="grey62", no_wrap=True, justify="right")
|
||||||
t.add_column("model", style="grey62", overflow="fold")
|
t.add_column("model", style="grey62", overflow="fold")
|
||||||
t.add_column("ch", style="grey62", justify="center")
|
t.add_column("ch", style="grey62", justify="center")
|
||||||
t.add_column("msgs", justify="right")
|
t.add_column("msgs", justify="right")
|
||||||
|
|
@ -1174,7 +1186,8 @@ def report(console, garden: Garden, book=None, imperial: bool = False) -> None:
|
||||||
for station in stations:
|
for station in stations:
|
||||||
name = book.name_for(station.key) if book is not None else ""
|
name = book.name_for(station.key) if book is not None else ""
|
||||||
t.add_row(name or "[grey62]—[/grey62]",
|
t.add_row(name or "[grey62]—[/grey62]",
|
||||||
station.sensor, station.model or "", station.channel or "",
|
station.sensor, station.number,
|
||||||
|
station.model or "", station.channel or "",
|
||||||
f"{station.messages:,}",
|
f"{station.messages:,}",
|
||||||
f"{station.gap:.0f} s" if station.gap else "",
|
f"{station.gap:.0f} s" if station.gap else "",
|
||||||
signal_text(station.snr, station.best_snr),
|
signal_text(station.snr, station.best_snr),
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,14 @@ class WeatherLog:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _decimal(sensor: str) -> str:
|
||||||
|
"""An identity in decimal, which is the form other tools print."""
|
||||||
|
try:
|
||||||
|
return str(int(sensor, 16))
|
||||||
|
except ValueError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _hex(bits: str) -> str:
|
def _hex(bits: str) -> str:
|
||||||
"""A message's bits as bytes, where they make whole ones."""
|
"""A message's bits as bytes, where they make whole ones."""
|
||||||
if not bits or len(bits) % 8:
|
if not bits or len(bits) % 8:
|
||||||
|
|
@ -198,8 +206,8 @@ def write_csv(path, readings, book=None, imperial: bool = False) -> Path:
|
||||||
for measure in reading.measures:
|
for measure in reading.measures:
|
||||||
if measure.name not in names:
|
if measure.name not in names:
|
||||||
names.append(measure.name)
|
names.append(measure.name)
|
||||||
heads = ["time", "unix", "name", "key", "model", "sensor", "channel",
|
heads = ["time", "unix", "name", "key", "model", "sensor", "decimal",
|
||||||
"battery", "signal (dB)"] \
|
"channel", "battery", "signal (dB)"] \
|
||||||
+ [_column(n, readings, imperial) for n in names]
|
+ [_column(n, readings, imperial) for n in names]
|
||||||
with open(path, "w", encoding="utf8", newline="") as fh:
|
with open(path, "w", encoding="utf8", newline="") as fh:
|
||||||
out = csv.writer(fh)
|
out = csv.writer(fh)
|
||||||
|
|
@ -210,7 +218,7 @@ def write_csv(path, readings, book=None, imperial: bool = False) -> Path:
|
||||||
f"{reading.at:.3f}" if reading.at else "",
|
f"{reading.at:.3f}" if reading.at else "",
|
||||||
book.name_for(reading.key) if book is not None else "",
|
book.name_for(reading.key) if book is not None else "",
|
||||||
reading.key, reading.model, reading.sensor,
|
reading.key, reading.model, reading.sensor,
|
||||||
reading.channel,
|
_decimal(reading.sensor), reading.channel,
|
||||||
"" if reading.battery_low is None else
|
"" if reading.battery_low is None else
|
||||||
("low" if reading.battery_low else "ok"),
|
("low" if reading.battery_low else "ok"),
|
||||||
f"{reading.snr:.1f}" if reading.snr else ""]
|
f"{reading.snr:.1f}" if reading.snr else ""]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
.\" Generated by packaging/make-man.py -- do not edit by hand.
|
.\" Generated by packaging/make-man.py -- do not edit by hand.
|
||||||
.TH BANDSAUNTER 1 "2026-09-07" "bandsaunter 2026-09-07_06" "User Commands"
|
.TH BANDSAUNTER 1 "2026-09-20" "bandsaunter 2026-09-20_01" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
bandsaunter \- scan, record and identify radio signals with an RTL-SDR
|
bandsaunter \- scan, record and identify radio signals with an RTL-SDR
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|
@ -2217,6 +2217,17 @@ of a hat in a factory \[em] or a different number out of the same hat the next
|
||||||
time the batteries were changed. It tells one sensor from another and is no
|
time the batteries were changed. It tells one sensor from another and is no
|
||||||
use at all for telling which is which.
|
use at all for telling which is which.
|
||||||
.PP
|
.PP
|
||||||
|
It is shown in both bases. These identities are bit fields with the channel
|
||||||
|
packed above them, so this prints them in hexadecimal where that shape shows,
|
||||||
|
while rtl_433 and everything built on it prints them in decimal: 3935 and
|
||||||
|
14645 are the same sensor. Anyone arriving with a list of their own already
|
||||||
|
has it in decimal, so
|
||||||
|
.B bandsaunter sensors
|
||||||
|
puts the two side by side and either may be typed at
|
||||||
|
.BR \-\-name .
|
||||||
|
An identity that is a valid number in both bases is reported as ambiguous
|
||||||
|
rather than resolved by guesswork.
|
||||||
|
.PP
|
||||||
So press
|
So press
|
||||||
.B n
|
.B n
|
||||||
while listening. The display comes down, the sensors are listed with numbers,
|
while listening. The display comes down, the sensors are listed with numbers,
|
||||||
|
|
|
||||||
|
|
@ -1329,6 +1329,17 @@ of a hat in a factory \[em] or a different number out of the same hat the next
|
||||||
time the batteries were changed. It tells one sensor from another and is no
|
time the batteries were changed. It tells one sensor from another and is no
|
||||||
use at all for telling which is which.
|
use at all for telling which is which.
|
||||||
.PP
|
.PP
|
||||||
|
It is shown in both bases. These identities are bit fields with the channel
|
||||||
|
packed above them, so this prints them in hexadecimal where that shape shows,
|
||||||
|
while rtl_433 and everything built on it prints them in decimal: 3935 and
|
||||||
|
14645 are the same sensor. Anyone arriving with a list of their own already
|
||||||
|
has it in decimal, so
|
||||||
|
.B bandsaunter sensors
|
||||||
|
puts the two side by side and either may be typed at
|
||||||
|
.BR \-\-name .
|
||||||
|
An identity that is a valid number in both bases is reported as ambiguous
|
||||||
|
rather than resolved by guesswork.
|
||||||
|
.PP
|
||||||
So press
|
So press
|
||||||
.B n
|
.B n
|
||||||
while listening. The display comes down, the sensors are listed with numbers,
|
while listening. The display comes down, the sensors are listed with numbers,
|
||||||
|
|
|
||||||
|
|
@ -1657,3 +1657,134 @@ def test_the_display_keeps_the_strength_on_a_narrow_terminal(book):
|
||||||
out = shown(garden, book, width=width, now=now)
|
out = shown(garden, book, width=width, now=now)
|
||||||
assert "29 dB" in out, f"lost at {width} columns"
|
assert "29 dB" in out, f"lost at {width} columns"
|
||||||
assert max(len(line) for line in out.splitlines()) <= width
|
assert max(len(line) for line in out.splitlines()) <= width
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# The same identity, written the way other tools write it
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# These identities are bit fields with a channel packed above them, so this
|
||||||
|
# program prints them in hexadecimal, where the shape shows. rtl_433 and
|
||||||
|
# everything built on it prints them in decimal. They are the same number --
|
||||||
|
# 0x3935 is 14645 -- and somebody who has been watching their own sensors for
|
||||||
|
# years has a list of the decimal ones and no reason to convert it.
|
||||||
|
|
||||||
|
# Real identities, from the sensors that finally decoded.
|
||||||
|
KNOWN_IDS = [("3935", "14645"), ("271B", "10011"), ("21E5", "8677"),
|
||||||
|
("397D", "14717"), ("0DD6", "3542")]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("hexid,decimal", KNOWN_IDS)
|
||||||
|
def test_an_identity_is_shown_in_both_bases(hexid, decimal):
|
||||||
|
station = wx.Station(key=f"tower/{hexid}")
|
||||||
|
assert station.sensor == hexid
|
||||||
|
assert station.number == decimal
|
||||||
|
assert int(hexid, 16) == int(decimal)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("hexid,decimal", KNOWN_IDS)
|
||||||
|
def test_a_sensor_is_found_by_its_identity_in_either_base(hexid, decimal,
|
||||||
|
book):
|
||||||
|
book.heard(reading(sensor=int(hexid, 16), at=1_000.0))
|
||||||
|
for typed in (hexid, hexid.lower(), decimal):
|
||||||
|
assert [s.key for s in book.find(typed)] == [f"tower/{hexid}"], typed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("hexid,decimal", KNOWN_IDS)
|
||||||
|
def test_a_sensor_is_named_by_its_identity_in_either_base(hexid, decimal,
|
||||||
|
book):
|
||||||
|
from bandsaunter.cli import _name_sensors
|
||||||
|
|
||||||
|
book.heard(reading(sensor=int(hexid, 16), at=1_000.0))
|
||||||
|
assert _name_sensors(book, [f"{decimal}=shed"], quiet=True) == 1
|
||||||
|
assert book.name_for(f"tower/{hexid}") == "shed"
|
||||||
|
assert len(book) == 1 # and not a second, decimal-keyed one
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_identity_that_is_a_number_in_both_bases_is_called_ambiguous(book):
|
||||||
|
"""3935 is a hexadecimal identity and also a decimal one.
|
||||||
|
|
||||||
|
Both sensors exist here, so the honest answer is to say so rather than to
|
||||||
|
pick whichever the code happens to reach first.
|
||||||
|
"""
|
||||||
|
book.heard(reading(sensor=0x3935, at=1_000.0)) # decimal 14645
|
||||||
|
book.heard(reading(sensor=0x0F5F, at=1_001.0)) # decimal 3935
|
||||||
|
found = book.find("3935")
|
||||||
|
assert {s.key for s in found} == {"tower/3935", "tower/0F5F"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_naming_an_ambiguous_identity_asks_rather_than_guesses(book,
|
||||||
|
monkeypatch):
|
||||||
|
from bandsaunter.cli import _name_sensors
|
||||||
|
import bandsaunter.cli as cli
|
||||||
|
|
||||||
|
book.heard(reading(sensor=0x3935, at=1_000.0))
|
||||||
|
book.heard(reading(sensor=0x0F5F, at=1_001.0))
|
||||||
|
console = Console(width=120, force_terminal=False)
|
||||||
|
monkeypatch.setattr(cli, "console", console)
|
||||||
|
with console.capture() as cap:
|
||||||
|
assert _name_sensors(book, ["3935=shed"]) == 0
|
||||||
|
out = cap.get()
|
||||||
|
assert "matches" in out and "tower/3935" in out and "tower/0F5F" in out
|
||||||
|
assert book.name_for("tower/3935") == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_identity_that_is_not_a_number_has_no_decimal_form(book):
|
||||||
|
"""A name given before the sensor was heard is filed under a made-up key."""
|
||||||
|
from bandsaunter.cli import _name_sensors
|
||||||
|
|
||||||
|
_name_sensors(book, ["ZZZZ=shed"], quiet=True)
|
||||||
|
assert book.get("?/ZZZZ").number == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_both_bases_are_in_the_report(book):
|
||||||
|
garden = wx.Garden()
|
||||||
|
garden.add(reading(sensor=0x3935, at=1_000.0))
|
||||||
|
out = rendered(garden, book)
|
||||||
|
assert "3935" in out and "14645" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_both_bases_are_in_the_spreadsheet(tmp_path, book):
|
||||||
|
where = wl.write_csv(tmp_path / "w.csv",
|
||||||
|
[reading(sensor=0x3935, at=1_000.0)], book)
|
||||||
|
head, row = where.read_text().splitlines()[:2]
|
||||||
|
columns = head.split(",")
|
||||||
|
assert "decimal" in columns
|
||||||
|
fields = row.split(",")
|
||||||
|
assert fields[columns.index("sensor")] == "3935"
|
||||||
|
assert fields[columns.index("decimal")] == "14645"
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_decimal_is_on_the_display_when_there_is_room(book):
|
||||||
|
garden = wx.Garden()
|
||||||
|
now = time.time()
|
||||||
|
garden.add(reading(sensor=0x3935, at=now))
|
||||||
|
wide = shown(garden, book, width=110, now=now)
|
||||||
|
assert "3935" in wide and "14645" in wide
|
||||||
|
# And dropped, with the identity kept, when there is not.
|
||||||
|
narrow = shown(garden, book, width=80, now=now)
|
||||||
|
assert "3935" in narrow and "14645" not in narrow
|
||||||
|
assert max(len(line) for line in narrow.splitlines()) <= 80
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_sensor_list_puts_the_two_side_by_side(tmp_path, monkeypatch):
|
||||||
|
"""Which is the whole point: it is a table for correlating two lists."""
|
||||||
|
from bandsaunter.cli import build_parser, cmd_sensors
|
||||||
|
import bandsaunter.cli as cli
|
||||||
|
import bandsaunter.sensors as sensors
|
||||||
|
|
||||||
|
where = tmp_path / "sensors.yaml"
|
||||||
|
monkeypatch.setattr(sensors, "names_path", lambda directory=None: where)
|
||||||
|
book = SensorBook(path=where)
|
||||||
|
for hexid, _decimal in KNOWN_IDS:
|
||||||
|
book.heard(reading(sensor=int(hexid, 16), at=1_000.0))
|
||||||
|
book.save()
|
||||||
|
|
||||||
|
console = Console(width=140, force_terminal=False)
|
||||||
|
monkeypatch.setattr(cli, "console", console)
|
||||||
|
with console.capture() as cap:
|
||||||
|
assert cmd_sensors(build_parser().parse_args(["sensors"])) == 0
|
||||||
|
out = cap.get()
|
||||||
|
for hexid, decimal in KNOWN_IDS:
|
||||||
|
assert hexid in out and decimal in out
|
||||||
|
assert "rtl_433" in out # and says why the second column is there
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue