scottyphillips/echonetlite_homeassistant

Struggling to add extra input_number for setmap exposed config

Closed this issue · 27 comments

My gas remote controller has floor heating and according to pychonet exposes the temperature under 0xE1

{'class': ('0x7b', 'Floor heater'),
  'getmap': [('0x80', 'Operation status'),
             ('0x90', 'ON timer reservation setting'),
             ('0x81', 'Installation location'),
             ('0x91', 'Time set by ON timer'),
             ('0xd1', 'Unknown'),
             ('0xe1', 'Temperature setting 2'),
             ('0xf1', 'Unknown'),
             ('0x82', 'Standard version information'),
             ('0xe2', 'Measured room temperature'),
             ('0xf2', 'Unknown'),
             ('0x83', 'Identification number'),
             ('0x93', 'Remote control setting'),
             ('0xf3', 'Unknown'),
             ('0x94', 'OFF timer reservation setting'),
             ('0x95', 'Time set by OFF timer'),
             ('0xf5', 'Unknown'),
             ('0x86', 'Manufacturers fault code'),
             ('0xe6', 'Daily timer setting'),
             ('0xf6', 'Unknown'),
             ('0xe7', 'Daily timer setting 1'),
             ('0xf7', 'Unknown'),
             ('0x88', 'Fault status'),
             ('0xe8', 'Daily timer setting 2'),
             ('0x89', 'Fault description'),
             ('0xf9', 'Unknown'),
             ('0x8a', 'Manufacturer code'),
             ('0xfa', 'Unknown'),
             ('0x8c', 'Product code'),
             ('0xfc', 'Unknown'),
             ('0x8d', 'Production number'),
             ('0x9d', 'Status change announcement property map'),
             ('0xfd', 'Unknown'),
             ('0x9e', 'Set property map'),
             ('0xfe', 'Unknown'),
             ('0x9f', 'Get property map')],
  'group': ('0x2', 'Housing/facility-related device group'),
  'host': 'REDACTED',
  'instance': '0x2',
  'manufacturer': 'Rinnai',
  'setmap': [('0x80', 'Operation status'),
             ('0x90', 'ON timer reservation setting'),
             ('0x81', 'Installation location'),
             ('0x91', 'Time set by ON timer'),
             ('0xe1', 'Temperature setting 2'),
             ('0xf2', 'Unknown'),
             ('0x93', 'Remote control setting'),
             ('0xf3', 'Unknown'),
             ('0x94', 'OFF timer reservation setting'),
             ('0x95', 'Time set by OFF timer'),
             ('0xf5', 'Unknown'),
             ('0xe6', 'Daily timer setting'),
             ('0xf6', 'Unknown'),
             ('0xe7', 'Daily timer setting 1'),
             ('0xf7', 'Unknown'),
             ('0xe8', 'Daily timer setting 2'),
             ('0xf9', 'Unknown'),
             ('0xfb', 'Unknown')],
  'uid': 'REDACTED'},

However by default the only thing that shows in controls is the Operation status. I've tried adding it to const.py:

       0x7B: {
            0xE1: {
                CONF_ICON: "mdi:thermometer",
                CONF_TYPE: SensorDeviceClass.TEMPERATURE,
                CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
                CONF_SERVICE: [SERVICE_SET_INT_1B],
                CONF_UNIT_OF_MEASUREMENT: "",
            },
        },

and removing the lines that seem to filter out adding entities when operation status is available:

                # removed these lines
                if (power_switch and ENL_STATUS == op_code) or (
                    mode_select and ENL_OPENSTATE == op_code
                ):
                    continue

But still no seeming change or any input_number entities added after reloading. I haven't tried completely resetting the plugin as that is a bit if a pain to restore the state. Before I rabbit hole and spend a tonne of time debugging further I was curious if anyone has implemented a change like this before and could point me in the right direction?

The echonetlite integration does not currently provide a number entity. However, since the integration provides a service that accepts user input, it can be configured from the UI by creating an input_number helper and making a service call. Please refer to the following documents for details.

I think you can do it just by adding the service definition to const.py. If it works, please send us a PR! Good luck!

And... I'm checking the documentation for Number Entity. I'm considering adding this to this integration.

The echonetlite integration does not currently provide a number entity. However, since the integration provides a service that accepts user input, it can be configured from the UI by creating an input_number helper and making a service call. Please refer to the following documents for details.

