mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-18 17:49:31 -07:00
Compare commits
5 Commits
main
...
personal-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
107fa26da8 | ||
|
|
afad953ed7 | ||
|
|
410880833b | ||
|
|
539e7663d6 | ||
|
|
5b5b313786 |
@@ -1,4 +1,4 @@
|
|||||||
import 'dotenv/config';
|
import { config } from 'dotenv';
|
||||||
import {
|
import {
|
||||||
src, dest, series, parallel,
|
src, dest, series, parallel,
|
||||||
} from 'gulp';
|
} from 'gulp';
|
||||||
@@ -20,6 +20,10 @@ import OVERRIDES from '../src/overrides.mjs';
|
|||||||
// get cloudfront
|
// get cloudfront
|
||||||
import reader from '../src/playlist-reader.mjs';
|
import reader from '../src/playlist-reader.mjs';
|
||||||
|
|
||||||
|
config({
|
||||||
|
path: ['gulp/.env', '.env'],
|
||||||
|
});
|
||||||
|
|
||||||
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
|
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
|
||||||
|
|
||||||
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
|
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
|
||||||
@@ -84,6 +88,10 @@ const mjsSources = [
|
|||||||
'server/scripts/index.mjs',
|
'server/scripts/index.mjs',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!process.env.DISABLE_PERSONAL) {
|
||||||
|
mjsSources.push('server/scripts/modules/personal-weather.mjs');
|
||||||
|
}
|
||||||
|
|
||||||
const buildJs = () => src(mjsSources)
|
const buildJs = () => src(mjsSources)
|
||||||
.pipe(webpack(webpackOptions))
|
.pipe(webpack(webpackOptions))
|
||||||
.pipe(dest(RESOURCES_PATH));
|
.pipe(dest(RESOURCES_PATH));
|
||||||
@@ -114,6 +122,7 @@ const compressHtml = async () => src(htmlSources)
|
|||||||
version,
|
version,
|
||||||
OVERRIDES,
|
OVERRIDES,
|
||||||
query: {},
|
query: {},
|
||||||
|
DISABLE_PERSONAL: process.env.DISABLE_PERSONAL === '1',
|
||||||
}))
|
}))
|
||||||
.pipe(rename({ extname: '.html' }))
|
.pipe(rename({ extname: '.html' }))
|
||||||
.pipe(htmlmin({ collapseWhitespace: true }))
|
.pipe(htmlmin({ collapseWhitespace: true }))
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import playlist from './src/playlist.mjs';
|
|||||||
import OVERRIDES from './src/overrides.mjs';
|
import OVERRIDES from './src/overrides.mjs';
|
||||||
import cache from './proxy/cache.mjs';
|
import cache from './proxy/cache.mjs';
|
||||||
import devTools from './src/com.chrome.devtools.mjs';
|
import devTools from './src/com.chrome.devtools.mjs';
|
||||||
|
import ambientRelay from "./src/personal-weather.mjs";
|
||||||
|
|
||||||
const travelCities = JSON.parse(await readFile('./datagenerators/output/travelcities.json'));
|
const travelCities = JSON.parse(await readFile('./datagenerators/output/travelcities.json'));
|
||||||
const regionalCities = JSON.parse(await readFile('./datagenerators/output/regionalcities.json'));
|
const regionalCities = JSON.parse(await readFile('./datagenerators/output/regionalcities.json'));
|
||||||
@@ -59,6 +60,7 @@ const renderIndex = (req, res, production = false) => {
|
|||||||
version,
|
version,
|
||||||
OVERRIDES,
|
OVERRIDES,
|
||||||
query: req.query,
|
query: req.query,
|
||||||
|
DISABLE_PERSONAL: process.env.DISABLE_PERSONAL === '1'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -170,6 +172,7 @@ if (process.env?.DIST === '1') {
|
|||||||
app.use('/resources', express.static('./server/scripts/modules'));
|
app.use('/resources', express.static('./server/scripts/modules'));
|
||||||
app.get('/', index);
|
app.get('/', index);
|
||||||
app.get('/.well-known/appspecific/com.chrome.devtools.json', devTools);
|
app.get('/.well-known/appspecific/com.chrome.devtools.json', devTools);
|
||||||
|
app.get('/ambient-relay/api/latest', ambientRelay);
|
||||||
app.get('*name', express.static('./server', staticOptions));
|
app.get('*name', express.static('./server', staticOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ const formatTimesForColumn = (times) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
const display = new Almanac(9, 'almanac');
|
const display = new Almanac(10, 'almanac');
|
||||||
registerDisplay(display);
|
registerDisplay(display);
|
||||||
|
|
||||||
export default display.getSun.bind(display);
|
export default display.getSun.bind(display);
|
||||||
|
|||||||
@@ -209,4 +209,4 @@ const shortenExtendedForecastText = (long) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new ExtendedForecast(8, 'extended-forecast'));
|
registerDisplay(new ExtendedForecast(9, 'extended-forecast'));
|
||||||
|
|||||||
@@ -148,4 +148,4 @@ const drawPath = (path, ctx, options) => {
|
|||||||
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
|
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
registerDisplay(new HourlyGraph(5, 'hourly-graph'));
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ const expand = (data, maxHours = 24) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
const display = new Hourly(3, 'hourly', false);
|
const display = new Hourly(4, 'hourly', false);
|
||||||
registerDisplay(display);
|
registerDisplay(display);
|
||||||
|
|
||||||
export default display.getHourlyData.bind(display);
|
export default display.getHourlyData.bind(display);
|
||||||
|
|||||||
@@ -205,4 +205,4 @@ const shortenCurrentConditions = (_condition) => {
|
|||||||
return condition;
|
return condition;
|
||||||
};
|
};
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new LatestObservations(2, 'latest-observations'));
|
registerDisplay(new LatestObservations(3, 'latest-observations'));
|
||||||
|
|||||||
@@ -262,4 +262,4 @@ const parse = (forecast, forecastUrl) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new LocalForecast(7, 'local-forecast'));
|
registerDisplay(new LocalForecast(8, 'local-forecast'));
|
||||||
|
|||||||
114
server/scripts/modules/personal-weather.mjs
Normal file
114
server/scripts/modules/personal-weather.mjs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
// current weather conditions display
|
||||||
|
import STATUS from './status.mjs';
|
||||||
|
import { safeJson } from './utils/fetch.mjs';
|
||||||
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
|
import { registerDisplay } from './navigation.mjs';
|
||||||
|
import {
|
||||||
|
temperature, pressure, distanceMm, windSpeed,
|
||||||
|
} from './utils/units.mjs';
|
||||||
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
|
|
||||||
|
class PersonalWeather extends WeatherDisplay {
|
||||||
|
constructor(navId, elemId) {
|
||||||
|
super(navId, elemId, 'Personal Weather Station', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getData(weatherParameters, refresh) {
|
||||||
|
// always load the data for use in the lower scroll
|
||||||
|
const superResult = super.getData(weatherParameters, refresh);
|
||||||
|
|
||||||
|
const dataUrl = '/ambient-relay/api/latest';
|
||||||
|
|
||||||
|
let personalData;
|
||||||
|
try {
|
||||||
|
personalData = await safeJson(dataUrl, {
|
||||||
|
retryCount: 3,
|
||||||
|
stillWaiting: () => this.stillWaiting(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Unexpected error getting personal weather station data from: ${dataUrl}: ${error.message}`);
|
||||||
|
}
|
||||||
|
// test for data received
|
||||||
|
if (!personalData) {
|
||||||
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||||
|
// send failed to subscribers
|
||||||
|
this.getDataCallback(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we only get here if there was no error above
|
||||||
|
this.data = parseData(personalData);
|
||||||
|
this.getDataCallback();
|
||||||
|
|
||||||
|
// stop here if we're disabled
|
||||||
|
if (!superResult) return;
|
||||||
|
|
||||||
|
// Data is available, ensure we're enabled for display
|
||||||
|
this.timing.totalScreens = 1;
|
||||||
|
this.setStatus(STATUS.loaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
async drawCanvas() {
|
||||||
|
super.drawCanvas();
|
||||||
|
|
||||||
|
const fill = {
|
||||||
|
temp: this.data.Temperature + String.fromCharCode(176),
|
||||||
|
wind: `${this.data.WindSpeed} ${this.data.WindUnit}`,
|
||||||
|
deviceName: this.data.device_name,
|
||||||
|
deviceLocation: this.data.device_location,
|
||||||
|
humidity: `${this.data.Humidity}%`,
|
||||||
|
pressure: `${this.data.Pressure} ${this.data.PressureUnit}`,
|
||||||
|
dailyRain: `${this.data.DailyRain} ${this.data.DailyRainUnit}`,
|
||||||
|
timestamp: `At ${this.data.timestamp}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const area = this.elem.querySelector('.main');
|
||||||
|
|
||||||
|
area.innerHTML = '';
|
||||||
|
area.append(this.fillTemplate('weather', fill));
|
||||||
|
|
||||||
|
this.finishDraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// make data available outside this class
|
||||||
|
// promise allows for data to be requested before it is available
|
||||||
|
async getCurrentWeather(stillWaiting) {
|
||||||
|
// an external caller has requested data, set up auto reload
|
||||||
|
this.setAutoReload();
|
||||||
|
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (this.data) resolve(this.data);
|
||||||
|
// data not available, put it into the data callback queue
|
||||||
|
this.getDataCallbacks.push(() => resolve(this.data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// format the received data
|
||||||
|
const parseData = (data) => {
|
||||||
|
// get the unit converters
|
||||||
|
const temperatureConverter = temperature('us');
|
||||||
|
const pressureConverter = pressure('us');
|
||||||
|
const inConverter = distanceMm('us');
|
||||||
|
const windConverter = windSpeed('us');
|
||||||
|
|
||||||
|
data.Pressure = pressureConverter(data.baromrelin * 10000) / 100;
|
||||||
|
data.PressureUnit = pressureConverter.units;
|
||||||
|
data.Humidity = data.humidity;
|
||||||
|
data.Temperature = temperatureConverter(data.tempf);
|
||||||
|
data.WindSpeed = windConverter(data.windspeedmph);
|
||||||
|
data.WindUnit = windConverter.units;
|
||||||
|
data.DailyRain = inConverter(data.dailyrainin);
|
||||||
|
data.DailyRainUnit = inConverter.units;
|
||||||
|
data.timestamp = DateTime.fromISO(data.date).toFormat('H:mm:ss a EEE MMM d').toUpperCase();
|
||||||
|
|
||||||
|
// set wind speed of 0 as calm
|
||||||
|
if (data.WindSpeed === 0) data.WindSpeed = 'Calm';
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const display = new PersonalWeather(2, 'personal-weather');
|
||||||
|
registerDisplay(display);
|
||||||
|
|
||||||
|
// export default display.getPersonalWeather.bind(display);
|
||||||
@@ -231,4 +231,4 @@ class Radar extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new Radar(11, 'radar'));
|
registerDisplay(new Radar(12, 'radar'));
|
||||||
|
|||||||
@@ -235,4 +235,4 @@ const getAndFormatPoint = async (lat, lon) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new RegionalForecast(6, 'regional-forecast'));
|
registerDisplay(new RegionalForecast(7, 'regional-forecast'));
|
||||||
|
|||||||
@@ -146,4 +146,4 @@ class SpcOutlook extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new SpcOutlook(10, 'spc-outlook'));
|
registerDisplay(new SpcOutlook(11, 'spc-outlook'));
|
||||||
|
|||||||
@@ -222,4 +222,4 @@ const getTravelCitiesDayName = (cities) => cities.reduce((dayName, city) => {
|
|||||||
}, '');
|
}, '');
|
||||||
|
|
||||||
// register display, not active by default
|
// register display, not active by default
|
||||||
registerDisplay(new TravelForecast(5, 'travel', false));
|
registerDisplay(new TravelForecast(6, 'travel', false));
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const fahrenheitToCelsius = (Fahrenheit) => Math.round((Fahrenheit - 32) * 5 / 9
|
|||||||
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34);
|
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34);
|
||||||
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
|
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
|
||||||
const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2);
|
const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2);
|
||||||
|
const mmToIn = (mm) => round2(mm / 25.4);
|
||||||
|
|
||||||
// each module/page/slide creates it's own unit converter as needed by providing the base units available
|
// each module/page/slide creates it's own unit converter as needed by providing the base units available
|
||||||
// the factory function then returns an appropriate converter or pass-thru function for use on the page
|
// the factory function then returns an appropriate converter or pass-thru function for use on the page
|
||||||
@@ -98,6 +99,23 @@ const distanceKilometers = (defaultUnit = 'si') => {
|
|||||||
return converter;
|
return converter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// millimeters (annoying with camel case)
|
||||||
|
const distanceMm = (defaultUnit = 'si') => {
|
||||||
|
// default to passthru
|
||||||
|
let converter = passthru();
|
||||||
|
// change the converter if there is a mismatch
|
||||||
|
if (defaultUnit !== settings.units.value) {
|
||||||
|
converter = convert((value) => Math.round(mmToIn(value)));
|
||||||
|
}
|
||||||
|
// append units
|
||||||
|
if (settings.units.value === 'si') {
|
||||||
|
converter.units = ' mm.';
|
||||||
|
} else {
|
||||||
|
converter.units = ' in.';
|
||||||
|
}
|
||||||
|
return converter;
|
||||||
|
};
|
||||||
|
|
||||||
const pressure = (defaultUnit = 'si') => {
|
const pressure = (defaultUnit = 'si') => {
|
||||||
// default to passthru (millibar)
|
// default to passthru (millibar)
|
||||||
let converter = passthru(100);
|
let converter = passthru(100);
|
||||||
@@ -121,6 +139,7 @@ export {
|
|||||||
distanceMeters,
|
distanceMeters,
|
||||||
distanceKilometers,
|
distanceKilometers,
|
||||||
pressure,
|
pressure,
|
||||||
|
distanceMm,
|
||||||
|
|
||||||
// formatter
|
// formatter
|
||||||
round2,
|
round2,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
|||||||
@use 'shared/_colors' as c;
|
@use 'shared/_colors'as c;
|
||||||
@use 'shared/_utils' as u;
|
@use 'shared/_utils'as u;
|
||||||
|
|
||||||
|
// also shared with personal weather
|
||||||
.weather-display .main.current-weather {
|
.weather-display .main.current-weather {
|
||||||
&.main {
|
&.main {
|
||||||
|
|
||||||
|
|||||||
68
server/styles/scss/_personal-weather.scss
Normal file
68
server/styles/scss/_personal-weather.scss
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
@use 'shared/_colors'as c;
|
||||||
|
@use 'shared/_utils'as u;
|
||||||
|
|
||||||
|
// also shared with personal weather
|
||||||
|
.weather-display .main.personal-weather {
|
||||||
|
&.main {
|
||||||
|
|
||||||
|
@include u.text-shadow();
|
||||||
|
|
||||||
|
font-family: "Star4000 Large";
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 24px;
|
||||||
|
top: 20px;
|
||||||
|
height: 290px;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.label,
|
||||||
|
.value {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
float: right;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp {
|
||||||
|
font-family: 'Star4000 Large';
|
||||||
|
font-size: 24pt;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceName,
|
||||||
|
.deviceLocation {
|
||||||
|
color: c.$title-color;
|
||||||
|
max-height: 32px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 15px;
|
||||||
|
right: 10px;
|
||||||
|
text-align: right;
|
||||||
|
font-family: "Star4000 Small";
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
@use 'page';
|
@use 'page';
|
||||||
@use 'weather-display';
|
@use 'weather-display';
|
||||||
@use 'current-weather';
|
@use 'current-weather';
|
||||||
|
@use 'personal-weather';
|
||||||
@use 'extended-forecast';
|
@use 'extended-forecast';
|
||||||
@use 'hourly';
|
@use 'hourly';
|
||||||
@use 'hourly-graph';
|
@use 'hourly-graph';
|
||||||
|
|||||||
20
src/personal-weather.mjs
Normal file
20
src/personal-weather.mjs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// testing data for use with personal weather stations via
|
||||||
|
// ambient-relay https://github.com/jasonkonen/ambient-relay
|
||||||
|
const ambientRelay = (req, res) => {
|
||||||
|
res.json({
|
||||||
|
"id": 123,
|
||||||
|
"mac_address": "00:00:00:00:00:00",
|
||||||
|
"device_name": "My Weather Station",
|
||||||
|
"device_location": "Backyard",
|
||||||
|
"dateutc": 1515436500000,
|
||||||
|
"date": "2018-01-08T18:35:00.000Z",
|
||||||
|
"tempf": 66.9,
|
||||||
|
"humidity": 30,
|
||||||
|
"windspeedmph": 0.9,
|
||||||
|
"baromrelin": 30.05,
|
||||||
|
"dailyrainin": 0,
|
||||||
|
"raw_data": {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ambientRelay;
|
||||||
@@ -63,6 +63,9 @@
|
|||||||
<script type="module" src="scripts/modules/settings.mjs"></script>
|
<script type="module" src="scripts/modules/settings.mjs"></script>
|
||||||
<script type="module" src="scripts/modules/media.mjs"></script>
|
<script type="module" src="scripts/modules/media.mjs"></script>
|
||||||
<script type="module" src="scripts/modules/custom-rss-feed.mjs"></script>
|
<script type="module" src="scripts/modules/custom-rss-feed.mjs"></script>
|
||||||
|
<% if (!DISABLE_PERSONAL) { %>
|
||||||
|
<script type="module" src="scripts/modules/personal-weather.mjs"></script>
|
||||||
|
<% } %>
|
||||||
<script type="module" src="scripts/index.mjs"></script>
|
<script type="module" src="scripts/index.mjs"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
@@ -109,6 +112,11 @@
|
|||||||
<div id="current-weather-html" class="weather-display">
|
<div id="current-weather-html" class="weather-display">
|
||||||
<%- include('partials/current-weather.ejs') %>
|
<%- include('partials/current-weather.ejs') %>
|
||||||
</div>
|
</div>
|
||||||
|
<% if (!DISABLE_PERSONAL) { %>
|
||||||
|
<div id="personal-weather-html" class="weather-display">
|
||||||
|
<%- include('partials/personal-weather.ejs') %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
<div id="local-forecast-html" class="weather-display">
|
<div id="local-forecast-html" class="weather-display">
|
||||||
<%- include('partials/local-forecast.ejs') %>
|
<%- include('partials/local-forecast.ejs') %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
25
views/partials/personal-weather.ejs
Normal file
25
views/partials/personal-weather.ejs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<%- include('header.ejs', {titleDual:{ top: 'Personal' , bottom: 'Weather Station' }, noaaLogo: false, hasTime: true}) %>
|
||||||
|
<div class="main has-scroll has-box personal-weather">
|
||||||
|
<div class="weather template">
|
||||||
|
<div class="deviceName value"></div>
|
||||||
|
<div class="deviceLocation value"></div>
|
||||||
|
<div class="temp value"></div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="label">Humidity:</div>
|
||||||
|
<div class="humidity value"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="label">Wind:</div>
|
||||||
|
<div class="wind value"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="label">Pressure:</div>
|
||||||
|
<div class="pressure value"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="label">Daily Rain:</div>
|
||||||
|
<div class="dailyRain value"></div>
|
||||||
|
</div>
|
||||||
|
<div class="timestamp value">At 12:34:55 PM</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user