diff --git a/README.md b/README.md index 7082da8..44e4c3e 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ ![HaVpdChart Image](https://github.com/mentalilll/ha-vpd-chart/blob/main/assets/image.png?raw=true) -![HaVpdChart Image](https://github.com/mentalilll/ha-vpd-chart/blob/main/assets/bar_view.png) +![HaVpdChart Image](https://github.com/mentalilll/ha-vpd-chart/blob/main/assets/bar_view.png?raw=true) -![HaVpdChart Image](https://github.com/mentalilll/ha-vpd-chart/blob/main/assets/bar_view_light.png) +![HaVpdChart Image](https://github.com/mentalilll/ha-vpd-chart/blob/main/assets/bar_view_light.png?raw=true) + +![HaVpdChart Image](https://github.com/mentalilll/ha-vpd-chart/blob/main/assets/settings.png?raw=true) `HaVpdChart` is a custom card component for Home Assistant that allows for visual representations of VPD (Vapour Pressure Deficit) based on temperature and humidity sensors. It's ideal for monitoring environmental conditions in tents or rooms. @@ -103,7 +105,7 @@ enable_triangle: false #optional enable_crosshair: true #optional enable_fahrenheit: false #optional enable_zoom: false #optional -leaf_temperature_offset: 2 #optional +leaf_temperature_offset: 2 || input_number.leaf_offset_example #optional sensors: - temperature: sensor.temperature_2 humidity: sensor.humidity_2 @@ -139,29 +141,29 @@ calculateVPD: |2- ## Configuration Parameters -| Name | Type | Required | Default | Description | -|-------------------------|---------|--------------|-----------------|-----------------------------------------------------------------------------------------------------| -| type | string | **required** | | Must be `custom:ha-vpd-chart`. | -| air_text | string | optional | `Air` | The text used for temperature readings. Default is "Air". | -| rh_text | string | optional | `RH` | The text used for humidity readings. Default is "RH". | -| kpa_text | string | optional | `kPa` | The text used for kPa readings. Default is "kPa". | -| min_temperature | number | optional | `5` | Minimum temperature in the chart. Default is 5. | -| min_humidity | number | optional | `10` | Minimum humidity in the chart. Default is 10. | -| max_temperature | number | optional | `35` | Maximum temperature in the chart. Default is 35. | -| max_humidity | number | optional | `90` | Maximum humidity in the chart. Default is 90. | -| min_height | number | optional | `200` | Minimum height of the chart as px. Default is 200. | -| leaf_temperature_offset | number | optional | `2` | Sets the Temperature Offset of the Leaf | | -| sensors | list | **required** | | A list of sensors with their temperature and humidity entity IDs, and an optional name for display. | -| vpd_phases | list | optional | See description | A list of VPD phases and their classes for visual representation. See below for defaults. | -| enable_tooltip | boolean | optional | `true` | Tooltip enabled by default. | -| is_bar_view | boolean | optional | `false` | Second view of this chart for fast information of sensors | -| enable_axes | boolean | optional | `true` | Enable Axes on the Chart | -| enable_ghostmap | boolean | optional | `true` | Enable Ghostmap on the Chart | -| enable_triangle | boolean | optional | `true` | Enable Triangle instead of Circle for tooltip marker | -| enable_crosshair | boolean | optional | `true` | Enable MouseHover Crosshair | -| enable_fahrenheit | boolean | optional | `false` | Enable Fahrenheit instead of Celsius | -| enable_zoom | boolean | optional | `false` | Enable zoom function for chart | -| calculateVPD | string | optional | See description | Custom function to calculate VPD. | +| Name | Type | Required | Default | Description | +|-------------------------|----------------|--------------|-----------------------------------------|-----------------------------------------------------------------------------------------------------| +| type | string | **required** | | Must be `custom:ha-vpd-chart`. | +| air_text | string | optional | `Air` | The text used for temperature readings. Default is "Air". | +| rh_text | string | optional | `RH` | The text used for humidity readings. Default is "RH". | +| kpa_text | string | optional | `kPa` | The text used for kPa readings. Default is "kPa". | +| min_temperature | number | optional | `5` | Minimum temperature in the chart. Default is 5. | +| min_humidity | number | optional | `10` | Minimum humidity in the chart. Default is 10. | +| max_temperature | number | optional | `35` | Maximum temperature in the chart. Default is 35. | +| max_humidity | number | optional | `90` | Maximum humidity in the chart. Default is 90. | +| min_height | number | optional | `200` | Minimum height of the chart as px. Default is 200. | +| leaf_temperature_offset | number\|string | optional | `2`\|`input_number.leaf_offset_example` | Sets the Temperature Offset of the Leaf | | +| sensors | list | **required** | | A list of sensors with their temperature and humidity entity IDs, and an optional name for display. | +| vpd_phases | list | optional | See description | A list of VPD phases and their classes for visual representation. See below for defaults. | +| enable_tooltip | boolean | optional | `true` | Tooltip enabled by default. | +| is_bar_view | boolean | optional | `false` | Second view of this chart for fast information of sensors | +| enable_axes | boolean | optional | `true` | Enable Axes on the Chart | +| enable_ghostmap | boolean | optional | `true` | Enable Ghostmap on the Chart | +| enable_triangle | boolean | optional | `true` | Enable Triangle instead of Circle for tooltip marker | +| enable_crosshair | boolean | optional | `true` | Enable MouseHover Crosshair | +| enable_fahrenheit | boolean | optional | `false` | Enable Fahrenheit instead of Celsius | +| enable_zoom | boolean | optional | `false` | Enable zoom function for chart | +| calculateVPD | string | optional | See description | Custom function to calculate VPD. | **Default `vpd_phases` Configuration:** diff --git a/assets/image.png b/assets/image.png index 83751ee..2660c9a 100644 Binary files a/assets/image.png and b/assets/image.png differ diff --git a/assets/settings.png b/assets/settings.png new file mode 100644 index 0000000..92176c8 Binary files /dev/null and b/assets/settings.png differ diff --git a/dist/bar.js b/dist/bar.js index c22e59c..eb72650 100644 --- a/dist/bar.js +++ b/dist/bar.js @@ -21,7 +21,7 @@ export const bar = { this.config.sensors.forEach((sensor) => { let humidity = this._hass.states[sensor.humidity].state; let temperature = this._hass.states[sensor.temperature].state; - let leafTemperature = temperature - (this.config.leaf_temperature_offset || 2); + let leafTemperature = temperature - this.getLeafTemperatureOffset(); if (sensor.leaf_temperature !== undefined) { if (this._hass.states[sensor.leaf_temperature] !== undefined) { leafTemperature = this._hass.states[sensor.leaf_temperature].state; @@ -86,7 +86,7 @@ export const bar = { this.config.sensors.forEach((sensor, index) => { let humidity = this._hass.states[sensor.humidity].state; let temperature = this._hass.states[sensor.temperature].state; - let leafTemperature = temperature - (this.config.leaf_temperature_offset || 2); + let leafTemperature = temperature - this.getLeafTemperatureOffset(); if (sensor.leaf_temperature !== undefined) { leafTemperature = this._hass.states[sensor.leaf_temperature].state; } @@ -183,7 +183,7 @@ export const bar = { if (humidities[tempIndex]) { data['sensor-' + index].push({ time: temperature.last_changed, - vpd: this.calculateVPD(this.toFixedNumber(temperature.state) - (this.config.leaf_temperature_offset || 2), this.toFixedNumber(temperature.state), this.toFixedNumber(humidities[tempIndex].state)).toFixed(2), + vpd: this.calculateVPD(this.toFixedNumber(temperature.state) - this.getLeafTemperatureOffset(), this.toFixedNumber(temperature.state), this.toFixedNumber(humidities[tempIndex].state)).toFixed(2), }); } }); diff --git a/dist/chart.css b/dist/chart.css index e66f317..815fc6a 100644 --- a/dist/chart.css +++ b/dist/chart.css @@ -51,7 +51,7 @@ ha-card.vpd-chart-view, ha-card.vpd-chart-view .vpd-card-container { .vpd-chart-view .highlight { position: absolute; - z-index: 3; + z-index: 2; cursor: pointer; width: 10px; height: 10px; @@ -66,10 +66,6 @@ ha-card.vpd-chart-view, ha-card.vpd-chart-view .vpd-card-container { transform: translate(-50%); } -.vpd-chart-view .highlight:hover { - z-index: 4; -} - .vpd-chart-view .vertical-line { top: 0; bottom: 0; @@ -130,7 +126,7 @@ ha-card.vpd-chart-view, ha-card.vpd-chart-view .vpd-card-container { font-size: 12px; line-height: 1.2; position: absolute; - z-index: 2; + z-index: 3; } .vpd-chart-view .mouse-custom-tooltip { @@ -261,12 +257,17 @@ ha-card.vpd-chart-view, ha-card.vpd-chart-view .vpd-card-container { .vpd-chart-view #sensors .sensor-triangle { width: 16px !important; - height: 0 !important; + height: 16px !important; padding: 0 !important; margin: 0 !important; border: 0 !important; border-radius: 0 !important; background: transparent !important; + z-index: 3; +} + +.vpd-chart-view #sensors .sensor-pointer { + z-index: 3; } .vpd-chart-view #sensors .sensor-triangle:after { @@ -281,4 +282,20 @@ ha-card.vpd-chart-view, ha-card.vpd-chart-view .vpd-card-container { content: " "; font-size: 0; line-height: 0; +} + +.vpd-chart-view .ghostmapClick { + display: inline-block; + vertical-align: text-bottom; + cursor: pointer; + pointer-events: all; + fill: #ffffff; +} + +.vpd-chart-view .ghostmapClick:hover { + fill: #00aa00; +} + +.vpd-chart-view .ghostmapClick:active, .vpd-chart-view .ghostmapClick.active { + fill: #00ff00; } \ No newline at end of file diff --git a/dist/chart.js b/dist/chart.js index 008546c..de2648c 100644 --- a/dist/chart.js +++ b/dist/chart.js @@ -109,7 +109,7 @@ export const chart = { const temperature = this.min_temperature + (temperatureRange * yPercent / 100); const humidity = this.max_humidity - (humidityRange * xPercent / 100); - const leafTemperature = temperature - (this.config.leaf_temperature_offset || 2); + const leafTemperature = temperature - this.getLeafTemperatureOffset(); const vpd = this.calculateVPD(leafTemperature, temperature, humidity); @@ -275,7 +275,7 @@ export const chart = { if (this._hass.states[sensor.humidity] && this._hass.states[sensor.temperature]) { const humidity = parseFloat(this._hass.states[sensor.humidity].state); const temperature = parseFloat(this._hass.states[sensor.temperature].state); - let leafTemperature = temperature - (this.config.leaf_temperature_offset || 2); + let leafTemperature = temperature - this.getLeafTemperatureOffset(); if (sensor.leaf_temperature !== undefined) { leafTemperature = parseFloat(this._hass.states[sensor.leaf_temperature].state); } @@ -284,8 +284,8 @@ export const chart = { } else { vpd = this.calculateVPD(leafTemperature, temperature, humidity).toFixed(2); } - const min_vpd = this.calculateVPD(temperature - (this.config.leaf_temperature_offset || 2), temperature, this.max_humidity); - const max_vpd = this.calculateVPD(temperature - (this.config.leaf_temperature_offset || 2), temperature, this.min_humidity); + const min_vpd = this.calculateVPD(temperature - this.getLeafTemperatureOffset(), temperature, this.max_humidity); + const max_vpd = this.calculateVPD(temperature - this.getLeafTemperatureOffset(), temperature, this.min_humidity); const relativeVpd = vpd - min_vpd; const totalVpdRange = max_vpd - min_vpd; const percentageVpd = (relativeVpd / totalVpdRange) * 100; @@ -302,6 +302,15 @@ export const chart = { // Check and append only if elements are Nodes if (pointerElements.pointer instanceof Node) { sensors.appendChild(pointerElements.pointer); + if (this.enable_ghostclick) { + let ghostmapClick = this.querySelector(`.ghostmapClick_${index}`) || document.createElement('div'); + let classes = `ghostmapClick ghostmapClick_${index}`; + if (this.clickedTooltip) { + classes += ' active'; + } + ghostmapClick.setAttribute('class', classes); + ghostmapClick.addEventListener('click', this.toggleSensorDetails.bind(this, index)); + } } if (pointerElements.horizontalLine instanceof Node) { sensors.appendChild(pointerElements.horizontalLine); @@ -334,30 +343,46 @@ export const chart = { verticalLine.style.left = `calc(${percentageHumidity}% - 0.5px)`; let tooltip = null; - if (this.enable_tooltip) { - tooltip = this.querySelector(`.custom-tooltip[data-index="${index}"]`) || document.createElement('div'); - tooltip.className = `custom-tooltip custom-tooltip-${index}`; - tooltip.setAttribute('data-index', index.toString()); - tooltip.innerHTML = `${sensorName} ${this.kpa_text ? this.kpa_text + ':' : ''} ${vpd}${this.rh_text ? this.rh_text + ':' : ''} ${humidity}%${this.air_text ? this.air_text + ':' : ''} ${temperature}${this.unit_temperature}`; - tooltip.style.bottom = `${100 - percentageTemperature}%`; - tooltip.style.left = `${percentageHumidity}%`; - if ((tooltip.offsetLeft + (tooltip.offsetWidth / 2)) > this.content.offsetWidth) { - const containerWidth = this.content.offsetWidth; - const overflowWidth = (tooltip.offsetLeft + (tooltip.offsetWidth / 2)) - containerWidth; - tooltip.style.left = `calc(${percentageHumidity}% - ${overflowWidth}px)`; - } + tooltip = this.querySelector(`.custom-tooltip[data-index="${index}"]`) || document.createElement('div'); + tooltip.className = `custom-tooltip custom-tooltip-${index}`; + tooltip.setAttribute('data-index', index.toString()); + // if sensorName not undefined or empty then show in tooltip + let innerHTML = ''; + if (sensorName !== undefined && sensorName !== '') { + innerHTML += `${sensorName}`; + } + innerHTML += `${this.kpa_text ? this.kpa_text + ': ' : ''}${vpd}`; + innerHTML += `${this.rh_text ? this.rh_text + ': ' : ''}${humidity}%`; + innerHTML += `${this.air_text ? this.air_text + ': ' : ''}${temperature}${this.unit_temperature}`; + innerHTML += `${this.getPhaseClass(vpd)}`; + if (this.enable_ghostclick) { + innerHTML += ` `; + } + tooltip.innerHTML = innerHTML; + + + tooltip.style.bottom = `${100 - percentageTemperature}%`; + tooltip.style.left = `${percentageHumidity}%`; + if ((tooltip.offsetLeft + (tooltip.offsetWidth / 2)) > this.content.offsetWidth) { + const containerWidth = this.content.offsetWidth; + const overflowWidth = (tooltip.offsetLeft + (tooltip.offsetWidth / 2)) - containerWidth + 5; + tooltip.style.left = `calc(${percentageHumidity}% - ${overflowWidth}px)`; + } + if ((tooltip.offsetLeft - (tooltip.offsetWidth / 2)) < 0) { + const overflowWidth = (tooltip.offsetWidth / 2) - tooltip.offsetLeft + 5; + tooltip.style.left = `calc(${percentageHumidity}% + ${overflowWidth}px)`; + } if (!pointer.isConnected) { pointer.addEventListener('mouseover', this.showSensorDetails.bind(this, index)); pointer.addEventListener('mouseleave', this.hideSensorDetails.bind(this, index)); - pointer.addEventListener('click', this.toggleSensorDetails.bind(this, index)); } return {pointer, horizontalLine, verticalLine, tooltip}; }, toggleSensorDetails(index) { this.clickedTooltip = !this.clickedTooltip; - if (this.clickedTooltip) { + if (!this.clickedTooltip) { this.hideSensorDetails(index); } else { this.showSensorDetails(index); diff --git a/dist/ha-vpd-chart-editor.js b/dist/ha-vpd-chart-editor.js index 9d20027..cd818f3 100644 --- a/dist/ha-vpd-chart-editor.js +++ b/dist/ha-vpd-chart-editor.js @@ -96,15 +96,19 @@ export class HaVpdChartEditor extends HTMLElement { } get _enable_axes() { - return this._config.enable_axes !== undefined ? this._config.enable_axes : false; + return this._config.enable_axes !== undefined ? this._config.enable_axes : true; + } + + get _enable_ghostclick() { + return this._config.enable_ghostclick !== undefined ? this._config.enable_ghostclick : true; } get _enable_ghostmap() { - return this._config.enable_ghostmap !== undefined ? this._config.enable_ghostmap : false; + return this._config.enable_ghostmap !== undefined ? this._config.enable_ghostmap : true; } get _enable_triangle() { - return this._config.enable_triangle || false; + return this._config.enable_triangle || true; } get _enable_crosshair() { @@ -112,7 +116,7 @@ export class HaVpdChartEditor extends HTMLElement { } get _enable_tooltip() { - return this._config.enable_tooltip !== undefined ? this._config.enable_tooltip : false; + return this._config.enable_tooltip !== undefined ? this._config.enable_tooltip : true; } get _ghostmap_hours() { @@ -155,7 +159,10 @@ export class HaVpdChartEditor extends HTMLElement { if (value === "off") { value = false; } - + // if empty value + if (value === "") { + value = undefined; + } if (Object.isExtensible(this._config)) { if (this._config[configValue] !== value) { this._config[configValue] = value; @@ -239,7 +246,7 @@ export class HaVpdChartEditor extends HTMLElement { - + @@ -275,38 +282,47 @@ export class HaVpdChartEditor extends HTMLElement { + + + + + - - + + + @@ -355,6 +371,7 @@ export class HaVpdChartEditor extends HTMLElement { {id: 'min_height', prop: '_min_height', type: 'value'}, {id: 'is_bar_view', prop: '_is_bar_view', type: 'checked'}, {id: 'enable_axes', prop: '_enable_axes', type: 'checked'}, + {id: 'enable_ghostclick', prop: '_enable_ghostclick', type: 'checked'}, {id: 'enable_ghostmap', prop: '_enable_ghostmap', type: 'checked'}, {id: 'enable_triangle', prop: '_enable_triangle', type: 'checked'}, {id: 'enable_crosshair', prop: '_enable_crosshair', type: 'checked'}, @@ -369,7 +386,15 @@ export class HaVpdChartEditor extends HTMLElement { const element = this.shadowRoot.querySelector(`#${id}`); if (element) { if (Object.isExtensible(element)) { - element[type] = this[prop]; + if (type === "checked") { + if (this[prop]) { + element[type] = 'checked'; + } else { + element[type] = ''; + } + } else { + element[type] = this[prop]; + } } else { console.warn('Cannot define property on a non-extensible object'); } diff --git a/dist/ha-vpd-chart.js b/dist/ha-vpd-chart.js index 795891c..20c037d 100644 --- a/dist/ha-vpd-chart.js +++ b/dist/ha-vpd-chart.js @@ -12,7 +12,7 @@ const FAHRENHEIT_UNIT_TEMPERATURE = '°F'; const CONFIG_KEYS = [ 'vpd_phases', 'sensors', 'air_text', 'rh_text', 'kpa_text', 'min_temperature', 'max_temperature', 'min_humidity', 'max_humidity', 'min_height', - 'is_bar_view', 'enable_axes', 'enable_ghostmap', 'enable_triangle', + 'is_bar_view', 'enable_axes', 'enable_ghostclick', 'enable_ghostmap', 'enable_triangle', 'enable_tooltip', 'enable_crosshair', 'enable_fahrenheit', 'ghostmap_hours', 'unit_temperature', 'enable_zoom' ]; @@ -25,7 +25,7 @@ class HaVpdChart extends HTMLElement { initializeDefaults() { this.vpd_phases = [ - {lower: -0.6, upper: 0, className: 'gray-danger-zone', color: '#999999'}, + {upper: 0, className: 'gray-danger-zone', color: '#999999'}, {lower: 0, upper: 0.4, className: 'under-transpiration', color: '#1a6c9c'}, {lower: 0.4, upper: 0.8, className: 'early-veg', color: '#22ab9c'}, {lower: 0.8, upper: 1.2, className: 'late-veg', color: '#9cc55b'}, @@ -47,6 +47,7 @@ class HaVpdChart extends HTMLElement { this.rh_text = "RH"; this.kpa_text = "kPa"; this.enable_axes = true; + this.enable_ghostclick = true; this.enable_ghostmap = true; this.enable_triangle = false; this.enable_crosshair = false; @@ -75,6 +76,7 @@ class HaVpdChart extends HTMLElement { enable_tooltip: {type: Boolean}, is_bar_view: {type: Boolean}, enable_axes: {type: Boolean}, + enable_ghostclick: {type: Boolean}, enable_ghostmap: {type: Boolean}, enable_triangle: {type: Boolean}, enable_crosshair: {type: Boolean}, diff --git a/dist/methods.js b/dist/methods.js index 9e5a02f..17ed0e2 100644 --- a/dist/methods.js +++ b/dist/methods.js @@ -56,7 +56,7 @@ export const methods = { for (let Tair = minTemperature; Tair <= maxTemperature; Tair += stepsTemperature) { const row = []; - const Tleaf = Tair - (this.config.leaf_temperature_offset || 2); + const Tleaf = Tair - 2; for (let RH = minHumidity; RH <= maxHumidity; RH += stepsHumidity) { const vpd = this.calculateVPD(Tleaf, Tair, RH).toFixed(2); @@ -70,6 +70,19 @@ export const methods = { } return vpdMatrix; - } + }, + getLeafTemperatureOffset() { + let offset = 2; + if (typeof this.config.leaf_temperature_offset === 'number') { + return this.config.leaf_temperature_offset; + } + if (typeof this.config.leaf_temperature_offset === 'string') { + offset = this._hass.states[this.config.leaf_temperature_offset].state; + if (!isNaN(offset)) { + return offset; + } + } + return offset; + }, } \ No newline at end of file