I think you can do it just by adding the service definition to const.py. If it works, please send us a PR! Good luck!

Hi @nao-pon thank you for the reply! I am happy to submit a PR of course again if I can get this to work. Unfortunately with what I tried yesterday I didn't see any input_number entities added. It's strange because even in the logs and the .storage/ state it shows the 0xe1 property in the setmap for the device. Is there anything else I need I to do get it to start appearing? Do I need to delete and re-add or something?

And... I'm checking the documentation for Number Entity. I'm considering adding this to this integration.

I feel unfortunately it doesn't fit with the current structure of state in this library, but it would be most flexible if we could define with YAML config those kind of integrations and map them to Home Assistant entities by the getter/setter IDs rather than having the service definition hardcoded in the repository...

I would set it up as a climate entity personally… just a simplistic one with temperature controls and with only “off” and “heat” as the options… that would give you control over 0xE1 using the climate slider, it would look nicer in my opinion…

I would set it up as a climate entity personally… just a simplistic one with temperature controls and with only “off” and “heat” as the options… that would give you control over 0xE1 using the climate slider, it would look nicer in my opinion…

I'm also considering that using hass-template-climate (just a bit unsure if it's possible to have the temperature not be a celsius unit of measurement as the temperature is a 1 to 9 level rather than a target temp), but right now struggling to get the integration to expose control of the 0xE1 integer itself

First of all, I tried implementing a time setting that can be tested in my environment. This is working fine. I plan to implement numerical settings as well. We'll let you know when it's ready, so please help us test it.

For temperature settings, I think you can use that service and create a climate entity helper.

  • time.py
import logging
import datetime
from datetime import time
from homeassistant.components.time import TimeEntity
from .const import (
    DOMAIN,
    CONF_FORCE_POLLING,
    ENL_OP_CODES,
    CONF_ICON,
    TYPE_TIME,
)
from pychonet.lib.epc import EPC_CODE
from pychonet.lib.eojx import EOJX_CLASS

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass, config, async_add_entities, discovery_info=None):
    entities = []
    for entity in hass.data[DOMAIN][config.entry_id]:
        eojgc = entity["instance"]["eojgc"]
        eojcc = entity["instance"]["eojcc"]
        # configure select entities by looking up full ENL_OP_CODE dict
        for op_code in entity["instance"]["setmap"]:
            if eojgc in ENL_OP_CODES.keys():
                if eojcc in ENL_OP_CODES[eojgc].keys():
                    if op_code in ENL_OP_CODES[eojgc][eojcc].keys():
                        if TYPE_TIME in ENL_OP_CODES[eojgc][eojcc][op_code].keys():
                            entities.append(
                                EchonetTime(
                                    hass,
                                    entity["echonetlite"],
                                    config,
                                    op_code,
                                    ENL_OP_CODES[eojgc][eojcc][op_code],
                                    entity["echonetlite"]._name or config.title,
                                )
                            )

    async_add_entities(entities, True)


class EchonetTime(TimeEntity):
    _attr_translation_key = DOMAIN

    def __init__(self, hass, connector, config, code, options, name=None):
        """Initialize the time."""
        self._connector = connector
        self._config = config
        self._code = code
        self._server_state = self._connector._api._state[
            self._connector._instance._host
        ]
        self._attr_icon = options.get(CONF_ICON, None)
        self._attr_name = f"{config.title} {EPC_CODE[self._connector._eojgc][self._connector._eojcc][self._code]}"
        self._attr_unique_id = (
            f"{self._connector._uidi}-{self._code}"
            if self._connector._uidi
            else f"{self._connector._uid}-{self._code}"
        )
        self._device_name = name
        self._attr_should_poll = True
        self._attr_available = True
        self._attr_native_value = self.get_time()

        self.update_option_listener()

    @property
    def device_info(self):
        return {
            "identifiers": {
                (
                    DOMAIN,
                    self._connector._uid,
                    self._connector._instance._eojgc,
                    self._connector._instance._eojcc,
                    self._connector._instance._eojci,
                )
            },
            "name": self._device_name,
            "manufacturer": self._connector._manufacturer,
            "model": EOJX_CLASS[self._connector._instance._eojgc][
                self._connector._instance._eojcc
            ]
            # "sw_version": "",
        }

    def get_time(self):
        hh_mm = self._connector._update_data.get(self._code)
        if hh_mm != None:
            val = hh_mm.split(":")
            time_obj = datetime.time(int(val[0]), int(val[1]))
        else:
            time_obj = None
        return time_obj

    async def async_set_value(self, value: time) -> None:
        """Update the current value."""
        h = int(value.hour)
        m = int(value.minute)
        mes = {"EPC": self._code, "PDC": 0x02, "EDT": h * 256 + m}
        if await self._connector._instance.setMessages([mes]):
            pass
        else:
            raise InvalidStateError(
                "The state setting is not supported or is an invalid value."
            )

    async def async_update(self):
        """Retrieve latest state."""
        try:
            await self._connector.async_update()
        except TimeoutError:
            pass

    async def async_added_to_hass(self):
        """Register callbacks."""
        self._connector.add_update_option_listener(self.update_option_listener)
        self._connector.register_async_update_callbacks(self.async_update_callback)

    async def async_update_callback(self, isPush=False):
        new_val = self.get_time()
        changed = (
            self._attr_native_value != new_val
            or self._attr_available != self._server_state["available"]
        )
        if changed:
            self._attr_native_value = new_val
            self._attr_available = self._server_state["available"]
            self.async_schedule_update_ha_state()

    def update_option_listener(self):
        self._attr_should_poll = (
            self._connector._user_options.get(CONF_FORCE_POLLING, False)
            or self._code not in self._connector._ntfPropertyMap
        )
        _LOGGER.info(
            f"{self._device_name}({self._code}): _should_poll is {self._attr_should_poll}"
        )

First of all, I tried implementing a time setting that can be tested in my environment. This is working fine. I plan to implement numerical settings as well. We'll let you know when it's ready, so please help us test it.

For temperature settings, I think you can use that service and create a climate entity helper.

...

Thank you so much! I'll try seeing if I can get it working based on this, but if you're making a number setter anyway it might be better to use that.

It's still a draft, but I can now test it on my edge branch. You can install it by following the steps below.
Before testing, please do a full backup so that you can restore it.

Memo: PR planned branch

It's still a draft, but I can now test it on my edge branch. You can install it by following the steps below.

Thank you so much! I had to slightly tweak your code (and fixed a typo I spotted) to get it running locally but now the number entities show up. It looks like the scale and min/max values aren't correct but I can figure this out from here. I really appreciate how quickly you did this @nao-pon !!!!

Diff of my changes if you want to incorporate them:

diff --git a/custom_components/echonetlite/const.py b/custom_components/echonetlite/const.py
index e18c0b6..1ccd57c 100644
--- a/custom_components/echonetlite/const.py
+++ b/custom_components/echonetlite/const.py
@@ -376,6 +376,11 @@ ENL_OP_CODES = {
                         CONF_NAME: "Auto",
                         CONF_ON_VALUE: 0x41,
                         CONF_OFF_VALUE: 0x31,
+                        CONF_SERVICE_DATA: {
+                            CONF_NAME: "Auto",
+                            DATA_STATE_ON: 0x41,
+                            DATA_STATE_OFF: 0x31,
+                        },
                     },
                 },
             },
diff --git a/custom_components/echonetlite/number.py b/custom_components/echonetlite/number.py
index 9840faa..f01030d 100644
--- a/custom_components/echonetlite/number.py
+++ b/custom_components/echonetlite/number.py
@@ -48,7 +48,7 @@ class EchonetNumber(NumberEntity):
     _attr_translation_key = DOMAIN
 
     def __init__(self, hass, connector, config, code, options, name=None):
-        """Initialize the time."""
+        """Initialize the number."""
         self._connector = connector
         self._config = config
         self._code = code
@@ -98,15 +98,20 @@ class EchonetNumber(NumberEntity):
     def get_value(self):
         value = self._connector._update_data.get(self._code)
         if value != None:
-            return float(self._connector._update_data.get(self._code) - self._as_zero)
+            if type(value) == str and value.isnumeric():
+                value = float(value)
+            return float(value - self._as_zero)
         else:
             return None
 
     def get_max_value(self):
         if self._options.get(CONF_MAX_OPC):
-            return self._connector._update_data.get(CONF_MAX_OPC)
-        else:
-            return self._options.get(CONF_MAXIMUM)
+            max_opc_value = self._connector._update_data.get(CONF_MAX_OPC)
+            if max_opc_value != None:
+                if type(max_opc_value) == str and max_opc_value.isnumeric():
+                    max_opc_value = float(max_opc_value)
+                return max_opc_value
+        return self._options.get(CONF_MAXIMUM)
 
     async def async_set_native_value(self, value: float) -> None:
         """Update the current value."""

