aprsNotifyJS/src/StationRow.vue

166 lines
4.0 KiB
Vue

<template>
<tr :class="{ timedOut, isLowVoltage, neverHeard: !stationStatus.lastHeard }">
<td :title="callsign">{{ tacticalAndOrCall }}</td>
<template v-if="stationStatus.lastHeard">
<td>{{ formatTime(stationStatus.lastHeard) }}</td>
<td>{{ formatTime(now - stationStatus.lastHeard, true) }}</td>
<td>{{ formatTime(Math.round(stationStatus.avgDelta), true) }}</td>
<td>{{ stationStatus.lastMicE }}</td>
<td>{{ stationStatus.lastVoltage || '' }}</td>
<td>{{ stationStatus.lastTemperature || '' }}</td>
<td>{{ stationStatus.lastComment }}</td>
</template>
<template v-else>
<td>Never Heard</td>
<td>Never Heard</td>
<td>Never Heard</td>
<td>Never Heard</td>
<td>Never Heard</td>
<td>Never Heard</td>
<td>Never Heard</td>
</template>
</tr>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import parseDuration from 'parse-duration';
const props = defineProps({
callsign: String,
tactical: String,
timeoutLength: String,
lowVoltage: Number,
finishedReplay: Boolean,
messages: Array,
now: Date,
});
function notify(title, body) {
if (props.finishedReplay) {
return new Notification(title, { body: body, requireInteraction: true });
}
}
function formatTime(time, isDuration = false) {
return new Date(time).toLocaleTimeString(
'en-GB',
isDuration ? { timeZone: 'UTC' } : {}
);
}
const timeoutLengthMs = computed(() => {
return parseDuration(props.timeoutLength);
});
function prettyDuration(duration) {
let date = new Date(duration);
return [
...Object.entries({
hours: date.getUTCHours(),
minutes: date.getUTCMinutes(),
seconds: date.getUTCSeconds(),
milliseconds: date.getUTCMilliseconds(),
}),
]
.filter(([suffix, num]) => num > 0)
.map(([suffix, num]) => num + ' ' + suffix)
.join(' ');
}
const tacticalAndOrCall = computed(() => {
return props.tactical
? `${props.tactical} [${props.callsign}]`
: props.callsign;
});
const timedOut = computed(() => {
if (stationStatus.value.lastHeard === null) {
return false;
} else {
return (
props.now.getTime() - stationStatus.value.lastHeard >
timeoutLengthMs.value
);
}
});
const isLowVoltage = computed(() => {
return (
stationStatus.value.lastVoltage &&
stationStatus.value.lastVoltage < props.lowVoltage
);
});
const stationStatus = computed(() => {
const status = {
lastHeard: null,
delta: null,
lastVoltage: null,
lastTemperature: null,
};
Object.assign(
status,
props.messages.reduce((acc, message, idx, arr) => {
acc.lastHeard = message.date.getTime();
if (idx === 0) {
acc.avgDelta = 0;
} else {
let delta = message.date.getTime() - arr[idx - 1].date.getTime();
acc.avgDelta = (acc.avgDelta * (idx - 1) + delta) / idx;
}
if ('data' in message) {
if ('analog' in message.data) {
acc.lastVoltage = message.data.analog[0] / 10;
acc.lastTemperature = message.data.analog[1];
}
acc.lastMicE = message.data.mice || acc.lastMicE;
acc.lastComment = message.data.comment || acc.lastComment;
}
return acc;
}, {})
);
return status;
});
watch(isLowVoltage, (newVal) => {
if (newVal) {
notify(
`${tacticalAndOrCall}'s battery has dropepd below ${props.lowVoltage}V`,
`Voltage: ${stationStatus.value.lastVoltage}`
);
}
});
watch(timedOut, (newVal) => {
if (newVal) {
notify(
`${tacticalAndOrCall.value} has not been heard for over ${prettyDuration(
timeoutLengthMs.value
)}!`,
`Last Heard: ${formatTime(
stationStatus.value.lastHeard
)} (${prettyDuration(
props.now.getTime() - stationStatus.value.lastHeard
)} ago!)`
);
}
});
</script>
<style>
tr.timedOut {
background-color: red;
}
tr.isLowVoltage {
background-color: yellow;
}
tr.neverHeard {
background-color: purple;
color: #eee;
}
</style>