signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
540 lines (472 loc) • 15.4 kB
text/typescript
/**
* Weather Plugin Example for Signal K
*
* Demonstrates:
* - Network capability by fetching weather data from OpenWeatherMap API
* - Resource provider capability for serving weather data via REST API
* - Delta emission for real-time weather updates
*/
import {
Plugin,
emit,
setStatus,
setError,
debug,
createSimpleDelta
} from '@signalk/assemblyscript-plugin-sdk/assembly'
import { hasNetworkCapability } from '@signalk/assemblyscript-plugin-sdk/assembly/network'
import {
registerResourceProvider,
ResourceGetRequest
} from '@signalk/assemblyscript-plugin-sdk/assembly/resources'
import { fetchSync } from 'as-fetch/sync'
import { Response } from 'as-fetch/assembly'
// Configuration interface
class WeatherConfig {
apiKey: string = ''
latitude: f64 = 0.0
longitude: f64 = 0.0
updateInterval: i32 = 600000 // 10 minutes default
}
// Simple JSON parsing helpers
class WeatherData {
temperature: f64 = 0.0
humidity: f64 = 0.0
pressure: f64 = 0.0
windSpeed: f64 = 0.0
windDirection: f64 = 0.0
description: string = ''
timestamp: string = ''
latitude: f64 = 0.0
longitude: f64 = 0.0
toJSON(): string {
return (
'{"temperature":' +
this.temperature.toString() +
',"humidity":' +
this.humidity.toString() +
',"pressure":' +
this.pressure.toString() +
',"windSpeed":' +
this.windSpeed.toString() +
',"windDirection":' +
this.windDirection.toString() +
',"timestamp":"' +
this.timestamp +
'"' +
',"location":{"latitude":' +
this.latitude.toString() +
',"longitude":' +
this.longitude.toString() +
'}}'
)
}
static parse(json: string): WeatherData | null {
const data = new WeatherData()
// Very basic JSON parsing - in production, use a proper JSON parser
// This is just for demonstration purposes
// Extract temperature: "temp":293.15
const tempMatch = json.indexOf('"temp":')
if (tempMatch >= 0) {
const tempStart = tempMatch + 7
let tempEnd = tempStart
while (
tempEnd < json.length &&
((json.charCodeAt(tempEnd) >= 48 && json.charCodeAt(tempEnd) <= 57) ||
json.charCodeAt(tempEnd) === 46)
) {
tempEnd++
}
const tempStr = json.substring(tempStart, tempEnd)
data.temperature = parseFloat(tempStr) - 273.15 // Convert Kelvin to Celsius
}
// Extract humidity: "humidity":60
const humMatch = json.indexOf('"humidity":')
if (humMatch >= 0) {
const humStart = humMatch + 11
let humEnd = humStart
while (
humEnd < json.length &&
json.charCodeAt(humEnd) >= 48 &&
json.charCodeAt(humEnd) <= 57
) {
humEnd++
}
const humStr = json.substring(humStart, humEnd)
data.humidity = parseFloat(humStr)
}
// Extract pressure: "pressure":1013
const pressMatch = json.indexOf('"pressure":')
if (pressMatch >= 0) {
const pressStart = pressMatch + 11
let pressEnd = pressStart
while (
pressEnd < json.length &&
json.charCodeAt(pressEnd) >= 48 &&
json.charCodeAt(pressEnd) <= 57
) {
pressEnd++
}
const pressStr = json.substring(pressStart, pressEnd)
data.pressure = parseFloat(pressStr) * 100.0 // Convert hPa to Pa
}
// Extract wind speed: "speed":5.2
const speedMatch = json.indexOf('"speed":')
if (speedMatch >= 0) {
const speedStart = speedMatch + 8
let speedEnd = speedStart
while (
speedEnd < json.length &&
((json.charCodeAt(speedEnd) >= 48 && json.charCodeAt(speedEnd) <= 57) ||
json.charCodeAt(speedEnd) === 46)
) {
speedEnd++
}
const speedStr = json.substring(speedStart, speedEnd)
data.windSpeed = parseFloat(speedStr)
}
// Extract wind direction: "deg":180
const degMatch = json.indexOf('"deg":')
if (degMatch >= 0) {
const degStart = degMatch + 6
let degEnd = degStart
while (
degEnd < json.length &&
json.charCodeAt(degEnd) >= 48 &&
json.charCodeAt(degEnd) <= 57
) {
degEnd++
}
const degStr = json.substring(degStart, degEnd)
// Convert degrees to radians
data.windDirection = (parseFloat(degStr) * 3.14159265359) / 180.0
}
return data
}
}
// Cached weather data for resource provider
let cachedWeatherData: WeatherData | null = null
let cachedConfig: WeatherConfig = new WeatherConfig()
// Weather plugin class
class WeatherPlugin extends Plugin {
private config: WeatherConfig = new WeatherConfig()
private lastUpdate: i64 = 0
// Note: Plugin ID is derived from package.json name
name(): string {
return 'Weather Data Plugin (Example)'
}
start(configJson: string): i32 {
debug('Weather plugin starting...')
// Check if network capability is available
if (!hasNetworkCapability()) {
setError('Network capability not granted - cannot fetch weather data')
return 1
}
// Parse configuration
debug('Parsing config JSON')
debug(configJson)
if (configJson.length > 2) {
// Very basic config parsing - extract apiKey, lat, lon
// Fixed: indexOf searches FROM the given position, including that position
// So we need to search from AFTER the colon
const apiKeyMatch = configJson.indexOf('"apiKey"')
debug('apiKeyMatch index:')
debug(apiKeyMatch.toString())
if (apiKeyMatch >= 0) {
// Find the opening quote of the value (after the colon)
const colonPos = configJson.indexOf(':', apiKeyMatch)
const quoteStart = configJson.indexOf('"', colonPos)
if (quoteStart >= 0) {
const keyStart = quoteStart + 1 // Position after opening quote
const keyEnd = configJson.indexOf('"', keyStart) // Find closing quote
if (keyEnd > keyStart) {
this.config.apiKey = configJson.substring(keyStart, keyEnd)
}
}
}
const latMatch = configJson.indexOf('"latitude"')
if (latMatch >= 0) {
const colonPos = configJson.indexOf(':', latMatch)
let latStart = colonPos + 1
let latEnd = latStart
// Skip whitespace
while (
latEnd < configJson.length &&
(configJson.charCodeAt(latEnd) === 32 ||
configJson.charCodeAt(latEnd) === 9)
) {
latEnd++
}
latStart = latEnd
// Read number
while (
latEnd < configJson.length &&
((configJson.charCodeAt(latEnd) >= 48 &&
configJson.charCodeAt(latEnd) <= 57) ||
configJson.charCodeAt(latEnd) === 46 ||
configJson.charCodeAt(latEnd) === 45)
) {
latEnd++
}
if (latEnd > latStart) {
this.config.latitude = parseFloat(
configJson.substring(latStart, latEnd)
)
}
}
const lonMatch = configJson.indexOf('"longitude"')
if (lonMatch >= 0) {
const colonPos = configJson.indexOf(':', lonMatch)
let lonStart = colonPos + 1
let lonEnd = lonStart
// Skip whitespace
while (
lonEnd < configJson.length &&
(configJson.charCodeAt(lonEnd) === 32 ||
configJson.charCodeAt(lonEnd) === 9)
) {
lonEnd++
}
lonStart = lonEnd
// Read number
while (
lonEnd < configJson.length &&
((configJson.charCodeAt(lonEnd) >= 48 &&
configJson.charCodeAt(lonEnd) <= 57) ||
configJson.charCodeAt(lonEnd) === 46 ||
configJson.charCodeAt(lonEnd) === 45)
) {
lonEnd++
}
if (lonEnd > lonStart) {
this.config.longitude = parseFloat(
configJson.substring(lonStart, lonEnd)
)
}
}
}
// Validate configuration
if (this.config.apiKey.length === 0) {
setError(
'No API key configured - get one from https://openweathermap.org/api'
)
return 1
}
// Store config globally for resource provider handlers
cachedConfig = this.config
// Register as a resource provider for "weather" type
debug('Registering as weather resource provider...')
if (registerResourceProvider('weather')) {
debug('Successfully registered as weather resource provider')
} else {
debug(
'Warning: Failed to register as resource provider (capability may not be granted)'
)
}
// Fetch and emit real weather data using as-fetch
// This demonstrates the Asyncify integration for synchronous-style async operations
this.fetchWeatherData()
setStatus('Weather plugin running - data fetched from OpenWeatherMap')
return 0
}
stop(): i32 {
debug('Weather plugin stopped')
setStatus('Stopped')
return 0
}
schema(): string {
return `{
"type": "object",
"required": ["apiKey", "latitude", "longitude"],
"properties": {
"apiKey": {
"type": "string",
"title": "OpenWeatherMap API Key",
"description": "Get your free API key from https://openweathermap.org/api"
},
"latitude": {
"type": "number",
"title": "Latitude",
"description": "Latitude for weather location",
"default": 60.1699
},
"longitude": {
"type": "number",
"title": "Longitude",
"description": "Longitude for weather location",
"default": 24.9384
},
"updateInterval": {
"type": "number",
"title": "Update Interval (ms)",
"description": "How often to fetch weather data",
"default": 600000
}
}
}`
}
private emitTestWeatherData(): void {
debug('Emitting test weather data...')
// Emit test temperature (15°C)
const tempDelta = createSimpleDelta(
'environment.outside.temperature',
'288.15' // 15°C in Kelvin
)
emit(tempDelta)
// Emit test humidity (65%)
const humDelta = createSimpleDelta('environment.outside.humidity', '0.65')
emit(humDelta)
// Emit test pressure (101300 Pa)
const pressDelta = createSimpleDelta(
'environment.outside.pressure',
'101300'
)
emit(pressDelta)
// Emit test wind speed (5.2 m/s)
const windSpeedDelta = createSimpleDelta(
'environment.wind.speedTrue',
'5.2'
)
emit(windSpeedDelta)
// Emit test wind direction (180° = 3.14159 radians)
const windDirDelta = createSimpleDelta(
'environment.wind.directionTrue',
'3.14159'
)
emit(windDirDelta)
debug('Test weather data emitted successfully')
}
private fetchWeatherData(): void {
// Build API URL
const lat = this.config.latitude.toString()
const lon = this.config.longitude.toString()
const url =
'https://api.openweathermap.org/data/2.5/weather?lat=' +
lat +
'&lon=' +
lon +
'&appid=' +
this.config.apiKey
debug('Fetching weather data from: ' + url)
// Fetch weather data using as-fetch synchronously
const fetchResponse = fetchSync(url)
if (!fetchResponse.ok) {
setError(
'Failed to fetch weather data - HTTP status: ' +
fetchResponse.status.toString()
)
return
}
const response = fetchResponse.text()
debug('Received weather response: ' + response.substring(0, 100) + '...')
// Parse weather data
const weatherData = WeatherData.parse(response)
if (weatherData === null) {
setError('Failed to parse weather data')
return
}
// Add location for resource provider (timestamp will be set by server)
weatherData.latitude = this.config.latitude
weatherData.longitude = this.config.longitude
// Cache the weather data for resource provider queries
cachedWeatherData = weatherData
// Emit temperature
const tempDelta = createSimpleDelta(
'environment.outside.temperature',
weatherData.temperature.toString()
)
emit(tempDelta)
// Emit humidity
const humDelta = createSimpleDelta(
'environment.outside.humidity',
(weatherData.humidity / 100.0).toString()
)
emit(humDelta)
// Emit pressure
const pressDelta = createSimpleDelta(
'environment.outside.pressure',
weatherData.pressure.toString()
)
emit(pressDelta)
// Emit wind speed
const windSpeedDelta = createSimpleDelta(
'environment.wind.speedTrue',
weatherData.windSpeed.toString()
)
emit(windSpeedDelta)
// Emit wind direction
const windDirDelta = createSimpleDelta(
'environment.wind.directionTrue',
weatherData.windDirection.toString()
)
emit(windDirDelta)
setStatus('Weather data updated')
debug('Weather data emitted successfully')
}
}
// Export plugin instance
const plugin = new WeatherPlugin()
// Plugin lifecycle exports
// Note: plugin_id() is no longer required - ID is derived from package.json name
export function plugin_name(): string {
return plugin.name()
}
export function plugin_schema(): string {
return plugin.schema()
}
export function plugin_start(configPtr: usize, configLen: usize): i32 {
// Read config string from memory
const len = i32(configLen)
const configBytes = new Uint8Array(len)
for (let i: i32 = 0; i < len; i++) {
configBytes[i] = load<u8>(configPtr + <usize>i)
}
const configJson = String.UTF8.decode(configBytes.buffer)
return plugin.start(configJson)
}
export function plugin_stop(): i32 {
return plugin.stop()
}
// ===== Resource Provider Handlers =====
// These are called by the Signal K server when requests come in to
// /signalk/v2/api/resources/weather
/**
* List available weather resources
* Called for: GET /signalk/v2/api/resources/weather
*
* @param queryJson - JSON string with query parameters (e.g., filters)
* @returns JSON object of resources: { "id": { ...resource... }, ... }
*/
export function resources_list_resources(queryJson: string): string {
debug('resources_list_resources called with query: ' + queryJson)
// Return available weather resources
// We have one resource: "current" for current weather conditions
if (cachedWeatherData !== null) {
const data = cachedWeatherData as WeatherData
return '{"current":' + data.toJSON() + '}'
}
// No data available yet
return '{}'
}
/**
* Get a specific weather resource
* Called for: GET /signalk/v2/api/resources/weather/{id}
*
* @param requestJson - JSON with { "id": "resource-id", "property": optional }
* @returns JSON object of the resource
*/
export function resources_get_resource(requestJson: string): string {
debug('resources_get_resource called with request: ' + requestJson)
// Parse the request to get the ID
const req = ResourceGetRequest.parse(requestJson)
debug('Parsed request id: ' + req.id)
if (req.id === 'current') {
if (cachedWeatherData !== null) {
const data = cachedWeatherData as WeatherData
return data.toJSON()
}
return '{"error":"No weather data available yet"}'
}
// Unknown resource ID
return '{"error":"Resource not found: ' + req.id + '"}'
}