Improve unit of measurement conversion in the frontend#30264
Improve unit of measurement conversion in the frontend#30264TCWORLD wants to merge 18 commits intohome-assistant:devfrom
Conversation
35be868 to
dd2fd84
Compare
73155ea to
0f9c04c
Compare
|
I am generally in favor of this because it is all eternal constants. The ratios will never change so I don't see a point in calling core every time. We could make this a dynamically loaded module so it doesn't bloat the main bundle. |
|
I guess the only point of calling to core is that they are frequently updating with new supported units. If we start to rely on unit conversions we will likely repeatedly find ourselves missing new units as they are added to core only. Just based on looking at core it seems like there's maybe only two unit conversions currently that use non-linear scaling function:
|
|
This may also be somewhat useful/related for the requested "UoM Selector" |
|
We can extract the conversion factors to a shared JSON file. Something like {
"temperature": {
"base": "K",
"units": {
"°C": {"scale": 1, "offset": -273.15},
"°F": {"scale": 1.8, "offset": -459.67},
"K": {"scale": 1, "offset": 0}
}
}
} |
|
I guess Beaufort uses an exponential function as well, so that may be complex to extract to json, but I'm sure it's something that can be worked around for a small handful of cases. |
|
This is kind of like option 3 but using a shared file rather than API query (which makes more sense). When I was looking at retreiving the operations from the core, there are basically four operations needed:
All of the unit conversions thus far can be acheived with those four, including Beaufort and the unit inverses (which can be acheived by raising to power of -1). (For reference: TCWORLD/core@6615c5a) I suppose the question is whether there is a clean way to import a operations file like that and convert it into executable code. I'm sure it's doable, but I stopped short of that in my API query attempt. |
|
The frontend team consensus is that a shared JSON file with core would be best. Now we have to convince the core team but @frenck seems to be on board. Given that there are various operations and the order might matter, I'll update my example to {
"temperature": {
"base": "K",
"units": {
"°C": [{"scale": 1, "offset": -273.15}],
"°F": [{"scale": 1.8, "offset": -459.67}, {"round": 0}]
}
}
} |
|
I'm happy to have a go making frontend/core code for parsing unit conversions from JSON if there is a consensus from both sides. Would we still want the proposed enumerations file, or generate essentially constant arrays of unit strings from the JSON file directly? The former would be easier to strictly type functions using specific sets of units. For the JSON example, I guess it might be easier to parse if we have an array of operations, but keep each operation as a single key-value pair (can also drop redundant ones). That way the parser could be a simple map of operation to function handle, calling each in turn. So: Converting from-to different units would be performing the "from" operations in reverse (with offsets negated, and the factors for scale/powers inverted), followed by the "to" operations. From the above, doing *F to *C would be performed as operations:
|
Currently there is no complete way of converting values between different units within the frontend. There are some partial implementations for specific purposes (primarily for energy cards). If we want to use state values from sensors for use in conjunction with statistics we need a way to convert the values, ideally without having to issue a call into the core. This is essentially a copy of the `util/unit_conversion.py` from core, converted to typescript for use in the frontend. Add convertTo/From Base Unit
Now we have unit conversion functionality, try to convert the state/now value if possible to the set unit/unitClass. Conversion will only be attempted if the unit is explicitly set. If not set, we don't know whether the statistics data itself is in the correct unit. Make sure we also perform unit lookup each time the data is generated in case the entities are changed in the mean time.
This catches a number of false-positive cases that the original simple length check would miss. Additional test cases have been added.
This removes all the hardcoded unit strings int he recorder.ts, as well as the VOLUME_UNITS lookup.
Rather than the FLOW_RATE_TO_LMIN lookup table, do the conversion using the new unit converters.
And convert valid units to string[] rather than Set<string> for better serialisation.
0f9c04c to
312eb5e
Compare
|
As an initial step, the following is all of the current units of measurement for each unit class, along with the conversion factors to the base unit for each class. It's not formatted yet in the operations we'd want to perform - literally nothing more than a JSON serialisation of the unit converter array from - but it gives rough idea of the fixed constants for conversion needed for most of the units, along with the special cases that have converter functions. |
|
Might as well go the final step in creating the JSON file. This is the full set of unit conversions and operations in the JSON format described above: I decided to leave any conversion which is just a simple scale factor as a bare number. It should be easy for an interpreter to select between simple scale factor (values read as a number), and set of operations (values read as tuple array), and perform the operation accordingly. I'll look at writing an interpreter in due course. For now I'm going to mark this PR as a draft again. |
|
We probably have to create an Architecture discussion for this in order to get it in core |
Move it after the map to avoid mutating the unit conversion source array.
|
I've decided to have a go and create the frontend implementation for the JSON approach anyway. Even if not pushed back to core, it seems to be a much cleaner approach here. One class instead of 20+ and no need for special override functions and inverse checks. Will probably need some stress testing, and documentation adding, but initial testing seems to work for both temperature and power cases, for both to and from units (F->C and C->F). |
Its a quick and dirty hack constant, which should probably be reworked out, but for now stick it back in energy.ts as all the other conversion constants are now in JSON.
Check that it contains at least something sensible with expected fields, removing any entries that don't make sense. This avoids importing nonsensical JSON data or incomplete unit classes. It won't stop failures if the JSON file is corrupt (i.e. the static import fails), but then we don't seem to handle that for localisation strings either.
|
I've been looking over the core code to see how possible it is to convert over to use the same JSON file, and its... interesting. Core seems to rely heavily on their being default instances of unit converters to provide constants in various places, so trying to load that dynamically from JSON would be rather messy. Instead I'm going to try adapting core to use the same operations as here, and then have a script to export those converters to JSON for front-end. That way if any changes are made to core in the future to add more units, it's a simple case of exporting the JSON from core to update the frontend to match. It might even be possible then to have frontend JSON file be entirely auto-generated by core at run time, but we'll see how that goes. |
Skip redundant scale by 1 if unit is already the base unit.
Use a separate list of inverse units rather than putting inverse power in the conversion operations. This is necessary because the inverse should not be performed when both units are inverses, otherwise a value of 0 gets converted incorrectly.
The format of the JSON file changed slightly with each operation being essentially now a tuple rather than a dictionary. This makes both export and import easier.


