Voluptuous .Coerce for int and float values

I’m trying to modify the ADS component to be able to read and write PLC real values.
The reading of PLC values is working perfect, the writing to PLC poses a problem.
The write_to_ADS service only accepts integer values due to this line of code:

vol.Required(CONF_ADS_VALUE): vol.Coerce(int)

Now I want to modify this line so it also accepts float values.
If I currently provide a float value to the service, it typecasts this to an integer and the decimal digits are dropped.
How do I modify the following schema, so it also accepts float and int for the CONF_ADS_VALUE?

SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
    {
        vol.Required(CONF_ADS_TYPE): vol.In(
            [
                ADSTYPE_INT,
                ADSTYPE_UINT,
                ADSTYPE_BYTE,
                ADSTYPE_BOOL,
                ADSTYPE_DINT,
                ADSTYPE_UDINT,
                ADSTYPE_REAL,
            ]
        ),
        vol.Required(CONF_ADS_VALUE): vol.Coerce(int),
        vol.Required(CONF_ADS_VAR): cv.string,
    }
)

I would try:

vol.Required(CONF_ADS_VALUE): vol.Any(vol.Coerce(float), vol.Coerce(int))

Just change it to:

        vol.Required(CONF_ADS_VALUE): vol.Coerce(float),

This will accept floats, ints, and strings that represent floats or ints, and will convert them all to a float. So, e.g., 1 will be converted to 1.0.

If, however, you want it to return an int if an int, or a string representing an int, is given, and a float only if a float, or a string representing a float, is given, then you could try:

        vol.Required(CONF_ADS_VALUE): vol.Any(vol.Coerce(int), vol.Coerce(float)),

This is similar to what @exxamalte suggested, but the order is important.

1 Like

There still seems to be an issue.
When I use:

vol.Required(CONF_ADS_VALUE): vol.Any(vol.Coerce(int), vol.Coerce(float)),

everything is converted into integer values. Even float, gets it’s decimal places truncated.
When I reverse the order into:

vol.Required(CONF_ADS_VALUE): vol.Any(vol.Coerce(float), vol.Coerce(int))

the float value is passed correctly, but the service errors on the integer values.
It seems that pyADS doesn’t like float values when it’s expecting an integer.
That’s why the following also doesn’t work:

vol.Required(CONF_ADS_VALUE): vol.Coerce(float),

So I need to make the schema dependent on the value that is passed via

CONF_ADS_TYPE

so it uses

vol.Coerce(float)

when the value is REAL
and uses

vol.Coerce(int)

for all other values (int, udint, byte, bool, dint).

After browsing the ads & pyads code a bit, it seems you need to do a bit more. I didn’t try to understand everything, and I certainly can’t test anything, but it seems like maybe you need:

# Supported Types
ADSTYPE_BOOL = "bool"
ADSTYPE_BYTE = "byte"
ADSTYPE_DINT = "dint"
ADSTYPE_INT = "int"
ADSTYPE_UDINT = "udint"
ADSTYPE_UINT = "uint"
ADSTYPE_REAL = "real" # ADD THIS

ADS_TYPEMAP = {
    ADSTYPE_BOOL: pyads.PLCTYPE_BOOL,
    ADSTYPE_BYTE: pyads.PLCTYPE_BYTE,
    ADSTYPE_DINT: pyads.PLCTYPE_DINT,
    ADSTYPE_INT: pyads.PLCTYPE_INT,
    ADSTYPE_UDINT: pyads.PLCTYPE_UDINT,
    ADSTYPE_UINT: pyads.PLCTYPE_UINT,
    ADSTYPE_REAL: pyads.PLCTYPE_REAL, # ADD THIS
}
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
    {
        vol.Required(CONF_ADS_TYPE): vol.In(
            [
                ADSTYPE_INT,
                ADSTYPE_UINT,
                ADSTYPE_BYTE,
                ADSTYPE_BOOL,
                ADSTYPE_DINT,
                ADSTYPE_UDINT,
                ADSTYPE_REAL, # ADD THIS
            ]
        ),
        vol.Required(CONF_ADS_VALUE): vol.Coerce(float), # CHANGE THIS
        vol.Required(CONF_ADS_VAR): cv.string,
    }
)
    def handle_write_data_by_name(call: ServiceCall) -> None:
        """Write a value to the connected ADS device."""
        ads_var = call.data[CONF_ADS_VAR]
        ads_type = call.data[CONF_ADS_TYPE]
        value = call.data[CONF_ADS_VALUE]
        if ads_type != ADSTYPE_REAL: # NOT SURE IF THIS IF STMT IS NECESSARY
            value = int(value)

        try:
            ads.write_by_name(ads_var, value, ADS_TYPEMAP[ads_type])
        except pyads.ADSError as err:
            _LOGGER.error(err)

Hello Phil

I’ve already added the lines to support the REAL values, for the reading of ADS variables.
But the following did the trick:

vol.Required(CONF_ADS_VALUE): vol.Coerce(float), # CHANGE THIS

and

        if ads_type != ADSTYPE_REAL:
            value = int(value)

Tnx!

1 Like