5 Commits

Author SHA1 Message Date
Claude
87dded2e4a Fix invalid device_class + unit_of_measurement combinations for HA 2026.5
HA 2026.5 enforces strict validation of device_class/unit_of_measurement
pairs in MQTT discovery payloads. Four categories were broken:

- power_factor: unit was sent as "power_factor" (the FIMP field name); HA
  requires the field to be omitted or empty for this device class. Fixed by
  normalising the display unit separately from the FIMP data-path key.

- illuminance: unit "Lux" is rejected; normalise to "lx". When a sensor
  reports luminance as "%" (some Z-Wave devices), the illuminance device
  class is dropped so the entity becomes a generic sensor rather than
  triggering a validation error.

- reactive_power (regular/export meter): unit "VAr" is rejected; HA requires
  lowercase "var".

- reactive_power (extended meter values, e.g. p_import_react): the unit
  determination logic fell through to the generic power branch and sent "W";
  reactive power extended values now emit "var", apparent power values emit
  "VA", and reactive energy values emit "kvarh".

Fixes #31
2026-05-16 21:19:01 +00:00
Adrian Jagielak
ce36669587 v1.6.1 2025-10-16 20:50:41 +02:00
Adrian Jagielak
dac16b0fd4 Tweak 'Ignore Availability Reports' setting 2025-10-16 20:50:03 +02:00
Adrian Jagielak
45182a6416 v1.6.0 2025-10-16 20:16:11 +02:00
Adrian Jagielak
9f2feea8c1 Add setting to always treat all devices as up 2025-10-16 20:15:31 +02:00
7 changed files with 66 additions and 11 deletions

View File

@@ -1,5 +1,13 @@
<!-- https://developers.home-assistant.io/docs/add-ons/presentation#keeping-a-changelog --> <!-- https://developers.home-assistant.io/docs/add-ons/presentation#keeping-a-changelog -->
## 1.6.1 (16.10.2025)
- Tweaked 'Ignore Availability Reports' setting.
## 1.6.0 (16.10.2025)
- Added setting to always treat all devices as up.
## 1.5.0 (16.10.2025) ## 1.5.0 (16.10.2025)
- Reverted: Add ability to specify a custom MQTT broker. - Reverted: Add ability to specify a custom MQTT broker.

View File

@@ -1,6 +1,6 @@
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config # https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
name: Futurehome name: Futurehome
version: '1.5.0' version: '1.6.1'
slug: futurehome slug: futurehome
description: Local Futurehome Smarthub integration description: Local Futurehome Smarthub integration
url: 'https://github.com/adrianjagielak/home-assistant-futurehome' url: 'https://github.com/adrianjagielak/home-assistant-futurehome'
@@ -22,6 +22,7 @@ options:
tp_username: '' tp_username: ''
tp_password: '' tp_password: ''
tp_allow_empty: false tp_allow_empty: false
ignore_availability_reports: false
demo_mode: false demo_mode: false
show_debug_log: false show_debug_log: false
@@ -32,6 +33,7 @@ schema:
tp_username: 'str?' tp_username: 'str?'
tp_password: 'password?' tp_password: 'password?'
tp_allow_empty: 'bool?' tp_allow_empty: 'bool?'
ignore_availability_reports: 'bool?'
demo_mode: 'bool?' demo_mode: 'bool?'
show_debug_log: 'bool?' show_debug_log: 'bool?'

View File

@@ -11,6 +11,7 @@ export FH_PASSWORD=$(bashio::config 'fh_password')
export TP_USERNAME=$(bashio::config 'tp_username') export TP_USERNAME=$(bashio::config 'tp_username')
export TP_PASSWORD=$(bashio::config 'tp_password') export TP_PASSWORD=$(bashio::config 'tp_password')
export TP_ALLOW_EMPTY=$(bashio::config 'tp_allow_empty') export TP_ALLOW_EMPTY=$(bashio::config 'tp_allow_empty')
export IGNORE_AVAILABILITY_REPORTS=$(bashio::config 'ignore_availability_reports')
export DEMO_MODE=$(bashio::config 'demo_mode') export DEMO_MODE=$(bashio::config 'demo_mode')
export SHOW_DEBUG_LOG=$(bashio::config 'show_debug_log') export SHOW_DEBUG_LOG=$(bashio::config 'show_debug_log')

View File

@@ -22,7 +22,14 @@ import { pollVinculum } from './fimp/vinculum';
const localApiPassword = process.env.FH_PASSWORD || ''; const localApiPassword = process.env.FH_PASSWORD || '';
const thingsplexUsername = process.env.TP_USERNAME || ''; const thingsplexUsername = process.env.TP_USERNAME || '';
const thingsplexPassword = process.env.TP_PASSWORD || ''; const thingsplexPassword = process.env.TP_PASSWORD || '';
const thingsplexAllowEmpty = (process.env.TP_ALLOW_EMPTY || '').toLowerCase().includes('true'); const thingsplexAllowEmpty = (process.env.TP_ALLOW_EMPTY || '')
.toLowerCase()
.includes('true');
const ignoreAvailabilityReports = (
process.env.IGNORE_AVAILABILITY_REPORTS || ''
)
.toLowerCase()
.includes('true');
const demoMode = (process.env.DEMO_MODE || '').toLowerCase().includes('true'); const demoMode = (process.env.DEMO_MODE || '').toLowerCase().includes('true');
const showDebugLog = (process.env.SHOW_DEBUG_LOG || '') const showDebugLog = (process.env.SHOW_DEBUG_LOG || '')
.toLowerCase() .toLowerCase()
@@ -267,6 +274,9 @@ import { pollVinculum } from './fimp/vinculum';
return; return;
} }
for (const deviceAvailability of devicesAvailability) { for (const deviceAvailability of devicesAvailability) {
if (ignoreAvailabilityReports) {
deviceAvailability.status = 'UP';
}
haUpdateAvailability({ hubId, deviceAvailability }); haUpdateAvailability({ hubId, deviceAvailability });
await delay(50); await delay(50);
} }

