Asopitech With · Thermo-hygrometer
SwitchBot Meter history with Alopex Skulk
SwitchBot Meter is a thermo-hygrometer that measures room temperature and humidity and shows the history as graphs in a phone app. What if we rebuilt it with Alopex Skulk?
Updated: Oct 13, 2026
What the service does
SwitchBot Meter measures temperature and humidity and sends readings to the phone app over Bluetooth. The app records temperature, relative humidity, absolute humidity, dew point and saturation vapor pressure, shows them as graphs, and exports the records as CSV. Paired with a hub, the records are saved to the cloud as well. This page rebuilds the history and graphs.
Where the data lives
- Skulk time series: temperature, relative humidity, absolute humidity, dew point and saturation vapor pressure are appended to Skulk with room and device tags.
- Time-bucketed aggregation: hourly and daily averages and maximums come from time buckets and feed the graphs.
- Retention: Skulk drops expired partitions automatically.
Reading values from the meter and writing them is the job of the app code.
Deployment
Skulk is embedded in the phone app and the hub software. Records collect locally first, and where many homes come together the same Skulk engine runs as a server or cluster to receive them.
Why Alopex Skulk fits
Temperature and humidity records are numeric time series that arrive on a schedule. Skulk handles ingestion, time aggregation and retention while embedded in the app. The stored data is Parquet, which spreadsheets and analysis tools read directly.
See it in code
Temperature and humidity are ingested into Skulk with Line Protocol and read back. Just the core (Rust).
use alopex_skulk::ingest::line_protocol::LineProtocolDecoder;
use alopex_skulk::ingest::{IngestLimits, Ingestor};
use alopex_skulk::model::FieldValue;
use alopex_skulk::store::recovery::{RecoveryConfig, RecoveryStore};
fn main() -> alopex_skulk::Result<()> {
std::fs::create_dir_all("switchbot-data").unwrap();
let store = RecoveryStore::open("switchbot-data", RecoveryConfig::default())?;
let limits = IngestLimits::default();
let mut ingestor = Ingestor::new(store, limits);
// 温度・湿度を Line Protocol で取り込む(measurement,tag field[,field] timestamp[ns])
let base: i64 = 7_200_000_000_000;
let now = base + 300_000_000_000;
for (i, (t, h)) in [(20.5, 55.0), (21.0, 58.0), (21.8, 60.0)].iter().enumerate() {
let ts = base + (i as i64) * 60_000_000_000;
let line = format!("room,sensor=bedroom temperature={t},humidity={h} {ts}");
let batch = LineProtocolDecoder::new(limits).decode(line.as_bytes(), now)?;
assert_eq!(ingestor.ingest(batch, now)?.accepted_count(), 1);
}
ingestor.sink_mut().flush_all()?;
// 取り込んだ温度を読み出す
for row in &ingestor.sink().read_measurement("room")? {
if let Some(FieldValue::Float(t)) = row.row().field("temperature") {
println!("t={} temperature={t}", row.row().timestamp());
}
}
Ok(())
}Ingest is durable at the WAL boundary when ingest() returns, and flush_all() writes Parquet. Hourly and daily averages come from time-bucket aggregation, and old partitions drop via retention.
Build room records with Alopex Skulk, which draws temperature and humidity graphs just by embedding it in your app.