diff --git a/hooks/VDM.js b/hooks/VDM.js index 10eb978c..1fd92b50 100644 --- a/hooks/VDM.js +++ b/hooks/VDM.js @@ -20,6 +20,11 @@ const debug = require('debug')('signalk-parser-nmea0183/VDM') const utils = require('@signalk/nmea0183-utilities') const Decoder = require('ggencoder').AisDecode const schema = require('@signalk/signalk-schema') +const knotsToMs = (v) => + parseFloat(utils.transform(v, 'knots', 'ms').toFixed(2)) +const degToRad = (v) => utils.transform(v, 'deg', 'rad') +const cToK = (v) => parseFloat(utils.transform(v, 'c', 'k').toFixed(2)) +const nmToM = (v) => parseFloat(utils.transform(v, 'nm', 'm').toFixed(2)) const stateMapping = { 0: 'motoring', @@ -67,6 +72,50 @@ const specialManeuverMapping = { 3: 'reserved', } +const beaufortScale = { + 0: 'calm, 0-0.2 m/s', + 1: 'light air, 0.3-1.5 m/s', + 2: 'light breeze, 1.6-3.3 m/s', + 3: 'gentle breeze, 3.4-5.4 m/s', + 4: 'moderate breeze, 5.5-7.9 m/s', + 5: 'fresh breeze, 8-10.7 m/s', + 6: 'strong breeze, 10.8-13.8 m/s', + 7: 'high wind, 13.9-17.1 m/s', + 8: 'gale, 17.2-20.7 m/s', + 9: 'strong gale, 20.8-24.4 m/s', + 10: 'storm, 24.5-28.4 m/s', + 11: 'violent storm, 28.5-32.6 m/s', + 12: 'hurricane-force, ≥ 32.7 m/s', + 13: 'not available', + 14: 'reserved', + 15: 'reserved', +} + +const statusTable = { + 0: 'steady', + 1: 'decreasing', + 2: 'increasing', + 3: 'not available', +} + +const precipitationType = { + 0: 'reserved', + 1: 'rain', + 2: 'thunderstorm', + 3: 'freezing rain', + 4: 'mixed/ice', + 5: 'snow', + 6: 'reserved', + 7: 'not available', +} + +const iceTable = { + 0: 'no', + 1: 'yes', + 2: 'reserved', + 3: 'not available', +} + module.exports = function (input, session) { const { id, sentence, parts, tags } = input const data = new Decoder(sentence, session) @@ -115,16 +164,6 @@ module.exports = function (input, session) { }) } - if (data.lon && data.lat) { - values.push({ - path: 'navigation.position', - value: { - longitude: data.lon, - latitude: data.lat, - }, - }) - } - if (data.length) { values.push({ path: 'design.length', @@ -262,18 +301,118 @@ module.exports = function (input, session) { } if (typeof data.fid !== 'undefined') { + if (data.fid == 31 || data.fid == 11 || data.fid == 33) { + contextPrefix = 'meteo.' + } values.push({ path: 'sensors.ais.functionalId', value: data.fid, }) } + if (data.lon && data.lat) { + values.push({ + path: 'navigation.position', + value: { + longitude: data.lon, + latitude: data.lat, + }, + }) + } + + ;[ + ['avgwindspd', 'wind.averageSpeed', knotsToMs], + ['windgust', 'wind.gust', knotsToMs], + ['winddir', 'wind.directionTrue', degToRad], + ['windgustdir', 'wind.gustDirectionTrue', degToRad], + ['airtemp', 'outside.temperature', cToK], + ['relhumid', 'outside.relativeHumidity', (v) => v], + ['dewpoint', 'outside.dewPointTemperature', cToK], + ['airpress', 'outside.pressure', (v) => v * 100], + ['waterlevel', 'water.level', (v) => v], + ['signwavewhgt', 'water.waves.significantHeight', (v) => v], + ['waveperiod', 'water.waves.period', (v) => v], + ['wavedir', 'water.waves.directionTrue', degToRad], + ['swellhgt', 'water.swell.height', (v) => v], + ['swellperiod', 'water.swell.period', (v) => v], + ['swelldir', 'water.swell.directionTrue', degToRad], + ['watertemp', 'water.temperature', cToK], + ['salinity', 'water.salinity', (v) => v], + ['surfcurrspd', 'water.current.drift', knotsToMs], + ['surfcurrdir', 'water.current.set', degToRad], + ].forEach(([propName, path, f]) => { + if (data[propName] !== undefined) { + contextPrefix = 'meteo.' + values.push({ + path: `environment.` + path, + value: f(data[propName]), + }) + } + }) + ;[ + ['ice', 'water.ice', iceTable], + ['precipitation', 'outside.precipitation', precipitationType], + ['seastate', 'water.seaState', beaufortScale], + ['waterlevelten', 'water.levelTendency', statusTable], + ['airpressten', 'outside.pressureTendency', statusTable], + ].forEach(([propName, path, f]) => { + if (data[propName] !== undefined) { + contextPrefix = 'meteo.' + values.push( + { + path: `environment.` + path, + value: f[data[propName]], + }, + { + path: `environment.` + path + `Value`, + value: data[propName], + } + ) + } + }) + + if (data.horvisib !== undefined && data.horvisibrange !== undefined) { + contextPrefix = 'meteo.' + values.push( + { + path: 'environment.outside.horizontalVisibility', + value: utils.transform(data.horvisib, 'nm', 'm'), + }, + { + path: 'environment.outside.horizontalVisibility.overRange', + value: data.horvisibrange, + } + ) + } + + if ( + data.utcday !== undefined && + data.utchour !== undefined && + data.utcminute !== undefined + ) { + contextPrefix = 'meteo.' + const y = new Date().getUTCFullYear() + const m = new Date().getUTCMonth() + 1 + const d = data.utcday + const h = data.utchour + const min = data.utcminute + const date = `${y}-${m.toString().padStart(2, '0')}-${d + .toString() + .padStart(2, '0')}T${h.toString().padStart(2, '0')}:${min + .toString() + .padStart(2, '0')}:00.000Z` + values.push({ + path: 'environment.date', + value: date, + }) + } + if (values.length === 0) { return null } const delta = { - context: contextPrefix + `urn:mrn:imo:mmsi:${data.mmsi}`, + context: contextPrefix + `urn:mrn:imo:mmsi:${data.mmsikey || data.mmsi}`, updates: [ { source: tags.source, diff --git a/package.json b/package.json index 62f626cb..f03792a8 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dependencies": { "@signalk/nmea0183-utilities": "^0.8.0", "@signalk/signalk-schema": "^1.7.1", - "ggencoder": "^1.0.4", + "ggencoder": "^1.0.8", "moment-timezone": "^0.5.21", "split": "^1.0.1" }, diff --git a/test/VDM.js b/test/VDM.js index effe144b..92889dcc 100644 --- a/test/VDM.js +++ b/test/VDM.js @@ -237,4 +237,52 @@ describe('VDM', function () { .filter((pathValue) => pathValue.path === '')[3] .value.registrations.imo.should.equal('IMO 1010258') }) + + it('meteo single sentence converts ok', () => { + const delta = new Parser().parse( + '!AIVDM,1,1,,A,8@2R5Ph0GhOCT1a2VvkrgwvlFR06EuOwgqrqwnSwe7wvlOwwsAwwnSGmwvwt,0*40' + ) + delta.context.should.equal('meteo.urn:mrn:imo:mmsi:002655619:366097') + const currentYear = new Date().getFullYear(); + const output = [ + ['environment.water.level', -0.17], + ['environment.water.levelTendency', 'steady'], + ['environment.water.levelTendencyValue', 0], + ['environment.date', currentYear + '-02-22T15:42:00.000Z'] + ] + output.forEach(([path, value]) => + delta.updates[0].values + .find((pathValue) => pathValue.path === path) + .value.should.equal(value) + ) + }) + + it('meteo dual sentence converts ok', () => { + const meteoSentences = [ + '!AIVDM,2,1,4,A,8@2R5Ph0GhENJAb8wnScjAJ:AB06EuOwgwl?wnSwe7wvlOwwsAwwnSGm,0*15', + '!AIVDM,2,2,4,A,wvwt,0*10', + ] + const parser = new Parser() + let delta = parser.parse(meteoSentences[0]) + should.equal(delta, null) + delta = parser.parse(meteoSentences[1]) + delta.context.should.equal('meteo.urn:mrn:imo:mmsi:002655619:967728') + delta.updates[0].values[3].value.longitude.should.equal(11.7283) + delta.updates[0].values[3].value.latitude.should.equal(57.9669) + const currentYear = new Date().getFullYear() + const output = [ + ['sensors.ais.designatedAreaCode', 1], + ['sensors.ais.functionalId', 31], + ['environment.wind.averageSpeed', 9.26], + ['environment.wind.gust', 11.32], + ['environment.wind.directionTrue', 4.817108736604238], + ['environment.wind.gustDirectionTrue', 4.817108736604238], + ['environment.date', currentYear + '-02-20T14:47:00.000Z'], + ] + output.forEach(([path, value]) => + delta.updates[0].values + .find((pathValue) => pathValue.path === path) + .value.should.equal(value) + ) + }) })