View File

@@ -370,16 +370,25 @@ export function _meter__components(
break; break;
} }
// Map FIMP unit names to HA-compatible unit_of_measurement values
const haUnit =
unit === 'power_factor' ? '' :
unit === 'VAr' ? 'var' :
unit === 'kVArh' ? 'kvarh' : unit;
const component: SensorComponent = { const component: SensorComponent = {
unique_id: componentId, unique_id: componentId,
platform: 'sensor', platform: 'sensor',
entity_category: 'diagnostic', entity_category: 'diagnostic',
name: friendlyName, name: friendlyName,
unit_of_measurement: unit,
state_class: stateClass, state_class: stateClass,
value_template: `{{ value_json['${svc.addr}'].meter.${unit}.val | default(0) }}`, value_template: `{{ value_json['${svc.addr}'].meter.${unit}.val | default(0) }}`,
}; };
if (haUnit) {
component.unit_of_measurement = haUnit;
}
if (deviceClass) { if (deviceClass) {
component.device_class = deviceClass; component.device_class = deviceClass;
} }
@@ -460,16 +469,24 @@ export function _meter__components(
break; break;
} }
// Map FIMP unit names to HA-compatible unit_of_measurement values
const haUnit =
unit === 'VAr' ? 'var' :
unit === 'kVArh' ? 'kvarh' : unit;
const component: SensorComponent = { const component: SensorComponent = {
unique_id: componentId, unique_id: componentId,
platform: 'sensor', platform: 'sensor',
entity_category: 'diagnostic', entity_category: 'diagnostic',
name: friendlyName, name: friendlyName,
unit_of_measurement: unit,
state_class: stateClass, state_class: stateClass,
value_template: `{{ value_json['${svc.addr}'].meter_export.${unit}.val | default(0) }}`, value_template: `{{ value_json['${svc.addr}'].meter_export.${unit}.val | default(0) }}`,
}; };
if (haUnit) {
component.unit_of_measurement = haUnit;
}
if (deviceClass) { if (deviceClass) {
component.device_class = deviceClass; component.device_class = deviceClass;
} }
@@ -510,14 +527,25 @@ export function _meter__components(
// Determine unit based on value name // Determine unit based on value name
let unit = ''; let unit = '';
if ( const isPhasePower =
valueName.startsWith('p_') || valueName.startsWith('p_') ||
valueName.startsWith('p1') || valueName.startsWith('p1') ||
valueName.startsWith('p2') || valueName.startsWith('p2') ||
valueName.startsWith('p3') || valueName.startsWith('p3');
valueName === 'dc_p' if (isPhasePower && valueName.includes('_react')) {
) { unit = 'var';
} else if (isPhasePower && valueName.includes('_apparent')) {
unit = 'VA';
} else if (isPhasePower || valueName === 'dc_p') {
unit = 'W'; unit = 'W';
} else if (
(valueName.startsWith('e_') ||
valueName.startsWith('e1') ||
valueName.startsWith('e2') ||
valueName.startsWith('e3')) &&
valueName.includes('_react')
) {
unit = 'kvarh';
} else if ( } else if (
valueName.startsWith('e_') || valueName.startsWith('e_') ||
valueName.startsWith('e1') || valueName.startsWith('e1') ||
@@ -565,9 +593,9 @@ export function _meter__components(
} }
// Set suggested display precision // Set suggested display precision
if (unit === 'kWh') { if (unit === 'kWh' || unit === 'kvarh') {
component.suggested_display_precision = 3; component.suggested_display_precision = 3;
} else if (unit === 'W' || unit === 'V' || unit === 'A') { } else if (unit === 'W' || unit === 'V' || unit === 'A' || unit === 'var' || unit === 'VA') {
component.suggested_display_precision = 1; component.suggested_display_precision = 1;
} else if (unit === 'Hz') { } else if (unit === 'Hz') {
component.suggested_display_precision = 2; component.suggested_display_precision = 2;

View File

@@ -72,12 +72,15 @@ export function _sensor_numeric__components(
if (!data) return undefined; if (!data) return undefined;
const device_class = data[0]; let device_class = data[0];
const name = data[1]; const name = data[1];
let unit = svc.props?.sup_units?.[0] ?? data[2]; let unit = svc.props?.sup_units?.[0] ?? data[2];
if (unit === 'C') unit = '°C'; if (unit === 'C') unit = '°C';
if (unit === 'F') unit = '°F'; if (unit === 'F') unit = '°F';
if (unit === 'kph') unit = 'km/h'; if (unit === 'kph') unit = 'km/h';
if (unit === 'Lux') unit = 'lx';
// HA rejects illuminance + %; drop device_class for percentage-reporting sensors
if (device_class === 'illuminance' && unit === '%') device_class = undefined;
const state_class = data[3]; const state_class = data[3];
return { return {

View File

@@ -17,6 +17,9 @@ configuration:
tp_allow_empty: tp_allow_empty:
name: Allow Empty Thingsplex Credentials name: Allow Empty Thingsplex Credentials
description: Allow empty Thingsplex username and/or password. description: Allow empty Thingsplex username and/or password.
ignore_availability_reports:
name: Ignore Availability Reports
description: Sometimes the hub incorrectly reports some devices as down. This setting forces all devices to always be treated as up.
demo_mode: demo_mode:
name: Demo Mode name: Demo Mode
description: Use a sample recorded state from a real Futurehome Smarthub to simulate devices. description: Use a sample recorded state from a real Futurehome Smarthub to simulate devices.