2 Star 2 Fork 2

lamber/node-paper-calendar

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
index.js 20.08 KB
一键复制 编辑 原始数据 按行查看 历史
lxrmido 提交于 2019-12-18 13:19 . 修正历史数据存放路径
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
require('dotenv').config();
var express = require('express');
var bodyParser = require("body-parser");
var fs = require('fs');
var app = express();
var canvas = require('canvas');
var solarLunar = require('solarlunar');
var request = require('request')
var bmp = require('fast-bmp');
var path = require('path');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
let w1DeviceFile = null;
let w1DeviceDir = "/sys/bus/w1/devices/";
if (process.env.W1_DEVICE) {
w1DeviceFile = process.env.W1_DEVICE;
} else {
if (fs.existsSync(w1DeviceDir)) {
fs.readdirSync(w1DeviceDir).forEach(function (name) {
if (name.indexOf('28-') === 0) {
w1DeviceFile = w1DeviceDir + name + '/w1_slave';
}
});
}
}
if (!w1DeviceFile) {
console.log('No 1-wired device found, local report disabled.');
}
var config = {
servicePort: process.env.SERIVCE_PORT || 3000,
dataDir: process.env.DATA_DIR || 'data',
backupValuesFile: process.env.BACKUP_VALUES_FILE || 'data/values.json',
backupChangesFile: process.env.BACKUP_CHANGES_FILE || 'data/changes.json',
backupInterval: process.env.BACKUP_INTERVAL || 60000,
changesLimit: process.env.CHANGES_LIMIT || 8640,
tempKey: process.env.TEMP_KEY || 'temp',
weatherLocation: process.env.WEATHER_LOCATION,
weatherKey: process.env.WEATHER_KEY
};
var valuesMap = {
};
var changesMap = {
};
var daily = {
dir: null,
key: null,
changes: {
}
};
var weatherData = [];
if (!fs.existsSync(config.dataDir)) {
fs.mkdirSync(config.dataDir);
}
if (fs.existsSync(config.backupValuesFile)) {
valuesMap = JSON.parse(fs.readFileSync(config.backupValuesFile));
}
if (fs.existsSync(config.backupChangesFile)) {
changesMap = JSON.parse(fs.readFileSync(config.backupChangesFile));
}
initRotate();
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname + '/index.html'));
});
app.post('/set', function (req, res) {
for (let i in req.body) {
valuesMap[i] = {
value: req.body[i],
updated: (new Date()).getTime()
};
process.nextTick(function () {
addChange(i);
});
}
res.send({
result: 'success'
});
});
app.get('/get/:key', function (req, res) {
let key = req.params.key;
if (key in valuesMap) {
res.send(valuesMap[key]);
} else {
res.send({
value: null
});
}
});
app.get('/today/:key', function (req, res) {
let key = req.params.key;
if (key in daily.changes) {
res.send({
changes: daily.changes[key]
});
} else {
res.send({
changes: []
});
}
});
app.get('/changes/:key', function (req, res) {
let key = req.params.key;
if (key in changesMap) {
res.send({
changes: changesMap[key]
});
} else {
res.send({
changes: []
});
}
});
app.get('/calendar', function (req, res) {
let width = 640;
let height = 384;
let hideWeather = req.query.hideWeather && parseInt(req.query.hideWeather) > 0;
let hideTemp = req.query.hideTemp && parseInt(req.query.hideTemp) > 0;
let bit = req.query.bit && parseInt(req.query.bit) > 0;
if (req.query.width && req.query.width > 0) {
width = parseInt(req.query.width);
}
if (req.query.height && req.query.height > 0) {
height = parseInt(req.query.height);
}
let tempKey = config.tempKey;
if (req.query.tempKey) {
tempKey = req.query.tempKey;
}
let cvs = canvas.createCanvas(width, height);
let ctx = cvs.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
let calendarWidth = Math.floor(width / 3 * 2);
let calendarHeight = Math.floor(height / 4 * 3);
let calendarX = Math.floor(width / 3);
let calendarY = 0;
let tempWidth = width;
let tempHeight = Math.floor(height / 4);
let tempX = 0;
let tempY = Math.floor(height / 4 * 3);
let weatherWidth = Math.floor(width / 3);
let weatherHeight = Math.floor(height / 4 * 3);
let weatherX = 0;
let weatherY = 0;
if (hideTemp) {
calendarHeight = height;
weatherHeight = height;
}
if (hideWeather) {
calendarWidth = width;
calendarX = 0;
}
let cvsCalendar = drawCalendar(calendarWidth, calendarHeight);
ctx.drawImage(cvsCalendar, calendarX, calendarY);
if (!hideTemp) {
let cvsTemps = drawChanges(tempWidth, tempHeight, tempKey, function (cur, min, max) {
return '温度:' + (cur / 1000).toFixed(1) + '℃,过去24小时:' + (min / 1000).toFixed(1) + ' - ' + (max / 1000).toFixed(1) + '';
});
ctx.drawImage(cvsTemps, tempX, tempY);
}
if (!hideWeather) {
let cvsForecast = drawWeatherForecast(weatherWidth, weatherHeight);
ctx.drawImage(cvsForecast, weatherX, weatherY);
}
var mime, img;
if (bit) {
mime = 'image/bmp',
img = canvasToBitmap(cvs);
} else {
mime = 'image/jpeg'
img = cvs.toBuffer('image/jpeg', {quality: 1});
}
res.writeHead(200, {
'Content-Type': mime,
'Content-Length': img.length
});
res.end(img);
});
function drawCalendar(width, height){
let cvs = canvas.createCanvas(width, height);
let ctx = cvs.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = '#000000';
ctx.fillStyle = '#000000';
initContext2d(ctx);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
let date = new Date();
let dayX = 0;
let dayY = 0;
let dayHeight = Math.floor(height / 4 * 3);
let dayWidth = Math.floor(width);
let dayText = date.getDate();
let dayFont = ctx.getPropertySingleLineFont(dayText, 400, 20, null, dayWidth - 16, dayHeight - 16);
ctx.font = dayFont.font;
ctx.fillText(dayText, Math.floor(dayX + dayWidth / 2), Math.floor(dayY + dayHeight / 2) + dayFont.offsetY);
let lunarX = 0;
let lunarY = Math.floor(height / 4 * 3);
let lunarWidth = Math.floor((width - lunarX) * 2 / 3);
let lunarHeight = Math.floor(height / 4);
let lunarInfo = solarLunar.solar2lunar(date.getFullYear(), date.getMonth() + 1, date.getDate());
let lunarText = lunarInfo.monthCn + lunarInfo.dayCn;
let lunarFont = ctx.getPropertySingleLineFont(lunarText, 100, null, null, lunarWidth - 16, lunarHeight - 16);
ctx.font = lunarFont.font;
ctx.fillText(lunarText, Math.floor(lunarX + lunarWidth / 2), Math.floor(lunarY + lunarHeight / 2 + lunarFont.offsetY));
let monthX = lunarX + lunarWidth;
let monthY = lunarY;
let monthWidth = Math.floor(lunarWidth / 2);
let monthHeight = Math.floor(lunarHeight / 2);
let monthText = date.getFullYear() + '' + (date.getMonth() + 1) + '';
let monthFont = ctx.getPropertySingleLineFont(monthText, 100, null, null, monthWidth, monthHeight);
ctx.font = monthFont.font;
ctx.fillText(monthText, Math.floor(monthX + monthWidth / 2), Math.floor(monthY + monthHeight / 2 + monthFont.offsetY));
let weekX = lunarX + lunarWidth;
let weekY = lunarY + monthHeight;
let weekWidth = Math.floor(lunarWidth / 2);
let weekHeight = Math.floor(lunarHeight / 2);
let weekText = lunarInfo.ncWeek;
let weekFont = ctx.getPropertySingleLineFont(weekText, 100, null, null, weekWidth, weekHeight);
ctx.font = weekFont.font;
ctx.fillRect(weekX, weekY, weekWidth, weekHeight);
ctx.fillStyle = '#ffffff';
ctx.fillText(weekText, Math.floor(weekX + weekWidth / 2), Math.floor(weekY + weekHeight / 2 + weekFont.offsetY));
return cvs;
}
function drawChanges(width, height, key, showText){
let datas = [];
if (key in changesMap) {
datas = changesMap[key];
}
let cvs = canvas.createCanvas(width, height);
let ctx = cvs.getContext('2d');
let calcValues = [];
let pixWidth = 1, pixHeight = 1;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = '#000000';
ctx.beginPath();
initContext2d(ctx);
let numberDatas = [];
for (let i = 0; i < datas.length; i ++) {
if (isNaN(datas[i].value)) {
continue;
}
numberDatas.push(parseFloat(datas[i].value));
}
if (numberDatas.length <= 1) {
ctx.moveTo(0, Math.floor(height / 2));
ctx.lineTo(width - 1, Math.floor(height / 2));
} else {
if (width > numberDatas.length) {
pixWidth = Math.floor(width / numberDatas.length);
calcValues = numberDatas;
} else {
let valsPerPix = Math.floor(numberDatas.length / width);
let cx = 0;
while (cx < width) {
let subGroup = numberDatas.slice(cx * valsPerPix, cx * valsPerPix + valsPerPix);
if (subGroup.length > 0) {
calcValues.push(Math.round(subGroup.reduce((a, b) => a + b) / subGroup.length));
}
cx ++;
}
}
let minValue = Math.min(...calcValues);
let maxValue = Math.max(...calcValues);
if (minValue == maxValue) {
ctx.moveTo(0, Math.floor(height / 2));
ctx.lineTo(width - 1, Math.floor(height / 2));
} else {
let scaleY = height / (maxValue - minValue);
for (let i = 0; i < calcValues.length; i ++) {
ctx.lineTo(pixWidth * i, Math.floor(scaleY * (maxValue - calcValues[i])));
}
}
ctx.stroke();
if (showText) {
let text = showText(numberDatas[numberDatas.length - 1], Math.min(...numberDatas), Math.max(...numberDatas))
let textFont = ctx.getPropertySingleLineFont(text, null, null, null, width - 8, height / 4);
ctx.font = textFont.font;
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#ffffff';
ctx.fillText(text, 6, height - 6);
ctx.fillText(text, 2, height - 2);
ctx.fillStyle = '#000000';
ctx.fillText(text, 4, height - 4);
}
}
return cvs;
}
function drawWeatherForecast(width, height){
let cvs = canvas.createCanvas(width, height);
let ctx = cvs.getContext('2d');
initContext2d(ctx);
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
ctx.beginPath();
ctx.moveTo(0, Math.floor(height / 3));
ctx.lineTo(width - 1, Math.floor(height / 3));
ctx.moveTo(0, Math.floor(height / 3 * 2));
ctx.lineTo(width - 1, Math.floor(height / 3 * 2));
ctx.stroke();
ctx.fillStyle = '#000000';
let rowHeight = Math.floor(height / 3);
let labelHeight = Math.floor(rowHeight / 3);
let labelWidth = labelHeight * 2;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
let labelFont = ctx.getPropertySingleLineFont('今天', null, null, null, labelWidth, labelHeight);
ctx.fillRect(0, 0, labelWidth, labelHeight);
ctx.fillRect(0, rowHeight, labelWidth, labelHeight);
ctx.fillRect(0, rowHeight * 2, labelWidth, labelHeight);
ctx.fillStyle = '#ffffff';
ctx.font = labelFont.font;
ctx.fillText('今天', labelWidth / 2, labelHeight / 2 + labelFont.offsetY);
ctx.fillText('明天', labelWidth / 2, labelHeight / 2 + labelFont.offsetY + rowHeight);
ctx.fillText('后天', labelWidth / 2, labelHeight / 2 + labelFont.offsetY + rowHeight * 2);
ctx.fillStyle = '#000000';
if (weatherData.length) {
weatherData.forEach(function (day, index) {
let tmpText = day.tmp_min + ' ~ ' + day.tmp_max + '';
let tmpX = labelWidth;
let tmpY = index * rowHeight;
let tmpWidth = width - tmpX;
let tmpHeight = labelHeight;
let tmpFont = ctx.getPropertySingleLineFont(tmpText, null, null, null, tmpWidth - 8, tmpHeight - 8);
ctx.font = tmpFont.font;
ctx.fillText(tmpText, tmpX + tmpWidth / 2, tmpY + tmpHeight / 2 + tmpFont.offsetY);
let condText = day.cond_txt_d + ' / ' + day.cond_txt_n;
let condX = 0;
let condY = index * rowHeight + tmpHeight;
let condWidth = width;
let condHeight = labelHeight;
let condFont = ctx.getPropertySingleLineFont(condText, null, null, null, condWidth - 8, condHeight - 8);
ctx.font = condFont.font;
ctx.fillText(condText, condX + condWidth / 2, condY + condHeight / 2 + condFont.offsetY);
let sunText = '日出 ' + day.sr + ' 日落 ' + day.ss;
let sunX = 0;
let sunY = index * rowHeight + tmpHeight + condHeight;
let sunWidth = width;
let sunHeight = labelHeight;
let sunFont = ctx.getPropertySingleLineFont(sunText, null, null, null, sunWidth - 8, sunHeight - 8);
ctx.font = sunFont.font;
ctx.fillText(sunText, sunX + sunWidth / 2, sunY + sunHeight / 2 + sunFont.offsetY);
});
}
return cvs;
}
app.listen(config.servicePort, function () {
console.log('Listening on port ' + config.servicePort);
});
function getWeatherForecastData(){
if (!config.tempKey) {
console.log('No Weather Key');
return;
}
let url = 'https://free-api.heweather.net/s6/weather/forecast?location=' + config.weatherLocation + '&key=' + config.weatherKey;
request.get(url, function (err, data) {
console.log(new Date().toString())
if (err) {
console.log('Get Weather Data Failed:' + err);
setTimeout(getWeatherForecastData, 60000);
return;
}
try {
let fcData = JSON.parse(data.body);
if (fcData.HeWeather6 && fcData.HeWeather6.length && fcData.HeWeather6[0].daily_forecast) {
weatherData = fcData.HeWeather6[0].daily_forecast;
console.log('Weather data refreshed.');
} else {
console.log('Weather data format unexpected.');
console.log(data.body);
}
} catch(e) {
console.log('Weather data parsing error.');
console.log(data.body);
}
setTimeout(getWeatherForecastData, 900000);
})
}
function initContext2d(ctx){
ctx.getPropertySingleLineFont = function (text, max, min, font, width, height) {
font = font || 'Impact'
max = max || 40;
min = min || 6;
var lastFont = this.font;
var fontSize = max;
this.font = fontSize + 'px ' + font;
var metrics = this.measureText(text);
while (metrics.width > width || metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent > height) {
fontSize -= 1;
if (fontSize < min) {
this.font = lastFont;
return {
font: min + 'px ' + font,
offsetY: Math.floor((metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2),
width: metrics.width
};
}
this.font = fontSize + 'px ' + font;
metrics = this.measureText(text);
}
this.font = lastFont;
return {
font: fontSize + 'px ' + font,
offsetY: Math.floor((metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2),
width: metrics.width
};
}
}
function addChange(key){
if (!(key in changesMap)) {
changesMap[key] = [];
} else {
if (changesMap[key].length >= config.changesLimit) {
changesMap[key].splice(0, changesMap[key].length - config.changesLimit + 1);
}
}
changesMap[key].push(valuesMap[key]);
checkRotate();
if (!(key in daily.changes)) {
daily.changes[key] = [];
}
daily.changes[key].push(valuesMap[key]);
}
function getDailyKey() {
let d = new Date();
function pad(x) {
return (x > 9 ? '' : '0') + x;
}
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
}
function checkRotate(){
let curKey = getDailyKey();
if (curKey != daily.key) {
backupDaily();
daily.dir = config.dataDir + '/' + curKey;
daily.key = curKey;
daily.changes = {};
loadRotateIfExists();
}
}
function initRotate(){
let curKey = getDailyKey();
daily.dir = config.dataDir + '/' + curKey + '/';
daily.key = curKey;
loadRotateIfExists();
}
function loadRotateIfExists(){
if (!fs.existsSync(daily.dir)) {
fs.mkdirSync(daily.dir);
return false;
}
fs.readdirSync(daily.dir).forEach(function (name) {
let segs = name.split('.');
daily.changes[segs[0]] = JSON.parse(fs.readFileSync(daily.dir + name));
});
return true;
}
function backupDaily(){
if (!fs.existsSync(daily.dir)) {
fs.mkdirSync(daily.dir);
}
for (let key in daily.changes) {
fs.writeFileSync(daily.dir + '/' + key + '.json', JSON.stringify(daily.changes[key]));
}
}
function backupRuntime(){
fs.writeFileSync(config.backupValuesFile, JSON.stringify(valuesMap));
fs.writeFileSync(config.backupChangesFile, JSON.stringify(changesMap));
}
function quickBackup(){
fs.writeFile(
config.backupValuesFile,
JSON.stringify(valuesMap),
function (err1) {
if (err1) {
console.log('Backup values failed:' + err1);
}
fs.writeFile(
config.backupChangesFile,
JSON.stringify(changesMap),
function (err2) {
if (err2) {
console.log('Backup values failed:' + err2);
}
setTimeout(quickBackup, config.backupInterval);
}
)
}
);
}
function reportTemp () {
// DS18B20 may lost connect
if(fs.existsSync(w1DeviceFile)){
let timeStart = new Date().getTime();
let fileContent = fs.readFileSync(w1DeviceFile).toString();
let temp = fileContent.match(/t=(\d+)/)[1];
console.log('Temp reading cost: ' + (new Date().getTime() - timeStart) + 'ms');
timeStart = new Date().getTime();
console.log('Temp read at ' + new Date().toString() + ', value: ' + temp);
valuesMap[config.tempKey] = {
value: parseInt(temp),
updated: (new Date()).getTime()
};
process.nextTick(function () {
addChange(config.tempKey);
});
}else{
console.log('Temp read failed at ' + new Date().toString())
}
setTimeout(reportTemp, 10000);
}
function canvasToBitmap(cvs){
let buffer = cvs.toBuffer('raw');
let offset = 0;
let data = [];
var b1, b2, b3, b4, b5, b6, b7, b8;
while (offset < buffer.length) {
b1 = buffer[offset] + buffer[offset + 1] + buffer[offset + 2] < 510 ? 0b00000000 : 0b10000000;
offset += 4;
b2 = buffer[offset] + buffer[offset + 1] + buffer[offset + 2] < 510 ? 0b00000000 : 0b01000000;
offset += 4;
b3 = buffer[offset] + buffer[offset + 1] + buffer[offset + 2] < 510 ? 0b00000000 : 0b00100000;
offset += 4;
b4 = buffer[offset] + buffer[offset + 1] + buffer[offset + 2] < 510 ? 0b00000000 : 0b00010000;
offset += 4;
b5 = buffer[offset] + buffer[offset + 1] + buffer[offset + 2] < 510 ? 0b00000000 : 0b00001000;
offset += 4;
b6 = buffer[offset] + buffer[offset + 1] + buffer[offset + 2] < 510 ? 0b00000000 : 0b00000100;
offset += 4;
b7 = buffer[offset] + buffer[offset + 1] + buffer[offset + 2] < 510 ? 0b00000000 : 0b00000010;
offset += 4;
b8 = buffer[offset] + buffer[offset + 1] + buffer[offset + 2] < 510 ? 0b00000000 : 0b00000001;
offset += 4;
data.push(b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8);
}
return bmp.encode({
width: cvs.width,
height: cvs.height,
data: new Uint8Array(data),
bitDepth: 1,
components: 1,
channels: 1
})
}
setTimeout(quickBackup, config.backupInterval);
getWeatherForecastData();
if (w1DeviceFile) {
reportTemp();
}
process.on('SIGINT', (code) => {
backupRuntime();
backupDaily();
console.log('Process exit.')
process.exit('SIGINT');
});
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/lamberma/node-paper-calendar.git
git@gitee.com:lamberma/node-paper-calendar.git
lamberma
node-paper-calendar
node-paper-calendar
master

搜索帮助