From f2015175109a14d0bfdb65bea927be2fcfb4ef82 Mon Sep 17 00:00:00 2001 From: Auke de Jong Date: Thu, 10 Oct 2024 16:42:19 +0200 Subject: [PATCH] New match strategy: time_frame. Fixes #64 --- README.md | 105 +++++++---- src/card/CardConfigDataPeriod.ts | 3 +- src/config/CardConfigWrapper.ts | 11 +- src/config/ConfigCheckUtils.ts | 2 +- src/config/DataPeriod.ts | 5 +- src/matcher/DirectionFirstMatcher.ts | 74 ++++++++ src/matcher/MatchUtils.ts | 49 +++++ src/matcher/MeasurementMatcher.ts | 176 +----------------- src/matcher/SpeedFirstMatcher.ts | 82 ++++++++ src/matcher/TimeFrameMatcher.ts | 102 ++++++++++ .../HomeAssistantMeasurementProvider.ts | 22 ++- 11 files changed, 415 insertions(+), 216 deletions(-) create mode 100644 src/matcher/DirectionFirstMatcher.ts create mode 100644 src/matcher/MatchUtils.ts create mode 100644 src/matcher/SpeedFirstMatcher.ts create mode 100644 src/matcher/TimeFrameMatcher.ts diff --git a/README.md b/README.md index 62d4642..d80cebc 100644 --- a/README.md +++ b/README.md @@ -61,48 +61,50 @@ Select "Manage Resources" ### Card options -| Name | Type | Default | Required | Description | -|----------------------------|:---------------------------------------:|:----------------------------:|:--------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| type | string | | x | `custom:windrose-card`. | -| title | string | | - | The card title. | -| wind_direction_entity | [object](#Object-wind_direction_entity) | | x | The wind direction entity, having directing in degrees as the state. | -| windspeed_entities | [object](#Object-windspeed_entities) | | x | One are more windspeed entities. Only the first is used for the windrose. | -| refresh_interval | number | 300 | - | Refresh interval in seconds | -| hours_to_show (DEPRECATED) | number | | - | Deprecated. Still works for now. Use the data period object instead. | -| data_period | [object](#Object-data_period) | | x | Configure what data period to query. See object data_period below. Only one options should be configured. | -| windspeed_bar_location | string | bottom | - | Location of the speed bar graph: `bottom`, `right` | -| windspeed_bar_full | boolean | true | - | When true, renders all wind ranges, when false, doesn't render the speed range without measurements. | -| hide_windspeed_bar | boolean | false | - | Hides all windspeed bars. | -| output_speed_unit | string | mps | - | Windspeed unit used on card, see Windspeed unit options bellow. | -| output_speed_unit_label | string | | - | Overwrite the output speed units name, only for display. | -| center_calm_percentage | boolean | true | - | Show the calm speed percentage in the center of windrose. Directions corresponding with speeds in the first speedrange are not displayed in a direction leave. | -| speed_range_beaufort | boolean | true | - | Uses the Beaufort speed ranges. The exact Beaufort ranges depend on the output windspeed unit. Default is true, when you want to show other speed unit on the bar graph, set this property to false. | -| speed_range_step | number | depends on output speed unit | - | Sets the speed range step to use. Not possible for output speed unit bft (Beaufort) . | -| speed_range_max | number | depends on output speed unit | - | Sets the speed range max to use. Not possible for output speed unit bft (Beaufort). For example: step 5, max 20 creates ranges: 0-5, 5-10, 10-15, 15-20, 20-infinity | -| speed_ranges | [object](#Object-speed_ranges) | depends on output speed unit | - | Define custom speedranges and colours. | -| cardinal_direction_letters | string | NESW | - | The cardinal letters used in the windrose. | -| wind_direction_count | string | 16 | - | How many wind direction the windrose can display, min. 4 max. 32 | -| windrose_draw_north_offset | number | 0 | - | At what degrees the north direction is drawn. For example, if you want the windrose north orientation the same as your properties north orientation | -| compass_direction | [object](#Ojbect-compass_direction) | | - | Configuration for using a compass sensor to rotate the windrose to the correct direction, for use on for example a boat. | -| current_direction | [object](#Object-current_direction) | | - | Shows the last reported wind direction with a red arrow on the wind rose. | -| corner_info | [object](#Ojbect-corner_info) | | - | Configuration for displaying entity states in the corners around the windrose. | -| matching_strategy | string | direction-first | - | How to match direction and speed measurements. Find a speed with each direction or a direction with each speed measurement. Options: `direction-first`, `speed-first` | -| background_image | string | | - | Displays a square image with the same size and exactly behind the outer circle of the windrose. | -| colors | [object](#Object-colors) | | - | Configure colors for different parts of the windrose and windspeedbar. See object Colors. | -| log_level | string | WARN | - | Browser console log level, options: NONE, ERROR, WARN, INFO, DEBUG and TRACE | +| Name | Type | Default | Required | Description | +|----------------------------|:---------------------------------------:|:----------------------------:|:--------:|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| type | string | | x | `custom:windrose-card`. | +| title | string | | - | The card title. | +| wind_direction_entity | [object](#Object-wind_direction_entity) | | x | The wind direction entity, having directing in degrees as the state. | +| windspeed_entities | [object](#Object-windspeed_entities) | | x | One are more windspeed entities. Only the first is used for the windrose. | +| refresh_interval | number | 300 | - | Refresh interval in seconds | +| hours_to_show (DEPRECATED) | number | | - | Deprecated. Still works for now. Use the data period object instead. | +| data_period | [object](#Object-data_period) | | x | Configure what data period to query. See object data_period below. Only one options should be configured. | +| windspeed_bar_location | string | bottom | - | Location of the speed bar graph: `bottom`, `right` | +| windspeed_bar_full | boolean | true | - | When true, renders all wind ranges, when false, doesn't render the speed range without measurements. | +| hide_windspeed_bar | boolean | false | - | Hides all windspeed bars. | +| output_speed_unit | string | mps | - | Windspeed unit used on card, see Windspeed unit options bellow. | +| output_speed_unit_label | string | | - | Overwrite the output speed units name, only for display. | +| center_calm_percentage | boolean | true | - | Show the calm speed percentage in the center of windrose. Directions corresponding with speeds in the first speedrange are not displayed in a direction leave. | +| speed_range_beaufort | boolean | true | - | Uses the Beaufort speed ranges. The exact Beaufort ranges depend on the output windspeed unit. Default is true, when you want to show other speed unit on the bar graph, set this property to false. | +| speed_range_step | number | depends on output speed unit | - | Sets the speed range step to use. Not possible for output speed unit bft (Beaufort) . | +| speed_range_max | number | depends on output speed unit | - | Sets the speed range max to use. Not possible for output speed unit bft (Beaufort). For example: step 5, max 20 creates ranges: 0-5, 5-10, 10-15, 15-20, 20-infinity | +| speed_ranges | [object](#Object-speed_ranges) | depends on output speed unit | - | Define custom speedranges and colours. | +| cardinal_direction_letters | string | NESW | - | The cardinal letters used in the windrose. | +| wind_direction_count | string | 16 | - | How many wind direction the windrose can display, min. 4 max. 32 | +| windrose_draw_north_offset | number | 0 | - | At what degrees the north direction is drawn. For example, if you want the windrose north orientation the same as your properties north orientation | +| compass_direction | [object](#Ojbect-compass_direction) | | - | Configuration for using a compass sensor to rotate the windrose to the correct direction, for use on for example a boat. | +| current_direction | [object](#Object-current_direction) | | - | Shows the last reported wind direction with a red arrow on the wind rose. | +| corner_info | [object](#Ojbect-corner_info) | | - | Configuration for displaying entity states in the corners around the windrose. | +| matching_strategy | string | direction-first | - | How to match direction and speed measurements. Find a speed with each direction or a direction with each speed measurement. Options: `direction-first`, `speed-first` or `time-frame`. More info at [Matching strategies](#Matching-strategies) | +| background_image | string | | - | Displays a square image with the same size and exactly behind the outer circle of the windrose. | +| colors | [object](#Object-colors) | | - | Configure colors for different parts of the windrose and windspeedbar. See object Colors. | +| log_level | string | WARN | - | Browser console log level, options: NONE, ERROR, WARN, INFO, DEBUG and TRACE | ### Object data_period Only one of the options should be configured. -| Name | Type | Default | Required | Description | -|------------------------|:------:|:-------:|:--------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| hours_to_show | number | | - | Show winddata for the last number of hours. Number higher then 0. | -| from_hour_of_day | number | | - | Show winddata from the configured hours till now. 0 is midnight, so only data of the current day is used. If the set hour is not yet arrived, data from the previous day from that hour is used. | +| Name | Type | Default | Required | Description | +|------------------|:------:|:-------:|:--------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| hours_to_show | number | | - | Show winddata for the last number of hours. Number higher then 0. | +| from_hour_of_day | number | | - | Show winddata from the configured hours till now. 0 is midnight, so only data of the current day is used. If the set hour is not yet arrived, data from the previous day from that hour is used. | +| time_interval | number | 60 | - | Time interval in seconds. Only used by the time-frame matching strategy. More info at [Matching strategies](#Matching-strategies) | ### Object wind_direction_entity + As of version 1.8.2 the direction unit is determined automatic. When the state is numeric, a degree value is assumed. When the state is letters, the direction is determined with the letter combination. @@ -152,6 +154,7 @@ When your windspeed entity uses an unit of measurement not mentioned in the tabl ### Object current_direction + Shows the current wind direction. The arrow is pointing too where to wind is flowing too. When the sensor state is not a direction a red center dot is displayed. Some sensors can have a value like CALM or VRB, indicating there is no direction measured. @@ -164,6 +167,7 @@ Some sensors can have a value like CALM or VRB, indicating there is no direction ### Ojbect compass_direction + This configuration is only needed if you want the windrose to rotate on an compass entity. Usefull on for example a boat. You can also make a helper number entity to rotate the windrose on manual input. @@ -175,6 +179,7 @@ You can also make a helper number entity to rotate the windrose on manual input. ### Ojbect corner_info + Configuration for displaying information in the corners around the windrose. | Name | Type | Default | Required | Description | @@ -202,7 +207,7 @@ corner_info: entity: input_number.compass ``` -### Object top_left, top_right_ bottom_left and bottom_right +### Object top_left, top_right, bottom_left and bottom_right | Name | Type | Default | Required | Description | |--------|:------:|:---------------------:|:--------:|--------------------------------------------------------------------------------------------| @@ -212,6 +217,38 @@ corner_info: | entity | string | | x | State of the entity will be displayed | +### Matching strategies + +The matching strategies can result in a different graph, depending on your sensor. How many state updates they get. + +#### Direction first + +Config value: 'direction-first' + +Every direction state during the configuration time frame is used for the graph. The algorithm searches for the last speed state at the time of the direction state measurment. +It's possible not all speed state are used in the graph. + +#### Speed first + +Config value: 'speed-first' + +Every speed state during the configuration time frame is used for the graph. The algorithm searches for the last direction state at the time of the direction state measurement. +It's possible not all direction states are used in the graph. + +It's probably best to choose the sensor that reports the most updates as first. + +#### Time frame + +Config value: 'time-frame' + +Extra data_period config: 'time-interval' + +Time is leading. For every moment back in time (default every 60 seocnds) the direction and speed states are determined. +For data sources that only update state changes, this should result in a better graph. + +For the first two strategies, a percentage in the graph is a percentage of the measurement count not a percentage of time. + + ### Object colors For some value the theme variable --primary-text-color is used. This is needed if HA switches theme light/dark mode. diff --git a/src/card/CardConfigDataPeriod.ts b/src/card/CardConfigDataPeriod.ts index ada2527..870f92c 100644 --- a/src/card/CardConfigDataPeriod.ts +++ b/src/card/CardConfigDataPeriod.ts @@ -1,4 +1,5 @@ export interface CardConfigDataPeriod { hours_to_show: number; from_hour_of_day: number; -} \ No newline at end of file + time_interval: number; +} diff --git a/src/config/CardConfigWrapper.ts b/src/config/CardConfigWrapper.ts index 7d60184..c596f0f 100644 --- a/src/config/CardConfigWrapper.ts +++ b/src/config/CardConfigWrapper.ts @@ -131,9 +131,13 @@ export class CardConfigWrapper { const oldHoursToShowCheck = this.checkHoursToShow(oldHoursToShow); const hoursToShowCheck = this.checkHoursToShow(dataPeriod?.hours_to_show); const fromHourOfDayCheck = this.checkFromHourOfDay(dataPeriod?.from_hour_of_day); + let timeInterval = ConfigCheckUtils.checkNummerOrDefault(dataPeriod.time_interval, 60); + if (timeInterval === 0) { + timeInterval = 60; + } if (oldHoursToShowCheck) { Log.warn('WindRoseCard: hours_to_show config is deprecated, use the data_period object.'); - return new DataPeriod(oldHoursToShow, undefined); + return new DataPeriod(oldHoursToShow, undefined, timeInterval); } if (hoursToShowCheck && fromHourOfDayCheck) { throw new Error('WindRoseCard: Only one is allowed: hours_to_show or from_hour_of_day'); @@ -141,7 +145,7 @@ export class CardConfigWrapper { if (!hoursToShowCheck && !fromHourOfDayCheck) { throw new Error('WindRoseCard: One config option object data_period should be filled.'); } - return new DataPeriod(dataPeriod.hours_to_show, dataPeriod.from_hour_of_day); + return new DataPeriod(dataPeriod.hours_to_show, dataPeriod.from_hour_of_day, timeInterval); } private checkHoursToShow(hoursToShow: number): boolean { @@ -369,7 +373,8 @@ export class CardConfigWrapper { private checkMatchingStrategy(): string { if (this.cardConfig.matching_strategy) { - if (this.cardConfig.matching_strategy !== 'direction-first' && this.cardConfig.matching_strategy !== 'speed-first') { + if (this.cardConfig.matching_strategy !== 'direction-first' && this.cardConfig.matching_strategy !== 'speed-first' + && this.cardConfig.matching_strategy !== 'time-frame') { throw new Error('Invalid matching stategy ' + this.cardConfig.matching_strategy + '. Valid options: direction-first, speed-first'); } diff --git a/src/config/ConfigCheckUtils.ts b/src/config/ConfigCheckUtils.ts index d22ebea..9700666 100644 --- a/src/config/ConfigCheckUtils.ts +++ b/src/config/ConfigCheckUtils.ts @@ -2,7 +2,7 @@ export class ConfigCheckUtils { public static checkNummerOrDefault(number: string | number, defaultNumber: number): number { - if (isNaN(number as any)) { + if (number === null || number === undefined || isNaN(+number as any)) { return defaultNumber; } return +number; diff --git a/src/config/DataPeriod.ts b/src/config/DataPeriod.ts index 33b3f1c..63c076d 100644 --- a/src/config/DataPeriod.ts +++ b/src/config/DataPeriod.ts @@ -1,6 +1,7 @@ export class DataPeriod { constructor( public readonly hourstoShow: number | undefined, - public readonly fromHourOfDay: number | undefined) { + public readonly fromHourOfDay: number | undefined, + public readonly timeInterval: number) { } -} \ No newline at end of file +} diff --git a/src/matcher/DirectionFirstMatcher.ts b/src/matcher/DirectionFirstMatcher.ts new file mode 100644 index 0000000..756636e --- /dev/null +++ b/src/matcher/DirectionFirstMatcher.ts @@ -0,0 +1,74 @@ +import {Log} from "../util/Log"; +import {DirectionSpeed} from "./DirectionSpeed"; +import {MeasurementMatcher} from "./MeasurementMatcher"; +import {MatchUtils} from "./MatchUtils"; + +export class DirectionFirstMatcher implements MeasurementMatcher { + + matchStatsHistory(directionStats: StatisticsData[], speedHistory: HistoryData[]): DirectionSpeed[] { + const directionSpeed: DirectionSpeed[] = []; + + for (const direction of directionStats) { + const speed = MatchUtils.findHistoryInPeriod(direction, speedHistory); + if (speed) { + if (MatchUtils.isInvalidSpeed(speed.s)) { + Log.warn("Speed " + speed.s + " at timestamp " + direction.start + " is not a number."); + } else { + directionSpeed.push(new DirectionSpeed(direction.mean, +speed.s)); + } + } else { + Log.trace('No matching speed found for direction ' + direction.mean + " at timestamp " + direction.start); + } + } + + return directionSpeed; + } + + matchHistoryStats(directionHistory: HistoryData[], speedStats: StatisticsData[]): DirectionSpeed[] { + const directionSpeed: DirectionSpeed[] = []; + + for (const direction of directionHistory) { + const speed = MatchUtils.findStatsAtTime(direction.lu * 1000, speedStats); + if (speed) { + directionSpeed.push(new DirectionSpeed(direction.s, speed.mean)); + } else { + Log.trace('No matching speed found for direction ' + direction.s + " at timestamp " + direction.lu); + } + } + + return directionSpeed; + } + + matchHistoryHistory(directionHistory: HistoryData[], speedHistory: HistoryData[]): DirectionSpeed[] { + const directionSpeed: DirectionSpeed[] = []; + + for (const direction of directionHistory) { + const speed = MatchUtils.findHistoryBackAtTime(direction.lu, speedHistory); + if (speed) { + if (MatchUtils.isInvalidSpeed(speed.s)) { + Log.warn("Speed " + speed.s + " at timestamp " + speed.lu + " is not a number."); + } else { + directionSpeed.push(new DirectionSpeed(direction.s, +speed.s)); + } + } else { + Log.trace('No matching speed found for direction ' + direction.s + " at timestamp " + direction.lu); + } + } + + return directionSpeed; + } + + matchStatsStats(directionStats: StatisticsData[], speedStats: StatisticsData[]): DirectionSpeed[] { + const directionSpeed: DirectionSpeed[] = []; + for (const directionStat of directionStats) { + const matchedSpeed = MatchUtils.findMatchingStatistic(directionStat, speedStats); + if (matchedSpeed) { + directionSpeed.push(new DirectionSpeed(directionStat.mean, matchedSpeed.mean)); + } else { + Log.trace(`No matching speed found for direction ${directionStat.mean} at timestamp start:${directionStat.start} end:${directionStat.end}`); + } + } + + return directionSpeed; + } +} diff --git a/src/matcher/MatchUtils.ts b/src/matcher/MatchUtils.ts new file mode 100644 index 0000000..6ce8f98 --- /dev/null +++ b/src/matcher/MatchUtils.ts @@ -0,0 +1,49 @@ +export class MatchUtils { + + public static findStatsAtTime(timestamp: number, stats: StatisticsData[]): StatisticsData | undefined { + return stats.find((stat) => stat.start <= timestamp && timestamp <= stat.end); + } + + public static findHistoryInPeriod(stat: StatisticsData, history: HistoryData[]): HistoryData | undefined { + const start = stat.start / 1000; + const end = stat.end / 1000; + const selection = history.filter((measurement) => start < measurement.lu && end >= measurement.lu); + if (selection.length == 1) { + return selection[0]; + } else if (selection.length > 1) { + selection.sort((a, b) => b.lu - a.lu); + return selection[Math.trunc(selection.length / 2)]; + } + return undefined; + } + + public static findMatchingStatistic(statistic: StatisticsData, stats: StatisticsData[]): StatisticsData | undefined { + return stats.find((stat) => statistic.start === stat.start && statistic.end === stat.end); + } + + public static findHistoryBackAtTime(timestamp: number, history: HistoryData[]): HistoryData | undefined { + for (let i = history.length - 1; i >= 0; --i) { + + if (timestamp > history[i].lu) { + return history[i]; + } + } + return undefined; + } + + public static isInvalidSpeed(speed: string) { + return speed === '' || speed === null || speed === undefined || isNaN(+speed); + } + + public static isValidSpeed(speed: string) { + return speed !== '' && speed !== null && speed !== undefined && !isNaN(+speed); + } + + public static isNumber(value: string | number | undefined) { + return value !== '' && value !== null && value !== undefined && !isNaN(+value); + } + + public static cleanDate(date: number) { + return new Date(date * 1000).toLocaleString(); + } +} diff --git a/src/matcher/MeasurementMatcher.ts b/src/matcher/MeasurementMatcher.ts index 028c386..5585f9c 100644 --- a/src/matcher/MeasurementMatcher.ts +++ b/src/matcher/MeasurementMatcher.ts @@ -1,177 +1,13 @@ import {DirectionSpeed} from "./DirectionSpeed"; -import {Log} from "../util/Log"; -export class MeasurementMatcher { +export interface MeasurementMatcher { - constructor(private readonly matchingStrategy: string) { - Log.debug('Matching init:', matchingStrategy); - if (this.matchingStrategy !== 'direction-first' && this.matchingStrategy !== 'speed-first') { - throw Error('Unkown matchfing strategy: ' + this.matchingStrategy); - } - } + matchStatsHistory(directionStats: StatisticsData[], speedHistory: HistoryData[]): DirectionSpeed[]; - matchStatsHistory(directionStats: StatisticsData[], speedHistory: HistoryData[]): DirectionSpeed[] { - const directionSpeed: DirectionSpeed[] = []; - if (this.matchingStrategy == 'direction-first') { - for (const direction of directionStats) { - const speed = this.findHistoryInPeriod(direction, speedHistory); - if (speed) { - if (this.isInvalidSpeed(speed.s)) { - Log.warn("Speed " + speed.s + " at timestamp " + direction.start + " is not a number."); - } else { - directionSpeed.push(new DirectionSpeed(direction.mean, +speed.s)); - } - } else { - Log.trace('No matching speed found for direction ' + direction.mean + " at timestamp " + direction.start); - } - } + matchHistoryStats(directionHistory: HistoryData[], speedStats: StatisticsData[]): DirectionSpeed[]; - } else { - for (const speed of speedHistory) { - const direction = this.findStatsAtTime(speed.lu * 1000, directionStats); - if (direction) { - directionSpeed.push(new DirectionSpeed(direction.mean, +speed.s)); - if (speed.s === '' || speed.s === null || isNaN(+speed.s)) { - Log.warn("Speed " + speed.s + " at timestamp " + direction.start + " is not a number."); - } else { - directionSpeed.push(new DirectionSpeed(direction.mean, +speed.s)); - } - } else { - Log.trace('No matching direction found for speed ' + speed.s + " at timestamp " + speed.lu); - } - } - } - return directionSpeed; - } + matchHistoryHistory(directionHistory: HistoryData[], speedHistory: HistoryData[]): DirectionSpeed[]; - matchHistoryStats(directionHistory: HistoryData[], speedStats: StatisticsData[]): DirectionSpeed[] { - const directionSpeed: DirectionSpeed[] = []; - if (this.matchingStrategy == 'direction-first') { - for (const direction of directionHistory) { - const speed = this.findStatsAtTime(direction.lu * 1000, speedStats); - if (speed) { - directionSpeed.push(new DirectionSpeed(direction.s, speed.mean)); - } else { - Log.trace('No matching speed found for direction ' + direction.s + " at timestamp " + direction.lu); - } - } + matchStatsStats(directionStats: StatisticsData[], speedStats: StatisticsData[]): DirectionSpeed[]; - } else { - for (const speed of speedStats) { - const direction = this.findHistoryInPeriod(speed, directionHistory); - if (direction) { - if (direction.s === '' || direction.s === null || isNaN(+direction.s)) { - Log.warn("Direction " + direction.s + " at timestamp " + direction.lu + " is not a number."); - } else { - directionSpeed.push(new DirectionSpeed(direction.s, speed.mean)); - } - } else { - Log.trace('No matching direction found for speed ' + speed.start + " at timestamp " + speed.mean); - } - } - } - return directionSpeed; - } - - matchHistoryHistory(directionHistory: HistoryData[], speedHistory: HistoryData[]): DirectionSpeed[] { - const directionSpeed: DirectionSpeed[] = []; - if (this.matchingStrategy == 'direction-first') { - for (const direction of directionHistory) { - const speed = this.findHistoryBackAtTime(direction.lu, speedHistory); - if (speed) { - if (this.isInvalidSpeed(speed.s)) { - Log.warn("Speed " + speed.s + " at timestamp " + speed.lu + " is not a number."); - } else { - directionSpeed.push(new DirectionSpeed(direction.s, +speed.s)); - } - } else { - Log.trace('No matching speed found for direction ' + direction.s + " at timestamp " + direction.lu); - } - } - } else { - for (const speed of speedHistory) { - if (this.isValidSpeed(speed.s)) { - const direction = this.findHistoryBackAtTime(speed.lu, directionHistory); - if (direction) { - if (direction.s === '' || direction.s === null || isNaN(+direction.s)) { - Log.warn("Speed " + speed.s + " at timestamp " + speed.lu + " is not a number."); - } else { - directionSpeed.push(new DirectionSpeed(direction.s, +speed.s)); - } - } else { - Log.trace('No matching direction found for speed ' + speed.s + " at timestamp " + speed.lu); - } - } - } - } - - return directionSpeed; - } - - matchStatsStats(directionStats: StatisticsData[], speedStats: StatisticsData[]): DirectionSpeed[] { - const directionSpeed: DirectionSpeed[] = []; - if (this.matchingStrategy == 'direction-first') { - for (const directionStat of directionStats) { - const matchedSpeed = this.findMatchingStatistic(directionStat, speedStats); - if (matchedSpeed) { - directionSpeed.push(new DirectionSpeed(directionStat.mean, matchedSpeed.mean)); - } else { - Log.trace(`No matching speed found for direction ${directionStat.mean} at timestamp start:${directionStat.start} end:${directionStat.end}`); - } - } - } else { - for (const speedStat of speedStats) { - const matchedDirection = this.findMatchingStatistic(speedStat, directionStats); - if (matchedDirection) { - directionSpeed.push(new DirectionSpeed(matchedDirection.mean, speedStat.mean)); - } else { - Log.trace(`No matching direction found for speed ${speedStat.mean} at timestamp start:${speedStat.start} end:${speedStat.end}`); - } - } - } - return directionSpeed; - } - - private findStatsAtTime(timestamp: number, stats: StatisticsData[]): StatisticsData | undefined { - return stats.find((stat) => stat.start <= timestamp && timestamp <= stat.end); - } - - - private findHistoryInPeriod(stat: StatisticsData, history: HistoryData[]): HistoryData | undefined { - const start = stat.start / 1000; - const end = stat.end / 1000; - const selection = history.filter((measurement) => start < measurement.lu && end >= measurement.lu); - if (selection.length == 1) { - return selection[0]; - } else if (selection.length > 1) { - selection.sort((a, b) => b.lu - a.lu); - return selection[Math.trunc(selection.length / 2)]; - } - return undefined; - } - - private findMatchingStatistic(statistic: StatisticsData, stats: StatisticsData[]): StatisticsData | undefined { - return stats.find((stat) => statistic.start === stat.start && statistic.end === stat.end); - } - - private findHistoryBackAtTime(timestamp: number, history: HistoryData[]): HistoryData | undefined { - let match: HistoryData | undefined ; - for (const measurement of history) { - if (measurement.lu <= timestamp) { - match = measurement; - } else { - break; - } - } - return match; - } - - private isInvalidSpeed(speed: string) { - return speed === '' || speed === null || speed === undefined || isNaN(+speed); - } - - private isValidSpeed(speed: string) { - return speed !== '' && speed !== null && speed !== undefined && !isNaN(+speed); - } - -} \ No newline at end of file +} diff --git a/src/matcher/SpeedFirstMatcher.ts b/src/matcher/SpeedFirstMatcher.ts new file mode 100644 index 0000000..43d9c77 --- /dev/null +++ b/src/matcher/SpeedFirstMatcher.ts @@ -0,0 +1,82 @@ +import {Log} from "../util/Log"; +import {DirectionSpeed} from "./DirectionSpeed"; +import {MeasurementMatcher} from "./MeasurementMatcher"; +import {MatchUtils} from "./MatchUtils"; + +export class SpeedFirstMatcher implements MeasurementMatcher { + + matchStatsHistory(directionStats: StatisticsData[], speedHistory: HistoryData[]): DirectionSpeed[] { + const directionSpeed: DirectionSpeed[] = []; + + for (const speed of speedHistory) { + const direction = MatchUtils.findStatsAtTime(speed.lu * 1000, directionStats); + if (direction) { + directionSpeed.push(new DirectionSpeed(direction.mean, +speed.s)); + if (speed.s === '' || speed.s === null || isNaN(+speed.s)) { + Log.warn("Speed " + speed.s + " at timestamp " + direction.start + " is not a number."); + } else { + directionSpeed.push(new DirectionSpeed(direction.mean, +speed.s)); + } + } else { + Log.trace('No matching direction found for speed ' + speed.s + " at timestamp " + speed.lu); + } + } + + return directionSpeed; + } + + matchHistoryStats(directionHistory: HistoryData[], speedStats: StatisticsData[]): DirectionSpeed[] { + const directionSpeed: DirectionSpeed[] = []; + + for (const speed of speedStats) { + const direction = MatchUtils.findHistoryInPeriod(speed, directionHistory); + if (direction) { + if (direction.s === '' || direction.s === null || isNaN(+direction.s)) { + Log.warn("Direction " + direction.s + " at timestamp " + direction.lu + " is not a number."); + } else { + directionSpeed.push(new DirectionSpeed(direction.s, speed.mean)); + } + } else { + Log.trace('No matching direction found for speed ' + speed.mean + " at timestamp " + speed.start); + } + } + + return directionSpeed; + } + + matchHistoryHistory(directionHistory: HistoryData[], speedHistory: HistoryData[]): DirectionSpeed[] { + const directionSpeed: DirectionSpeed[] = []; + + for (const speed of speedHistory) { + if (MatchUtils.isValidSpeed(speed.s)) { + const direction = MatchUtils.findHistoryBackAtTime(speed.lu, directionHistory); + if (direction) { + if (direction.s === '' || direction.s === null || isNaN(+direction.s)) { + Log.warn("Speed " + speed.s + " at timestamp " + speed.lu + " is not a number."); + } else { + directionSpeed.push(new DirectionSpeed(direction.s, +speed.s)); + } + } else { + Log.trace('No matching direction found for speed ' + speed.s + " at timestamp " + speed.lu); + } + } + } + + return directionSpeed; + } + + matchStatsStats(directionStats: StatisticsData[], speedStats: StatisticsData[]): DirectionSpeed[] { + const directionSpeed: DirectionSpeed[] = []; + + for (const speedStat of speedStats) { + const matchedDirection = MatchUtils.findMatchingStatistic(speedStat, directionStats); + if (matchedDirection) { + directionSpeed.push(new DirectionSpeed(matchedDirection.mean, speedStat.mean)); + } else { + Log.trace(`No matching direction found for speed ${speedStat.mean} at timestamp start:${speedStat.start} end:${speedStat.end}`); + } + } + + return directionSpeed; + } +} diff --git a/src/matcher/TimeFrameMatcher.ts b/src/matcher/TimeFrameMatcher.ts new file mode 100644 index 0000000..0693f2a --- /dev/null +++ b/src/matcher/TimeFrameMatcher.ts @@ -0,0 +1,102 @@ +import {MeasurementMatcher} from "./MeasurementMatcher"; +import {DirectionSpeed} from "./DirectionSpeed"; +import {MatchUtils} from "./MatchUtils"; +import {Log} from "../util/Log"; + +export class TimeFrameMatcher implements MeasurementMatcher { + + constructor(private readonly periodSeconds: number) { + } + + matchStatsHistory(directionStats: StatisticsData[], speedHistory: HistoryData[]): DirectionSpeed[] { + const maxBackTimestamp = directionStats[0].start > speedHistory[0].lu ? directionStats[0].start : speedHistory[0].lu + let end = Date.now() / 1000; + const directionSpeed: DirectionSpeed[] = []; + while (end > maxBackTimestamp) { + + let direction = MatchUtils.findStatsAtTime(end * 1000, directionStats); + let speed = MatchUtils.findHistoryBackAtTime(end, speedHistory); + + if (this.checkMeasurementStats(direction, end, "Direction") && this.checkMeasurement(speed, end, "Speed")) { + directionSpeed.push(new DirectionSpeed(direction!.mean, +speed!.s)); + } + end -= this.periodSeconds; + } + return directionSpeed; + } + + matchHistoryStats(directionHistory: HistoryData[], speedStats: StatisticsData[]): DirectionSpeed[] { + const maxBackTimestamp = directionHistory[0].lu > speedStats[0].start / 1000 ? directionHistory[0].lu : speedStats[0].start / 1000 + let end = Date.now() / 1000; + const directionSpeed: DirectionSpeed[] = []; + while (end > maxBackTimestamp) { + + let direction = MatchUtils.findHistoryBackAtTime(end, directionHistory); + let speed = MatchUtils.findStatsAtTime(end * 1000, speedStats); + + if (this.checkMeasurement(direction, end, "Direction") && this.checkMeasurementStats(speed, end, "Speed")) { + directionSpeed.push(new DirectionSpeed(direction!.s, +speed!.mean)); + } + end -= this.periodSeconds; + } + return directionSpeed; + } + + matchHistoryHistory(directionHistory: HistoryData[], speedHistory: HistoryData[]): DirectionSpeed[] { + const maxBackTimestamp = directionHistory[0].lu > speedHistory[0].lu ? directionHistory[0].lu : speedHistory[0].lu + let end = Date.now() / 1000; + const directionSpeed: DirectionSpeed[] = []; + while (end > maxBackTimestamp) { + + let direction = MatchUtils.findHistoryBackAtTime(end, directionHistory); + let speed = MatchUtils.findHistoryBackAtTime(end, speedHistory); + + if (this.checkMeasurement(direction, end, "Direction") && this.checkMeasurement(speed, end, "Speed")) { + directionSpeed.push(new DirectionSpeed(direction!.s, +speed!.s)); + } + end -= this.periodSeconds; + } + return directionSpeed; + } + + matchStatsStats(directionStats: StatisticsData[], speedStats: StatisticsData[]): DirectionSpeed[] { + const maxBackTimestamp = directionStats[0].start > speedStats[0].start ? speedStats[0].start / 1000: speedStats[0].start / 1000; + let end = Date.now() / 1000; + const directionSpeed: DirectionSpeed[] = []; + while (end > maxBackTimestamp) { + + let direction = MatchUtils.findStatsAtTime(end * 1000, directionStats); + let speed = MatchUtils.findStatsAtTime(end * 1000, speedStats); + + if (this.checkMeasurementStats(direction, end, "Direction") && this.checkMeasurementStats(speed, end, "Speed")) { + directionSpeed.push(new DirectionSpeed(direction!.mean, +speed!.mean)); + } + end -= this.periodSeconds; + } + return directionSpeed; + } + + private checkMeasurement(measurement: HistoryData | undefined, timestamp: number, logText: string): boolean { + if (measurement) { + if (MatchUtils.isNumber(measurement.s)) { + return true; + } + Log.warn(logText + " " + measurement.s + " at timestamp " + MatchUtils.cleanDate(measurement.lu) + " is not a number."); + } else { + Log.warn("No " + logText + " found for timestamp " + MatchUtils.cleanDate(timestamp)); + } + return false; + } + + private checkMeasurementStats(measurement: StatisticsData | undefined, timestamp: number, logText: string): boolean { + if (measurement) { + if (MatchUtils.isNumber(measurement.mean)) { + return true; + } + Log.warn(logText + " " + measurement.mean + " at timestamp " + MatchUtils.cleanDate(measurement.start / 1000) + " is not a number."); + } else { + Log.warn("No " + logText + " found for timestamp " + MatchUtils.cleanDate(timestamp)); + } + return false; + } +} diff --git a/src/measurement-provider/HomeAssistantMeasurementProvider.ts b/src/measurement-provider/HomeAssistantMeasurementProvider.ts index 2f30fc0..4ad8e7a 100644 --- a/src/measurement-provider/HomeAssistantMeasurementProvider.ts +++ b/src/measurement-provider/HomeAssistantMeasurementProvider.ts @@ -4,6 +4,9 @@ import {Log} from "../util/Log"; import {CardConfigWrapper} from "../config/CardConfigWrapper"; import {MeasurementMatcher} from "../matcher/MeasurementMatcher"; import {DataPeriod} from "../config/DataPeriod"; +import {SpeedFirstMatcher} from "../matcher/SpeedFirstMatcher"; +import {DirectionFirstMatcher} from "../matcher/DirectionFirstMatcher"; +import {TimeFrameMatcher} from "../matcher/TimeFrameMatcher"; export class HomeAssistantMeasurementProvider { @@ -11,14 +14,21 @@ export class HomeAssistantMeasurementProvider { private cardConfig: CardConfigWrapper; private rawEntities: string[]; private statsEntities: string[]; - private measurementMatcher: MeasurementMatcher; + private measurementMatcher!: MeasurementMatcher; private waitingForMeasurements = false; constructor(cardConfig: CardConfigWrapper) { this.cardConfig = cardConfig; this.rawEntities = cardConfig.createRawEntitiesArray(); this.statsEntities = cardConfig.createStatisticsEntitiesArray(); - this.measurementMatcher = new MeasurementMatcher(this.cardConfig.matchingStrategy); + + if (this.cardConfig.matchingStrategy === 'speed-first') { + this.measurementMatcher = new SpeedFirstMatcher(); + } else if (this.cardConfig.matchingStrategy === 'direction-first') { + this.measurementMatcher = new DirectionFirstMatcher(); + } else if (this.cardConfig.matchingStrategy === 'time-frame') { + this.measurementMatcher = new TimeFrameMatcher(this.cardConfig.dataPeriod.timeInterval); + } } setHass(hass: HomeAssistant): void { @@ -97,12 +107,14 @@ export class HomeAssistantMeasurementProvider { } else { Log.info(`Matched measurements, direction-first: ${matchedMeasurements}`); } - } else { + } else if (this.cardConfig.matchingStrategy === 'speed-first') { if (matchedMeasurements < speedMeasurements) { Log.warn(`Matching results entity ${speedEntity}, ${speedMeasurements - matchedMeasurements} not matched of total ${speedMeasurements} speed measurements`); } else { - Log.info(`Matched measurements, speed-first: ${matchedMeasurements}`); + } + } else { + Log.info(`Matched measurements: ${matchedMeasurements}`); } } @@ -155,4 +167,4 @@ export class HomeAssistantMeasurementProvider { Log.info('Using start time for data query', startTime); return startTime; } -} \ No newline at end of file +}