From c39b94b28ed8be55f0f025e4886c09e3955acd3b Mon Sep 17 00:00:00 2001 From: Serhii Chaichuk <128085928+chaichuk@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:09:07 +0200 Subject: [PATCH 1/7] Update __init__.py --- custom_components/NBEConnect/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/custom_components/NBEConnect/__init__.py b/custom_components/NBEConnect/__init__.py index d038659..63ab1a7 100644 --- a/custom_components/NBEConnect/__init__.py +++ b/custom_components/NBEConnect/__init__.py @@ -65,9 +65,7 @@ async def _async_update_data(self): """Fetch the latest data from the device.""" try: operating_data = await self.hass.async_add_executor_job(self.proxy.get, "operating_data/") - consumption_data = await self.hass.async_add_executor_job(self.proxy.get, "consumption_data/counter") - logger.debug(operating_data) - logger.debug(consumption_data) + consumption_data = await self.hass.async_add_executor_job(self.proxy.get, "consumption_data/counter") if operating_data is not None: if consumption_data is not None: operating_data = operating_data + consumption_data From 1f09853be259bef04d395f069a0aa72e548bb5a3 Mon Sep 17 00:00:00 2001 From: Serhii Chaichuk <128085928+chaichuk@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:09:54 +0200 Subject: [PATCH 2/7] Update button.py --- custom_components/NBEConnect/button.py | 66 ++++++++++++++++---------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/custom_components/NBEConnect/button.py b/custom_components/NBEConnect/button.py index 6a0210e..7076b96 100644 --- a/custom_components/NBEConnect/button.py +++ b/custom_components/NBEConnect/button.py @@ -1,48 +1,66 @@ -from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.components.button import ButtonEntity - from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .protocol import Proxy from logging import getLogger _LOGGER = getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): - coordinator = hass.data[DOMAIN][config_entry.entry_id+'_coordinator'] - proxy = hass.data[DOMAIN][config_entry.entry_id] +async def async_setup_entry(hass, entry, async_add_entities): + dc = hass.data[DOMAIN][entry.entry_id+'_coordinator'] + + serial = "Unknown" + if hasattr(dc.proxy, 'serial') and dc.proxy.serial: + serial = dc.proxy.serial + elif entry.data.get("serial"): + serial = entry.data.get("serial") + + device_info = { + "identifiers": {(DOMAIN, serial)}, + "name": "NBE Boiler", + "manufacturer": "NBE", + "model": "V7/V13 Controller" + } async_add_entities([ - RTBSignalButton(coordinator, proxy, "Start Boiler", "settings/misc/start", "nbestart", "1"), - RTBSignalButton(coordinator, proxy, "Stop Boiler", "settings/misc/stop", "nbestop", "1"), - RTBSignalButton(coordinator, proxy, "Reset Boiler Alarm", "settings/misc/reset_alarm", "nbereset", "1") + RTBSignalButton( + coordinator=dc, + name="Reset Boiler Alarm", + path="settings/misc/reset_alarm", + uid="nbereset", + value="1", + icon="mdi:alert-remove-outline", + dev_info=device_info + ) ]) class RTBSignalButton(CoordinatorEntity, ButtonEntity): - """Representation of a signal switch.""" + """Representation of a signal button.""" - def __init__(self, coordinator, proxy, name, path, uid, value): - """Initialize the switch.""" + def __init__(self, coordinator, name, path, uid, value, icon, dev_info): super().__init__(coordinator) self._name = name - self._state = False - self.proxy = proxy self._path = path self._value = value self.uid = uid + self._attr_icon = icon + self._dev_info = dev_info + self._attr_unique_id = f"{coordinator.entry_id}_{uid}" @property def name(self): - """Return the name of the switch.""" - return self._name - - @property - def unique_id(self): - return self.uid + return f"NBE {self._name}" def press(self) -> None: """Press the button.""" - _LOGGER.debug(f"asynch press {self._name} - awaiting sendboilercmd..") - self.proxy.set(self._path, self._value) - _LOGGER.debug(f"asynch press {self._name} - Done!") \ No newline at end of file + _LOGGER.debug(f"Async press {self._name}") + proxy = self.coordinator.proxy + if proxy: + try: + proxy.set(self._path, self._value) + except Exception as e: + _LOGGER.warning(f"Button press error (might be okay): {e}") + _LOGGER.debug(f"Async press {self._name} - Done!") + + @property + def device_info(self): + return self._dev_info From 7d22023cf2207ddf1d883a6ed2d7152c44194073 Mon Sep 17 00:00:00 2001 From: Serhii Chaichuk <128085928+chaichuk@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:10:24 +0200 Subject: [PATCH 3/7] Update const.py --- custom_components/NBEConnect/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/NBEConnect/const.py b/custom_components/NBEConnect/const.py index f8ba77f..837a9b2 100644 --- a/custom_components/NBEConnect/const.py +++ b/custom_components/NBEConnect/const.py @@ -1,2 +1,2 @@ DOMAIN = "nbeconnect" -PLATFORMS = ['sensor', 'button'] \ No newline at end of file +PLATFORMS = ['sensor', 'button', "number", "switch"] From eeed413dd4953635bdd5f8a26387b00b4eaf976c Mon Sep 17 00:00:00 2001 From: Serhii Chaichuk <128085928+chaichuk@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:10:42 +0200 Subject: [PATCH 4/7] Update manifest.json --- custom_components/NBEConnect/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/NBEConnect/manifest.json b/custom_components/NBEConnect/manifest.json index 22cfcbf..82d4a07 100644 --- a/custom_components/NBEConnect/manifest.json +++ b/custom_components/NBEConnect/manifest.json @@ -10,5 +10,5 @@ "issue_tracker": "n/a", "iot_class": "local_polling", "integration_type": "device", - "version": "0.1.4" + "version": "1.0.0" } From 164d148eeda95ad5315080fc9577407d8a02c07c Mon Sep 17 00:00:00 2001 From: Serhii Chaichuk <128085928+chaichuk@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:12:11 +0200 Subject: [PATCH 5/7] Create number.py --- custom_components/NBEConnect/number.py | 112 +++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 custom_components/NBEConnect/number.py diff --git a/custom_components/NBEConnect/number.py b/custom_components/NBEConnect/number.py new file mode 100644 index 0000000..6b184cc --- /dev/null +++ b/custom_components/NBEConnect/number.py @@ -0,0 +1,112 @@ +from homeassistant.components.number import ( + NumberEntity, + NumberDeviceClass, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN +from logging import getLogger + +_LOGGER = getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the NBE-RTB numbers.""" + dc = hass.data[DOMAIN][entry.entry_id+'_coordinator'] + + serial = "Unknown" + if hasattr(dc.proxy, 'serial') and dc.proxy.serial: + serial = dc.proxy.serial + elif entry.data.get("serial"): + serial = entry.data.get("serial") + + device_info = { + "identifiers": {(DOMAIN, serial)}, + "name": "NBE Boiler", + "manufacturer": "NBE", + "model": "V7/V13 Controller" + } + + async_add_entities([ + RTBNumber( + coordinator=dc, + name='Boiler Target Temp', + read_key='operating_data/boiler_ref', + write_key='settings/boiler/temp', + unit="°C", + min_val=10, + max_val=85, + read_multiplier=1, + write_multiplier=1, + icon="mdi:thermometer-check", + dev_info=device_info + ), + + RTBNumber( + coordinator=dc, + name='Hopper Content', + read_key='operating_data/content', + write_key='settings/hopper/content', + unit="kg", + min_val=0, + max_val=9000, + read_multiplier=10, + write_multiplier=1, + icon="mdi:silo", + dev_info=device_info + ), + ]) + +class RTBNumber(CoordinatorEntity, NumberEntity): + + def __init__(self, coordinator, name, read_key, write_key, unit, min_val, max_val, read_multiplier=1, write_multiplier=1, icon=None, dev_info=None): + super().__init__(coordinator) + self.read_key = read_key + self.write_key = write_key + self.sensorname = name + self._attr_native_unit_of_measurement = unit + self._attr_native_min_value = min_val + self._attr_native_max_value = max_val + self._attr_device_class = NumberDeviceClass.TEMPERATURE if "Temp" in name else NumberDeviceClass.WEIGHT + self._attr_unique_id = f"{coordinator.entry_id}_{write_key}" + + self.read_multiplier = read_multiplier + self.write_multiplier = write_multiplier + self._attr_icon = icon + self._dev_info = dev_info + + @property + def name(self): + return f"NBE {self.sensorname}" + + @property + def native_value(self): + val = self.coordinator.rtbdata.get(self.read_key) + try: + if val: + return float(val) * self.read_multiplier + return None + except ValueError: + return None + + @property + def device_info(self): + return self._dev_info + + async def async_set_native_value(self, value: float) -> None: + _LOGGER.info(f"Setting NBE value for {self.write_key} to {value}") + + proxy = self.coordinator.proxy + + if proxy: + try: + val_to_send = int(value * self.write_multiplier) + + await self.coordinator.hass.async_add_executor_job(proxy.set, self.write_key, str(val_to_send)) + + except (OSError, IOError) as e: + _LOGGER.warning(f"NBEConnect: Command sent via {self.write_key}, but response decode failed (expected). Error: {e}") + except Exception as e: + _LOGGER.error(f"NBEConnect: Unexpected error: {e}") + + await self.coordinator.async_request_refresh() + else: + _LOGGER.error("NBEConnect: Cannot find 'proxy' in coordinator to send command!") From 9c3d9c6d4764ea0ccde9f49d793c74dae94808b9 Mon Sep 17 00:00:00 2001 From: Serhii Chaichuk <128085928+chaichuk@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:13:25 +0200 Subject: [PATCH 6/7] Update sensor.py --- custom_components/NBEConnect/sensor.py | 104 ++++++++++++++++--------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/custom_components/NBEConnect/sensor.py b/custom_components/NBEConnect/sensor.py index 02c2c9a..7473f45 100644 --- a/custom_components/NBEConnect/sensor.py +++ b/custom_components/NBEConnect/sensor.py @@ -22,30 +22,52 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the NBE-RTB sensors.""" - _LOGGER.info("This is debugging from sensor.py") + _LOGGER.info("Setting up NBE sensors with Renamed Temperatures...") - - #DataUpdateCoordinator dc = hass.data[DOMAIN][entry.entry_id+'_coordinator'] - async_add_entities([ - RTBBinarySensor(dc, 'Boiler Running', 'operating_data/power_pct', 'boiler_power_pct', BinarySensorDeviceClass.HEAT), - RTBBinarySensor(dc, 'Boiler Alarm', 'operating_data/off_on_alarm', 'boiler_state_off_on_alarm', BinarySensorDeviceClass.PROBLEM), - RTBSensor(dc, 'Boiler Temperature', 'operating_data/boiler_temp', 'boiler_temp', "\u00b0C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT), - RTBSensor(dc, 'DWH Temperature', 'operating_data/sun_dhw_temp', 'dhw_temp', "\u00b0C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT), - RTBSensor(dc, 'External Temperature', 'operating_data/external_temp', 'external_temp', "\u00b0C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT), - RTBSensor(dc, 'Boiler Effect', 'operating_data/power_kw', 'power_kw', "kW", SensorDeviceClass.POWER, SensorStateClass.MEASUREMENT), - RTBSensor(dc, 'Boiler Power', 'operating_data/power_pct', 'power_pct', "%", SensorDeviceClass.POWER_FACTOR, SensorStateClass.MEASUREMENT), - RTBSensor(dc, 'Total Consumption', 'consumption_data/counter', 'pelletcounter', "kg", SensorDeviceClass.WEIGHT, SensorStateClass.TOTAL_INCREASING), - ]) - _LOGGER.info(f"Sensor.py, sensors where added!") + serial = "Unknown" + if hasattr(dc.proxy, 'serial') and dc.proxy.serial: + serial = dc.proxy.serial + elif entry.data.get("serial"): + serial = entry.data.get("serial") + + device_info = { + "identifiers": {(DOMAIN, serial)}, + "name": "NBE Boiler", + "manufacturer": "NBE", + "model": "V7/V13 Controller", + "configuration_url": f"http://{entry.data.get('ip_address')}" if entry.data.get('ip_address') else None + } + + sensors = [ + RTBBinarySensor(dc, 'Boiler Running', 'operating_data/power_pct', 'boiler_power_pct', BinarySensorDeviceClass.RUNNING, icon="mdi:fire", dev_info=device_info), + RTBBinarySensor(dc, 'Boiler Alarm', 'operating_data/off_on_alarm', 'boiler_state_off_on_alarm', BinarySensorDeviceClass.PROBLEM, icon="mdi:alert-circle", dev_info=device_info), + RTBBinarySensor(dc, 'Boiler Pump', 'operating_data/boiler_pump_state', 'house_pump_state', None, icon="mdi:pump", dev_info=device_info), + RTBSensor(dc, 'Temperature Boiler', 'operating_data/boiler_temp', 'boiler_temp', "°C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT, icon="mdi:thermometer", dev_info=device_info), + RTBSensor(dc, 'Temperature Target', 'operating_data/boiler_ref', 'boiler_ref', "°C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT, icon="mdi:thermometer-check", dev_info=device_info), + RTBSensor(dc, 'Temperature Smoke', 'operating_data/smoke_temp', 'smoke_temp', "°C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT, icon="mdi:thermometer-alert", dev_info=device_info), + RTBSensor(dc, 'Temperature Return', 'operating_data/return_temp', 'return_temp', "°C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT, icon="mdi:thermometer-water", dev_info=device_info), + RTBSensor(dc, 'Temperature Shaft', 'operating_data/shaft_temp', 'shaft_temp', "°C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT, icon="mdi:thermometer-lines", dev_info=device_info), + RTBSensor(dc, 'Temperature External', 'operating_data/external_temp', 'external_temp', "°C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT, icon="mdi:thermometer", dev_info=device_info), + RTBSensor(dc, 'Temperature DHW', 'operating_data/dhw_temp', 'dhw_temp', "°C", SensorDeviceClass.TEMPERATURE, SensorStateClass.MEASUREMENT, icon="mdi:water-thermometer", dev_info=device_info), + RTBSensor(dc, 'Boiler Power %', 'operating_data/power_pct', 'power_pct', "%", SensorDeviceClass.POWER_FACTOR, SensorStateClass.MEASUREMENT, icon="mdi:lightning-bolt-outline", dev_info=device_info), + RTBSensor(dc, 'Boiler Power kW', 'operating_data/power_kw', 'power_kw', "kW", SensorDeviceClass.POWER, SensorStateClass.MEASUREMENT, icon="mdi:lightning-bolt", dev_info=device_info), + RTBSensor(dc, 'Photo Sensor', 'operating_data/photo_level', 'photo_level', "lx", SensorDeviceClass.ILLUMINANCE, SensorStateClass.MEASUREMENT, icon="mdi:eye", dev_info=device_info), + RTBSensor(dc, 'Oxygen (O2)', 'operating_data/oxygen', 'oxygen_level', "%", None, SensorStateClass.MEASUREMENT, icon="mdi:gas-cylinder", dev_info=device_info), + RTBSensor(dc, 'Total Pellet Consumption', 'consumption_data/counter', 'pelletcounter', "kg", SensorDeviceClass.WEIGHT, SensorStateClass.TOTAL_INCREASING, icon="mdi:chart-line", dev_info=device_info), + RTBSensor(dc, 'Hopper Content', 'operating_data/content', 'hopper_content', "kg", SensorDeviceClass.WEIGHT, SensorStateClass.MEASUREMENT, multiplier=10, icon="mdi:silo", dev_info=device_info), + RTBSensor(dc, 'State Code', 'operating_data/state', 'boiler_state_code', None, None, SensorStateClass.MEASUREMENT, icon="mdi:information-outline", dev_info=device_info), + ] + + async_add_entities(sensors) + _LOGGER.info(f"NBE Sensors updated successfully!") class RTBSensor(CoordinatorEntity, SensorEntity): """Representation of an RTB sensor.""" - def __init__(self, coordinator, name, client_key, uid, unitofmeassurement, device_class, state_class): - """Initialize the sensor.""" + def __init__(self, coordinator, name, client_key, uid, unitofmeassurement, device_class, state_class, multiplier=None, icon=None, dev_info=None): super().__init__(coordinator) self.client_key = client_key self._device_class = device_class @@ -53,16 +75,16 @@ def __init__(self, coordinator, name, client_key, uid, unitofmeassurement, devic self.uid = uid self._unit_of_measurement = unitofmeassurement self._state_class = state_class + self.multiplier = multiplier + self._attr_icon = icon + self._dev_info = dev_info @property def name(self): - """Return the name of the sensor.""" - #_LOGGER.info(f"sensor.py (name) returning \"NBE {self.sensorname}\"") return f"NBE {self.sensorname}" @property - def unit_of_measurement(self): - """Return the unit of measurement of the sensor.""" + def native_unit_of_measurement(self): return self._unit_of_measurement @property @@ -70,50 +92,54 @@ def unique_id(self): return self.uid @property - def state(self): - """Return the state of the sensor.""" + def native_value(self): state = self.coordinator.rtbdata.get(self.client_key) - #_LOGGER.info(f"Sensor.py RTBSensor (state) returning \"{state}\"") + + if not state or "999.9" in str(state): + return None + + if self.multiplier: + try: + val_float = float(state) + return val_float * self.multiplier + except ValueError: + return state + return state @property def device_class(self): - """Return the device class of the sensor.""" return self._device_class @property def state_class(self): - """Return the state class of the sensor.""" return self._state_class + @property + def device_info(self): + return self._dev_info + class RTBBinarySensor(CoordinatorEntity, BinarySensorEntity): """Representation of an RTB binary sensor.""" - def __init__(self, coordinator, name, client_key, uid, device_class): - """Initialize the binary sensor.""" + def __init__(self, coordinator, name, client_key, uid, device_class, icon=None, dev_info=None): super().__init__(coordinator) self.client_key = client_key self._device_class = device_class self.sensorname = name self.uid = uid + self._attr_icon = icon + self._dev_info = dev_info @property def name(self): - """Return the name of the binary sensor.""" - #_LOGGER.info(f"sensor.py RTBBinarySensor (name) returning \"NBE {self.sensorname}\"") return f"NBE {self.sensorname}" @property def is_on(self): - _LOGGER.debug("is_on called") - """Return the state of the binary sensor.""" s = self.coordinator.rtbdata.get(self.client_key) - _LOGGER.debug(f"sensor.py RTBBinarySensor (is_on) value \"{s}\"") - if "power_pct" in self.client_key: - return s != "0" - if "off_on_alarm" in self.client_key: - return s == "2" - return s == "0" + if not s: return None + return s != "0" @property def unique_id(self): @@ -121,6 +147,8 @@ def unique_id(self): @property def device_class(self): - """Return the device class of the binary sensor.""" return self._device_class + @property + def device_info(self): + return self._dev_info From 7d04d640c8a4b6fcef046b2bc78d7a2c2013b0dc Mon Sep 17 00:00:00 2001 From: Serhii Chaichuk <128085928+chaichuk@users.noreply.github.com> Date: Sat, 27 Dec 2025 00:14:36 +0200 Subject: [PATCH 7/7] Create switch.py --- custom_components/NBEConnect/switch.py | 93 ++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 custom_components/NBEConnect/switch.py diff --git a/custom_components/NBEConnect/switch.py b/custom_components/NBEConnect/switch.py new file mode 100644 index 0000000..6b3a8a6 --- /dev/null +++ b/custom_components/NBEConnect/switch.py @@ -0,0 +1,93 @@ +from homeassistant.components.switch import ( + SwitchEntity, + SwitchDeviceClass, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN +from logging import getLogger + +_LOGGER = getLogger(__name__) + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the NBE-RTB switches.""" + dc = hass.data[DOMAIN][entry.entry_id+'_coordinator'] + + serial = "Unknown" + if hasattr(dc.proxy, 'serial') and dc.proxy.serial: + serial = dc.proxy.serial + elif entry.data.get("serial"): + serial = entry.data.get("serial") + + device_info = { + "identifiers": {(DOMAIN, serial)}, + "name": "NBE Boiler", + "manufacturer": "NBE", + "model": "V7/V13 Controller" + } + + async_add_entities([ + RTBSwitch( + coordinator=dc, + name="Boiler Power", + read_key="operating_data/state", + write_key_on="settings/misc/start", + write_key_off="settings/misc/stop", + icon="mdi:power", + dev_info=device_info + ), + + ]) + +class RTBSwitch(CoordinatorEntity, SwitchEntity): + """Representation of a NBE Switch.""" + + def __init__(self, coordinator, name, read_key, write_key_on, write_key_off, icon, dev_info, is_pump=False): + super().__init__(coordinator) + self.read_key = read_key + self.write_key_on = write_key_on + self.write_key_off = write_key_off + self.sensorname = name + self._attr_icon = icon + self._attr_device_class = SwitchDeviceClass.SWITCH + self._dev_info = dev_info + self._attr_unique_id = f"{coordinator.entry_id}_{read_key}_switch" + self._is_pump = is_pump + + @property + def name(self): + return f"NBE {self.sensorname}" + + @property + def is_on(self): + val = self.coordinator.rtbdata.get(self.read_key) + + if val is None: + return None + + if self.read_key == "operating_data/state": + return str(val) != "14" + + return str(val) != "0" + + async def async_turn_on(self, **kwargs): + _LOGGER.info(f"NBE Switch {self.name}: Turning ON") + await self._send_command(self.write_key_on, "1") + + async def async_turn_off(self, **kwargs): + _LOGGER.info(f"NBE Switch {self.name}: Turning OFF") + val_to_send = "0" if self._is_pump else "1" + await self._send_command(self.write_key_off, val_to_send) + + async def _send_command(self, key, value): + proxy = self.coordinator.proxy + if proxy: + try: + await self.coordinator.hass.async_add_executor_job(proxy.set, key, value) + except (OSError, IOError) as e: + _LOGGER.warning(f"NBEConnect: Command sent but response decode failed (expected). Error: {e}") + + await self.coordinator.async_request_refresh() + + @property + def device_info(self): + return self._dev_info