@rubyroobs Thank you for the verification! It seems that the minimum/maximum value settings are incorrect. I will correct it and let you know. 😄

@rubyroobs I made the corrections and maintenance. If all goes well, you will also be able to set the On/Off timer. 👍

Please continue testing on my edge branch.

Hi @nao-pon, thank you so much again! They do show up but the scales are still wrong - I'm honestly not sure what's going on here from your code as it appears correct (I thought the scale was 0x31 to 0x39 - maybe 31 was decimal and it's actually 0x1F to 0x27? I'll play around tomorrow). Also I got a python error where it's trying to pass a hex number as base 10 and causing an error on the set value. I'll try to look more in the morning :)

Screenshot 2024-01-27 at 1 29 48

(small update - this gets the scale correct now but still getting errors on setting, now another error about invalidstateerror not being imported

 0xE1: {
                CONF_ICON: "mdi:thermometer",
                CONF_TYPE: None,
                CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
                CONF_UNIT_OF_MEASUREMENT: "",
                TYPE_NUMBER: {
                    CONF_AS_ZERO: 0x1E,
                    CONF_MINIMUM: 0x1F,
                    CONF_MAXIMUM: 0x27,
                },
            },
            

)

Is the device implemented differently than the specifications? Investigation is required. . .
240127-100912

Ah, understood. pychonet maintenance is required. You need device definition for 0x02-0x7B. I'll try making it.

It's a draft, but it's done. Updating with my edge version applies the new pychonet. Please try it

@nao-pon this seems to work perfect! This controller only goes up to 9 though but I think I can modify that myself locally as it's out of specification - everything else works fine. Thank you so much!!!!!!!!

As a heads up, changing to your branch also seemed to recreate all of my Panasonic リンクプラス lights again (I'd given them custom IDs like lights.ldk and now they have been recreated by their name exposed in the Panasonic app i.e. "リビングダウンライト" to light.ribingudaunraito). I don't mind recreating them but might want to mark the update if it's a breaking change.

Congratulations!

I see, changing the entity ID will affect things like automation, so I'll check to see if there's a way to keep it the same.

There was a way that the integration could change the default entity name with keeping the existing entity id, so I apply that to each component.

I did notice when I ran master version it created an entire new device and I had to manually edit out the old device in core.device_registry

@scottyphillips I am changing a lot of lines in pychonet and echonetlite to accommodate input entities. Is it possible for you to try my edge version?

What type of device is the newly configured device? Is it for all devices?

It doesn't seem to be recreated as a separate entity, at least to the extent of my testing. The edge version is a merge of two PRs:

Hi @nao-pon - I updated your edge version today and it resulted in all the entities being recreated again 😅 is there a way you know of hacking the config files or similar to merge them so I don’t have to manually rename all the IDs?

I think the following local files are affected.

  • .storage
    • core.config_entries
    • core.entity_registry
    • core.device_registry

However, I am not familiar with this process, so I cannot give you accurate advice.

Meanwhile, I updated my production environment from echonetlite 3.7.9 to edge 5853e34.

There were no other issues other than the sensor entity that switched to time input being disabled.
The devices are as follows.

  • Low voltage smart electric energy meter by Mitsubishi Electric
  • Display by Mitsubishi Electric
  • Home air conditioner by Panasonic
  • Hot water generator by Rinnai
  • Gas meter by Rinnai
  • Water flow meter by Rinnai

I can actually check only a limited number of devices, so there may be a mistake somewhere. What type of device is the problem occurring?

I think it was you adding this and then uncommenting it 😅 nao-pon@e8f5382#diff-bbc37a7b50cc55632638f8ded8d846051ac1c2b0a2c99360601d3244ea8a1b9dR202

Uncommenting that for me got everything working again for the interim - once everything is merged to the main plugin I will reconcile the IDs manually :)

In that respect, I think that only the Panasonic WTY2001 Advanced Series Link Plus Wireless Adapter is affected. Is that correct?

This Panasonic WTY2001 0xFE data is device-specific and undocumented, so I'm not sure how to handle it.

@xen2 wrote the code to set it instead of uidi, but since the instance number is undefined, it may have been done that way, but I don't know. If the instance number does not change, the original uidi is fine, so I leave it disabled.

rel. #117 (comment)

This is probably fixed in version 3.8.0, so I will close it. 👍