wlbot 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -0
- package/README.md +108 -0
- package/commands/config.js +21 -0
- package/commands/metadata/catalog.js +40 -0
- package/commands/metadata/mine.js +46 -0
- package/commands/metadata/stations.js +40 -0
- package/commands/status.js +39 -0
- package/commands/weather/current.js +40 -0
- package/commands/weather/historic.js +46 -0
- package/index.js +81 -0
- package/lib/utils.js +74 -0
- package/package.json +50 -0
- package/test/utils.test.js +108 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2023 Michael Weiner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# WeatherLink Bot (wlbot)
|
|
2
|
+
|
|
3
|
+
`wlbot` is a Node.js based CLI that can be used to make *some* of the most common API calls that are possible with the [WeatherLink v2 API](https://weatherlink.github.io/v2-api/api-reference).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Obtaining a WeatherLink API Key](#obtaining-a-weatherlink-api-key)
|
|
7
|
+
- [Setting Your Environmental Variables](#setting-your-environmental-variables)
|
|
8
|
+
- [Installing `wlbot` via npm](#installing-wlbot-via-npm)
|
|
9
|
+
- [Installing `wlbot` Manually via the Github Repository](#installing-wlbot-manually-via-the-github-repository)
|
|
10
|
+
- [Install Dependencies](#install-dependencies)
|
|
11
|
+
- [Clone Repo](#clone-repo)
|
|
12
|
+
- [Install npm Packages](#install-npm-packages)
|
|
13
|
+
- [Installing CLI Globally](#installing-cli-globally)
|
|
14
|
+
- [Calling the CLI](#calling-the-cli)
|
|
15
|
+
- [Contributing](#contributing)
|
|
16
|
+
- [License](#license)
|
|
17
|
+
- [References](#references)
|
|
18
|
+
|
|
19
|
+
## Obtaining a WeatherLink API Key
|
|
20
|
+
No matter how you install `wlbot`, it requires that you have a WeatherLink v2 API Key and Secret.
|
|
21
|
+
|
|
22
|
+
The WeatherLink Developer Portal outlines how to get your API key at [https://weatherlink.github.io/v2-api/tutorial](https://weatherlink.github.io/v2-api/tutorial):
|
|
23
|
+
|
|
24
|
+
> To retrieve your WeatherLink v2 API Key and API Secret you can go to WeatherLink.com and visit the Account page at [https://www.weatherlink.com/account](https://www.weatherlink.com/account).
|
|
25
|
+
>
|
|
26
|
+
> Once on the Account page you can click the Generate v2 Key button to create a new WeatherLink v2 API Key and API Secret.
|
|
27
|
+
|
|
28
|
+
**Note:** Your API Secret should ***never*** be shared with anyone.
|
|
29
|
+
|
|
30
|
+
## Setting Your Environmental Variables
|
|
31
|
+
No matter how you install `wlbot`, it requires several **permanent** environmental variables to be set. Before running the CLI locally for this first time, you must set the necessary environment variables. The table below specifies the name and value of the environment variables that are required.
|
|
32
|
+
|
|
33
|
+
| Environment Variable Name | Environment Variable Value |
|
|
34
|
+
| ----------------------------| --------------------------------- |
|
|
35
|
+
| `WEATHER_LINK_API_KEY` | `<your_api_key>` |
|
|
36
|
+
| `WEATHER_LINK_API_SECRET` | `<your_api_secret>` |
|
|
37
|
+
| `WEATHER_LINK_BASE_API_URL` | `https://api.weatherlink.com/v2/` |
|
|
38
|
+
|
|
39
|
+
**Note:** `<your_api_key>` and `<your_api_secret>` should be replaced with the API Key and API Secret that you where given when creating your WeatherLink API as described above.
|
|
40
|
+
|
|
41
|
+
This article describes how you can [set permanent environment variables on macos or Linux](https://apple.stackexchange.com/questions/356441/how-to-add-permanent-environment-variable-in-zsh).
|
|
42
|
+
|
|
43
|
+
This article describes how you can [set permanent environment variables on Windows Operating Systems](https://www3.ntu.edu.sg/home/ehchua/programming/howto/Environment_Variables.html#zz-2.).
|
|
44
|
+
|
|
45
|
+
## Installing `wlbot` via npm
|
|
46
|
+
`wlbot` is listed on the `npm` repository at [https://npmjs.com/package/wlbot](https://npmjs.com/package/wlbot).
|
|
47
|
+
|
|
48
|
+
You can globally install the `wlbot` CLI by:
|
|
49
|
+
1. Opening a command line prompt on your machine and running `npm install -g wlbot`.
|
|
50
|
+
|
|
51
|
+
This process could take a minute or two, depending on your internet connection.
|
|
52
|
+
|
|
53
|
+
## Installing `wlbot` Manually via the Github Repository
|
|
54
|
+
It is easy to get a copy of the `wlbot` CLI running locally.
|
|
55
|
+
|
|
56
|
+
### Install Dependencies
|
|
57
|
+
In order to run `wlbot` locally, the following will need to be installed on your machine:
|
|
58
|
+
- [Node.js](https://nodejs.dev) (required)
|
|
59
|
+
- Node.js is required to run `wlbot`
|
|
60
|
+
- **Installation Instructions:** [https://nodejs.org/en/download/](https://nodejs.org/en/download/)
|
|
61
|
+
- [npm](https://www.npmjs.com) (required)
|
|
62
|
+
- npm is used to manage the packages needed by `wlbot` to run on top of Node.js
|
|
63
|
+
- **Installation Instructions:** [https://docs.npmjs.com/downloading-and-installing-node-js-and-npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
|
64
|
+
|
|
65
|
+
### Clone Repo
|
|
66
|
+
Navigate to the location on your development machine where you want to place this project's directory and clone the repository by running the following command:
|
|
67
|
+
|
|
68
|
+
git clone https://github.com/mike-weiner/wlbot.git
|
|
69
|
+
|
|
70
|
+
### Install npm Packages
|
|
71
|
+
`wlbot` requires several packages to run on top of Node.js. Those packages can be installed by the following command at the root of the directory for this project running:
|
|
72
|
+
|
|
73
|
+
npm install
|
|
74
|
+
|
|
75
|
+
### Installing CLI Globally
|
|
76
|
+
Once you have all of the environment variables required by the CLI set, we need to globally install the CLI in npm. This will allow you to call `wlbot` on your command line without being in this project's directory.
|
|
77
|
+
|
|
78
|
+
To globally install the `wlbot` package, navigate to the project's root directory on your command line. This should be the directory where this README is found. Then run the following command:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
npm i -g .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Note:** It is not required that you install `wlbot` globally in npm. However, if it is not installed globally you will only be able to call the `wlbot` command when you are in the directory where you have this project located.
|
|
85
|
+
|
|
86
|
+
## Calling the CLI
|
|
87
|
+
Congrats! You should now be able to open up a new terminal window and run `wlbot`. You can run `wlbot -h` to get help understanding how to use this CLI.
|
|
88
|
+
|
|
89
|
+
`wlbot` contains a helpful command to ensure that your environmental variables are set correctly. Run `wlbot config` to print out the values of the environmental variables that are required by `wlbot`.
|
|
90
|
+
|
|
91
|
+
You can run `wlbot config -l` to list the full values of the environmental variables. **Note:** Be careful running this command as your WeatherLink API Secret's value will be shown. You don't want to share this with ***anyone***.
|
|
92
|
+
|
|
93
|
+
### Common Commands
|
|
94
|
+
There are several commands that you will probably call more frequently than others. Some of the more common commands include:
|
|
95
|
+
- `wlbot metadata mine`: This command will return an array of `station-id` numbers for the weather station's that your WeatherLink account has permission to view data for.
|
|
96
|
+
- `wlbot weather current <station-id>`: This command will return the current weather record for the station whose station id matches `<station-id>`.
|
|
97
|
+
|
|
98
|
+
## Contributing
|
|
99
|
+
All contributions are welcome! First, search open issues to see if a ticket has already been created for the issue or feature request that you have. If a ticket does not already exist, open an issue to discuss what contributions you would like to make. All contributions should be developed in a `feature/` branch as a PR will be required before any changes are merged into the `main` branch.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
Distributed under the MIT License. See `LICENSE.txt` for more information.
|
|
103
|
+
|
|
104
|
+
## References
|
|
105
|
+
Below are several references that were used to help find inspiration for this project, get a starting point for the CLI, and serve as a resource for the WeatherLink API.
|
|
106
|
+
- [WeatherLink Developer Portal](https://weatherlink.github.io)
|
|
107
|
+
- [WeatherLink Portal](https://www.weatherlink.com)
|
|
108
|
+
- [How to Build a Command Line Interface (CLI) Using Node.js](https://cheatcode.co/tutorials/how-to-build-a-command-line-interface-cli-using-node-js)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
|
|
4
|
+
export default (options) => {
|
|
5
|
+
const spinner = !options.raw ? ora('Reading Environment Variables').start() : undefined;
|
|
6
|
+
|
|
7
|
+
let evValues = {
|
|
8
|
+
"WEATHER_LINK_API_KEY": process.env.WEATHER_LINK_API_KEY,
|
|
9
|
+
"WEATHER_LINK_API_SECRET": process.env.WEATHER_LINK_API_SECRET,
|
|
10
|
+
"WEATHER_LINK_BASE_API_URL": process.env.WEATHER_LINK_BASE_API_URL,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!options.list) {
|
|
14
|
+
for (const [key, value] of Object.entries(evValues)) {
|
|
15
|
+
evValues[key] = evValues[key].slice(0, 3) + "*".repeat(value.length - evValues[key].slice(0, 3).length)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
spinner.succeed(chalk.green.bold(`Environment Variables Retrieved`));
|
|
20
|
+
return console.log(JSON.stringify(evValues, undefined, 2))
|
|
21
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
|
|
5
|
+
import { buildWeatherLinkApiUrl, checkForRequired } from '../../lib/utils.js';
|
|
6
|
+
|
|
7
|
+
export default (options) => {
|
|
8
|
+
const spinner = !options.raw ? ora('Retrieving Sensor Catalog').start() : undefined;
|
|
9
|
+
|
|
10
|
+
const envVars = checkForRequired(["WEATHER_LINK_API_KEY", "WEATHER_LINK_API_SECRET", "WEATHER_LINK_BASE_API_URL"])
|
|
11
|
+
if (!envVars.exist) {
|
|
12
|
+
return console.log(`${chalk.red.bold(`Missing Environment Variable(s):`)} ${envVars.missing.join(", ")}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const API_KEY = process.env.WEATHER_LINK_API_KEY;
|
|
16
|
+
|
|
17
|
+
axios.get(
|
|
18
|
+
buildWeatherLinkApiUrl(
|
|
19
|
+
`sensor-catalog`,
|
|
20
|
+
{ "api-key": API_KEY, "t": String(Math.round(Date.now() / 1000)) },
|
|
21
|
+
{ "api-key": API_KEY, "t": String(Math.round(Date.now() / 1000)) }
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
.then((response) => {
|
|
25
|
+
if (options.raw) {
|
|
26
|
+
return console.log(JSON.stringify(response.data));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spinner.succeed(chalk.green.bold(`Catalog Retrieved`));
|
|
30
|
+
return console.dir(response.data, { depth: null })
|
|
31
|
+
})
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
if (options.raw) {
|
|
34
|
+
return console.log(JSON.stringify(error.response.data));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
spinner.fail('Failed to Retrieve Catalog');
|
|
38
|
+
return console.log(`${chalk.red.bold(`Error ${error.response.status}:`)} ${error.response.data.message}`);
|
|
39
|
+
})
|
|
40
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
|
|
5
|
+
import { buildWeatherLinkApiUrl, checkForRequired } from '../../lib/utils.js';
|
|
6
|
+
|
|
7
|
+
export default (options) => {
|
|
8
|
+
const spinner = !options.raw ? ora('Searching for Stations').start() : undefined;
|
|
9
|
+
|
|
10
|
+
const envVars = checkForRequired(["WEATHER_LINK_API_KEY", "WEATHER_LINK_API_SECRET", "WEATHER_LINK_BASE_API_URL"])
|
|
11
|
+
if (!envVars.exist) {
|
|
12
|
+
return console.log(`${chalk.red.bold(`Missing Environment Variable(s):`)} ${envVars.missing.join(", ")}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const API_KEY = process.env.WEATHER_LINK_API_KEY;
|
|
16
|
+
|
|
17
|
+
axios.get(
|
|
18
|
+
buildWeatherLinkApiUrl(
|
|
19
|
+
'stations',
|
|
20
|
+
{ "api-key": API_KEY, "t": String(Math.round(Date.now() / 1000)) },
|
|
21
|
+
{ "api-key": API_KEY, "t": String(Math.round(Date.now() / 1000)) }
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
.then((response) => {
|
|
25
|
+
|
|
26
|
+
if (options.raw) {
|
|
27
|
+
return console.log(JSON.stringify(response.data));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let stationIdList = [];
|
|
31
|
+
response.data.stations.forEach(station => {
|
|
32
|
+
stationIdList.push(station.station_id);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
spinner.succeed(chalk.green.bold(`${stationIdList.length} Station(s) Found`));
|
|
36
|
+
return console.log(stationIdList);
|
|
37
|
+
})
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
if (options.raw) {
|
|
40
|
+
return console.log(JSON.stringify(error.response.data));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
spinner.fail('Unable to Find Stations');
|
|
44
|
+
return console.log(`${chalk.red.bold(`Error ${error.response.status}:`)} ${error.response.data.message}`);
|
|
45
|
+
})
|
|
46
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
|
|
5
|
+
import { buildWeatherLinkApiUrl, checkForRequired } from '../../lib/utils.js';
|
|
6
|
+
|
|
7
|
+
export default (stationIds, options) => {
|
|
8
|
+
const spinner = !options.raw ? ora('Searching for Stations').start() : undefined;
|
|
9
|
+
|
|
10
|
+
const envVars = checkForRequired(["WEATHER_LINK_API_KEY", "WEATHER_LINK_API_SECRET", "WEATHER_LINK_BASE_API_URL"])
|
|
11
|
+
if (!envVars.exist) {
|
|
12
|
+
return console.log(`${chalk.red.bold(`Missing Environment Variable(s):`)} ${envVars.missing.join(", ")}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const API_KEY = process.env.WEATHER_LINK_API_KEY;
|
|
16
|
+
|
|
17
|
+
axios.get(
|
|
18
|
+
buildWeatherLinkApiUrl(
|
|
19
|
+
`stations/${stationIds}`,
|
|
20
|
+
{ "api-key": API_KEY, "station-ids": String(stationIds), "t": String(Math.round(Date.now() / 1000)) },
|
|
21
|
+
{ "api-key": API_KEY, "t": String(Math.round(Date.now() / 1000)) }
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
.then((response) => {
|
|
25
|
+
if (options.raw) {
|
|
26
|
+
return console.log(JSON.stringify(response.data));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spinner.succeed(chalk.green.bold(`${response.data.stations.length} Station(s) Found`));
|
|
30
|
+
return console.log(response.data);
|
|
31
|
+
})
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
if (options.raw) {
|
|
34
|
+
return console.log(JSON.stringify(error.response.data));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
spinner.fail('Unable to Find Stations');
|
|
38
|
+
return console.log(`${chalk.red.bold(`Error ${error.response.status}:`)} ${error.response.data.message}`);
|
|
39
|
+
})
|
|
40
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
export default function(service, options) {
|
|
4
|
+
|
|
5
|
+
// Mapping of wlbot's shorthand for service names to Davis Instrument's official names.
|
|
6
|
+
const fullServiceName = {
|
|
7
|
+
'api' : 'APIs and Data Feeds',
|
|
8
|
+
'dataingest' : 'Data Ingestion',
|
|
9
|
+
'mobile' : 'Mobile Applications',
|
|
10
|
+
'syscomms' : 'System Communication',
|
|
11
|
+
'website' : 'WeatherLink Website'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
axios.get(
|
|
16
|
+
'https://0886445102835570.hostedstatus.com/1.0/status/600712dea9c1290530967bc6'
|
|
17
|
+
)
|
|
18
|
+
.then((response) => {
|
|
19
|
+
|
|
20
|
+
var requestedDavisServices = service === 'all' ?
|
|
21
|
+
response.data.result.status
|
|
22
|
+
:
|
|
23
|
+
response.data.result.status.filter(davisService => {
|
|
24
|
+
return davisService.name === fullServiceName[service];
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return requestedDavisServices;
|
|
28
|
+
|
|
29
|
+
})
|
|
30
|
+
.catch((error) => {
|
|
31
|
+
return ({
|
|
32
|
+
'error': {
|
|
33
|
+
'msg': `${error.response.data.message}`,
|
|
34
|
+
'status': `${error.response.status}`
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
|
|
5
|
+
import { buildWeatherLinkApiUrl, checkForRequired } from '../../lib/utils.js';
|
|
6
|
+
|
|
7
|
+
export default (stationId, options) => {
|
|
8
|
+
const spinner = !options.raw ? ora('Retrieving Current Weather Data').start() : undefined;
|
|
9
|
+
|
|
10
|
+
const envVars = checkForRequired(["WEATHER_LINK_API_KEY", "WEATHER_LINK_API_SECRET", "WEATHER_LINK_BASE_API_URL"])
|
|
11
|
+
if (!envVars.exist) {
|
|
12
|
+
return console.log(`${chalk.red.bold(`Missing Environment Variable(s):`)} ${envVars.missing.join(", ")}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const API_KEY = process.env.WEATHER_LINK_API_KEY;
|
|
16
|
+
|
|
17
|
+
axios.get(
|
|
18
|
+
buildWeatherLinkApiUrl(
|
|
19
|
+
`current/${stationId}`,
|
|
20
|
+
{ "api-key": API_KEY, "station-id": String(stationId), "t": String(Math.round(Date.now() / 1000)) },
|
|
21
|
+
{ "api-key": API_KEY, "t": String(Math.round(Date.now() / 1000)) }
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
.then((response) => {
|
|
25
|
+
if (options.raw) {
|
|
26
|
+
return console.log(JSON.stringify(response.data));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spinner.succeed(chalk.green.bold(`Current Weather Data Retrieved`));
|
|
30
|
+
return console.dir(response.data, { depth: null })
|
|
31
|
+
})
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
if (options.raw) {
|
|
34
|
+
return console.log(JSON.stringify(error.response.data));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
spinner.fail('Failed to Retrieve Current Weather Data');
|
|
38
|
+
return console.log(`${chalk.red.bold(`Error ${error.response.status}:`)} ${error.response.data.message}`);
|
|
39
|
+
})
|
|
40
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
|
|
5
|
+
import { buildWeatherLinkApiUrl, checkForRequired, dateRangeIsValid } from '../../lib/utils.js';
|
|
6
|
+
|
|
7
|
+
export default (stationId, startTimestamp, endTimestamp, options) => {
|
|
8
|
+
const spinner = !options.raw ? ora('Retrieving Historical Weather Data').start() : undefined;
|
|
9
|
+
|
|
10
|
+
const envVars = checkForRequired(["WEATHER_LINK_API_KEY", "WEATHER_LINK_API_SECRET", "WEATHER_LINK_BASE_API_URL"])
|
|
11
|
+
if (!envVars.exist) {
|
|
12
|
+
return console.log(`${chalk.red.bold(`Missing Environment Variable(s):`)} ${envVars.missing.join(", ")}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const resultsOfDateRangeCheck = dateRangeIsValid(startTimestamp, endTimestamp);
|
|
16
|
+
|
|
17
|
+
if (!resultsOfDateRangeCheck.isValid) {
|
|
18
|
+
return console.log(`${chalk.red.bold(`Error:`)} ${resultsOfDateRangeCheck.msg}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const API_KEY = process.env.WEATHER_LINK_API_KEY;
|
|
22
|
+
|
|
23
|
+
axios.get(
|
|
24
|
+
buildWeatherLinkApiUrl(
|
|
25
|
+
`historic/${stationId}`,
|
|
26
|
+
{ "api-key": API_KEY, "end-timestamp": endTimestamp, "start-timestamp": startTimestamp, "station-id": String(stationId), "t": String(Math.round(Date.now() / 1000)) },
|
|
27
|
+
{ "api-key": API_KEY, "end-timestamp": endTimestamp, "start-timestamp": startTimestamp, "t": String(Math.round(Date.now() / 1000)) }
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
.then((response) => {
|
|
31
|
+
if (options.raw) {
|
|
32
|
+
return console.log(JSON.stringify(response.data));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
spinner.succeed(chalk.green.bold(`Historical Weather Data Retrieved`));
|
|
36
|
+
return console.dir(response.data, { depth: null })
|
|
37
|
+
})
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
if (options.raw) {
|
|
40
|
+
return console.log(JSON.stringify(error.response.data));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
spinner.fail('Failed to Retrieve Historical Weather Data');
|
|
44
|
+
return console.log(`${chalk.red.bold(`Error ${error.response.status}:`)} ${error.response.data.message}`);
|
|
45
|
+
})
|
|
46
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command, Argument } from 'commander';
|
|
4
|
+
|
|
5
|
+
import catalog from './commands/metadata/catalog.js';
|
|
6
|
+
import mine from './commands/metadata/mine.js';
|
|
7
|
+
import stations from './commands/metadata/stations.js';
|
|
8
|
+
|
|
9
|
+
import current from './commands/weather/current.js';
|
|
10
|
+
import historic from './commands/weather/historic.js';
|
|
11
|
+
|
|
12
|
+
import config from './commands/config.js';
|
|
13
|
+
|
|
14
|
+
import status from './commands/status.js';
|
|
15
|
+
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.description("A CLI for the WeatherLink Live API.")
|
|
20
|
+
.name("wlbot")
|
|
21
|
+
.version('1.1.0')
|
|
22
|
+
.usage('<command>');
|
|
23
|
+
|
|
24
|
+
const metadata = program.command("metadata")
|
|
25
|
+
.description("Subcommand for accessing all metadata routes from the WeatherLink v2 API.");
|
|
26
|
+
|
|
27
|
+
metadata.command("catalog")
|
|
28
|
+
.description("Get a catalog of all available sensor types and the data reported by each sensor.")
|
|
29
|
+
.option("-r, --raw", "Display the raw response from the WeatherLink API.")
|
|
30
|
+
.action(catalog);
|
|
31
|
+
|
|
32
|
+
metadata.command("mine")
|
|
33
|
+
.description("Returns an array of Weather Station Id(s) that are associated with your WeatherLink API Key.")
|
|
34
|
+
.option("-r, --raw", "Display the raw response from the WeatherLink API.")
|
|
35
|
+
.action(mine);
|
|
36
|
+
|
|
37
|
+
metadata.command("stations")
|
|
38
|
+
.description("Returns all available information about 1 or more weather stations associated with your WeatherLink API Key.")
|
|
39
|
+
.argument('<station-ids>', 'A comma-separated list of Weather Station Id(s) that you want information about.')
|
|
40
|
+
.option("-r, --raw", "Display the raw response from the WeatherLink API.")
|
|
41
|
+
.action(stations);
|
|
42
|
+
|
|
43
|
+
const weather = program.command("weather")
|
|
44
|
+
.description("Subcommand for accessing all weather routes from the WeatherLink v2 API.");
|
|
45
|
+
|
|
46
|
+
weather.command("current")
|
|
47
|
+
.description("Get the current weather data for 1 weather station associated with your WeatherLink API Key.")
|
|
48
|
+
.argument('<station-id>', 'The Station ID of the weather station that you want current weather data for.')
|
|
49
|
+
.option("-r, --raw", "Display the raw response from the WeatherLink API.")
|
|
50
|
+
.action(current);
|
|
51
|
+
|
|
52
|
+
weather.command("historic")
|
|
53
|
+
.description("Get the historical weather data for 1 weather station associated with your WeatherLink API Key within a 24hr period.")
|
|
54
|
+
.argument('<station-id>', 'The Station ID of the weather station that you want current weather data for.')
|
|
55
|
+
.argument('<start-timestamp>', 'A Unix timestamp marking the beginning of the historical period (must be earlier than end-timestamp but not more than 24 hours earlier).')
|
|
56
|
+
.argument('<end-timestamp>', 'A Unix timestamp marking the end of the historical period (must be later than start-timestamp but not more than 24 hours later).')
|
|
57
|
+
.option("-r, --raw", "Display the raw response from the WeatherLink API.")
|
|
58
|
+
.action(historic);
|
|
59
|
+
|
|
60
|
+
program.command("config")
|
|
61
|
+
.description("Display the value(s) of the environment variable(s) being consumed.")
|
|
62
|
+
.option("-l, --list", "List the complete values of the environment variables.")
|
|
63
|
+
.action(config);
|
|
64
|
+
|
|
65
|
+
program.command("status")
|
|
66
|
+
.description("Retrieves the operational status(es) of Davis Instrument's services.")
|
|
67
|
+
.addArgument(new Argument('[service]', 'The Davis Instrument service that you want to obtain the operational status of.').choices(['all', 'api', 'dataingest', 'mobile', 'syscomms', 'website']).default('all'))
|
|
68
|
+
.action((service, options) => {
|
|
69
|
+
status(service, options).then((result) => {
|
|
70
|
+
if (!Array.isArray(result)) {
|
|
71
|
+
console.log('Error: ' + result.error.msg);
|
|
72
|
+
} else {
|
|
73
|
+
result.forEach((requestedService) => {
|
|
74
|
+
console.log(requestedService.name + ' is ' + requestedService.status + ' (Status Code: ' + requestedService.status_code + ')');
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
program.commands.sort((a, b) => a._name.localeCompare(b._name));
|
|
81
|
+
program.parse(process.argv);
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build and return the URL to query the WeatherLink Live API with.
|
|
5
|
+
*
|
|
6
|
+
* @param {String} endpoint The endpoint in the WeatherLink Live API that you want to query.
|
|
7
|
+
* @param {Object} signature_parameters An object containing the required information to build the API Signature for each request.
|
|
8
|
+
* @param {Object} uri_parameters An object containing the required query parameters for the requested endpoint to query.
|
|
9
|
+
* @return {String} The string to make the API call with to retrieve the requested data.
|
|
10
|
+
*/
|
|
11
|
+
export function buildWeatherLinkApiUrl(endpoint, signature_parameters, uri_parameters) {
|
|
12
|
+
const BASE_URL = process.env.WEATHER_LINK_BASE_API_URL;
|
|
13
|
+
const API_SECRET = process.env.WEATHER_LINK_API_SECRET;
|
|
14
|
+
|
|
15
|
+
var apiSignature = "";
|
|
16
|
+
for (const key in signature_parameters) {
|
|
17
|
+
apiSignature = apiSignature + key + signature_parameters[key]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var hmac = crypto.createHmac('sha256', API_SECRET);
|
|
21
|
+
var hashedData = hmac.update(apiSignature).digest('hex');
|
|
22
|
+
|
|
23
|
+
var apiRequestURL = BASE_URL + endpoint + "?";
|
|
24
|
+
Object.keys(uri_parameters).forEach((key, index) => {
|
|
25
|
+
if (index === 0) {
|
|
26
|
+
apiRequestURL = apiRequestURL + key + "=" + uri_parameters[key];
|
|
27
|
+
} else {
|
|
28
|
+
apiRequestURL = apiRequestURL + "&" + key + "=" + uri_parameters[key];
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
apiRequestURL = apiRequestURL + "&api-signature=" + hashedData;
|
|
33
|
+
return apiRequestURL;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check that the required environmental variables are defined to complete the specific action.
|
|
38
|
+
*
|
|
39
|
+
* @param {Array} envVars An array of the names of the environmental variables to check for values.
|
|
40
|
+
* @return {Object} An object containing whether all
|
|
41
|
+
*/
|
|
42
|
+
export function checkForRequired(envVars) {
|
|
43
|
+
let missingEnvVars = []
|
|
44
|
+
|
|
45
|
+
for (const envVar of envVars) {
|
|
46
|
+
if (!process.env[envVar]){
|
|
47
|
+
missingEnvVars.push(envVar)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (missingEnvVars.length != 0) {
|
|
52
|
+
return {"exist": false, "missing": missingEnvVars};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {"exist": true, "missing": missingEnvVars};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Determines if the start and end timestamps the user provides are valid.
|
|
60
|
+
*
|
|
61
|
+
* @param {Integer} startTimestamp A Unix timestamp marking the beginning of the historical period (must be earlier than end-timestamp but not more than 24 hours earlier).
|
|
62
|
+
* @param {Integer} endTimestamp A Unix timestamp marking the end of the historical period (must be later than start-timestamp but not more than 24 hours later).
|
|
63
|
+
*/
|
|
64
|
+
export function dateRangeIsValid(startTimestamp, endTimestamp) {
|
|
65
|
+
if (startTimestamp <= 0 || endTimestamp <= 0) {
|
|
66
|
+
return {isValid: false, msg:"start-timestamp and end-timestamp must be greater than 0."};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (startTimestamp >= endTimestamp) {
|
|
70
|
+
return {isValid: false, msg:"start-timestamp must be less than end-timestamp."};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {isValid: true, msg:""};
|
|
74
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wlbot",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A CLI for the WeatherLink Live API.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Davis Instruments",
|
|
7
|
+
"WeatherLink v2 API",
|
|
8
|
+
"WeatherLink API",
|
|
9
|
+
"Weather Link API CLI",
|
|
10
|
+
"WeatherLink",
|
|
11
|
+
"WeatherLink CLI",
|
|
12
|
+
"weather cli"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/mike-weiner/wlbot",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/mike-weiner/wlbot/issues",
|
|
17
|
+
"email": "michael.weiner@hey.com"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Michael Weiner",
|
|
22
|
+
"email": "michael.weiner@hey.com",
|
|
23
|
+
"url": "https://michaelweiner.org"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./index.js",
|
|
27
|
+
"bin": {
|
|
28
|
+
"wlbot": "./index.js"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/mike-weiner/wlbot.git"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "mocha"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"axios": "^0.27.2",
|
|
39
|
+
"chalk": "^5.0.1",
|
|
40
|
+
"commander": "^9.4.0",
|
|
41
|
+
"ora": "^6.1.2"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"chai": "^4.3.6",
|
|
45
|
+
"mocha": "^10.0.0"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=16"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { buildWeatherLinkApiUrl, checkForRequired, dateRangeIsValid } from '../lib/utils.js';
|
|
2
|
+
import chai from 'chai';
|
|
3
|
+
|
|
4
|
+
describe('buildWeatherLinkApiUrl', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Set the environment variables
|
|
7
|
+
process.env.WEATHER_LINK_API_KEY = 'sampleKey';
|
|
8
|
+
process.env.WEATHER_LINK_API_SECRET = 'sampleSecret';
|
|
9
|
+
process.env.WEATHER_LINK_BASE_API_URL = 'https://api.weatherlink.com/v2/';
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('Endpoint: current/13', () => {
|
|
13
|
+
chai.expect(
|
|
14
|
+
buildWeatherLinkApiUrl(
|
|
15
|
+
`current/13`,
|
|
16
|
+
{'api-key': process.env.WEATHER_LINK_API_KEY, 'station-id': String(13), 't': String(1660759469)},
|
|
17
|
+
{'api-key': process.env.WEATHER_LINK_API_KEY, 't': String(1660759469)}
|
|
18
|
+
)
|
|
19
|
+
).to.equal('https://api.weatherlink.com/v2/current/13?api-key=sampleKey&t=1660759469&api-signature=dcf2f0eafc43ec8b6d6a1a7acfc6faa44f9bc1045124d7d6416f2f486ff72e95')
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('Endpoint: stations', () => {
|
|
23
|
+
chai.expect(
|
|
24
|
+
buildWeatherLinkApiUrl(
|
|
25
|
+
'stations',
|
|
26
|
+
{'api-key': process.env.WEATHER_LINK_API_KEY, 't': String(1660757289)},
|
|
27
|
+
{'api-key': process.env.WEATHER_LINK_API_KEY, 't': String(1660757289)}
|
|
28
|
+
)
|
|
29
|
+
).to.equal('https://api.weatherlink.com/v2/stations?api-key=sampleKey&t=1660757289&api-signature=1663a50336ae4b7d975e322ad010e297d4ec487c5d3d9d4dad1d8a4a9e53d606')
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('Endpoint: stations/13,14,15', () => {
|
|
33
|
+
chai.expect(
|
|
34
|
+
buildWeatherLinkApiUrl(
|
|
35
|
+
`stations/13,14,15`,
|
|
36
|
+
{'api-key': process.env.WEATHER_LINK_API_KEY, 'station-ids': '13,14,15', 't': String(1660759608)},
|
|
37
|
+
{'api-key': process.env.WEATHER_LINK_API_KEY, 't': String(1660759608)}
|
|
38
|
+
)
|
|
39
|
+
).to.equal('https://api.weatherlink.com/v2/stations/13,14,15?api-key=sampleKey&t=1660759608&api-signature=7accbcf70808449f944e5da9e3e06db0aae7362870e7ace8312d5d9e8103d274')
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('checkForRequired', () => {
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
// Set the environment variables
|
|
47
|
+
process.env.WEATHER_LINK_API_KEY = 'sampleKey';
|
|
48
|
+
process.env.WEATHER_LINK_API_SECRET = 'sampleSecret';
|
|
49
|
+
process.env.WEATHER_LINK_BASE_API_URL = 'https://api.weatherlink.com/v2/';
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('Missing All Env Vars', () => {
|
|
53
|
+
chai.expect(
|
|
54
|
+
checkForRequired(["WEATHER_LINK_VERY_FAKE_NONEXISTENT"])
|
|
55
|
+
).to.deep.equal({"exist": false, "missing": ["WEATHER_LINK_VERY_FAKE_NONEXISTENT"]})
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('All Env Vars Defined', () => {
|
|
59
|
+
chai.expect(
|
|
60
|
+
checkForRequired(["WEATHER_LINK_API_KEY", "WEATHER_LINK_API_SECRET", "WEATHER_LINK_BASE_API_URL"])
|
|
61
|
+
).to.deep.equal({"exist": true, "missing": []})
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('dateRangeIsValid', () => {
|
|
66
|
+
|
|
67
|
+
it('Start Timestamp is Before End Timestamp', () => {
|
|
68
|
+
chai.expect(
|
|
69
|
+
dateRangeIsValid(1673472399, 1673494000).isValid
|
|
70
|
+
).to.equal(true)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('Start Timestamp is the Same as End Timestamp', () => {
|
|
74
|
+
chai.expect(
|
|
75
|
+
dateRangeIsValid(1673494000, 1673494000).isValid
|
|
76
|
+
).to.equal(false)
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('Start Timestamp is After End Timestamp', () => {
|
|
80
|
+
chai.expect(
|
|
81
|
+
dateRangeIsValid(1673494010, 1673494000).isValid
|
|
82
|
+
).to.equal(false)
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('Start Timestamp is 0', () => {
|
|
86
|
+
chai.expect(
|
|
87
|
+
dateRangeIsValid(0, 1673494000).isValid
|
|
88
|
+
).to.equal(false)
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('Start Timestamp is Negative', () => {
|
|
92
|
+
chai.expect(
|
|
93
|
+
dateRangeIsValid(-10, 1673494000).isValid
|
|
94
|
+
).to.equal(false)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('End Timestamp is 0', () => {
|
|
98
|
+
chai.expect(
|
|
99
|
+
dateRangeIsValid(1673494010, 0).isValid
|
|
100
|
+
).to.equal(false)
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('End Timestamp is Negative', () => {
|
|
104
|
+
chai.expect(
|
|
105
|
+
dateRangeIsValid(1673494010, -10).isValid
|
|
106
|
+
).to.equal(false)
|
|
107
|
+
});
|
|
108
|
+
});
|