Proposed change
Note: I'm very much open to discussion as to whether this feature is wanted and also whether this is the right way to go about it. Figured I'd have a go to see what was possible and prompt ideas.
Basically, there is currently no complete front-end unit conversion utilities, which means rendering state values is very much fixed to whatever the entity unit was set to. Take for example the statistics graph - that displays statistics data but also the current state value of the entities in question. If however the state values are not all the same unit (particularly kW/W or say *F/*C), while the statistics data provided by the core is converted into a single compatible unit, the state values are not and as such cannot easily be plotted. It would be nice to have a way to convert the values.
In the energy cards there is also a case where state values are converted to form totals. In that case a set of lookup tables were previously added which were specific to that one use case and unit set, and thus not more generally useable.
Furthermore, there are a lot of hard-coded unit strings about in various frontend files, but no nice enumerations or constants for those unit strings. Unlike in core where there is a full set of constants for each of the possible units.
I've thought about some ways this could be rectified:
(x - 32) / 1.8to convert *F to *C, so couldn't be returned as a simple scale factor. Would need custom handling.So what I've proposed here, and for initial review/discussion the three files to focus on are:
Add a nice definitive set of unit constants. These can then be used as and when required.
common/unit-conversion/const.tsconst.pyin core, converted to typescript.UnitOfConcentrationis the only exception here, but I'd want to push that change back to the core repo if this goes ahead.Add a set of converter classes to allow converting values between groups of compatible units.
common/unit-conversion/unit-conversion.tsutil/unit_conversion.pyfrom core but as typescript.A helper function for identifying and instantiating the correct converter class for a given unit and/or unit class
common/unit-conversion/unit-conversion.ts.getUnitConverter(unit)orgetUnitConverter(unit, unitClass).isValidUnit()and then callingconvert()if valid.For now I've only test updated the energy and statistics-chart routines to use units/converters as those are the main target of this PR.
There are a large number of other place that use hard-coded units which could benefit from at the very least the unit enums proposed here. I updated those to, but for now I've left them on a different branch to reduce the size of this PR.
Screenshots
Nice example of use case - rendering the statistics graph "now" point. This requires unit conversion if it the entities had different units (e.g. kW and W).
Type of change
Additional information
Checklist
If user exposed functionality or configuration variables are added/changed:
To help with the load of incoming pull requests: