|
| 1 | +local utils = require "utils" |
| 2 | +local Fields = require "fields" |
| 3 | +local command_handlers = require "handlers.grouped_light_commands" |
| 4 | +local capabilities = require "st.capabilities" |
| 5 | +local KVCounter = require "utils.kv_counter" |
| 6 | + |
| 7 | +--- Alias for clearer documentation. |
| 8 | +---@alias CommandName string |
| 9 | +--- Alias for clearer documentation. |
| 10 | +---@alias DeviceUuid string |
| 11 | + |
| 12 | +--- Capability command before being normalized via st.capabilities |
| 13 | +---@class RawCapCommand |
| 14 | +---@field public capability string |
| 15 | +---@field public component string |
| 16 | +---@field public command CommandName |
| 17 | +---@field public args any[] |
| 18 | +---@field public named_args table? |
| 19 | + |
| 20 | +--- Serialized batched command from receive_batch. |
| 21 | +--- This is already in a table format unlike the normal receive function. |
| 22 | +---@class BatchedCommand |
| 23 | +---@field public device_uuid DeviceUuid |
| 24 | +---@field public capability_command RawCapCommand |
| 25 | +---@field public auxilary_command_data table? |
| 26 | + |
| 27 | +--- Table where all commands match by args and any additional data needed for the command. |
| 28 | +---@alias MatchingCommands table<DeviceUuid, BatchedCommand> |
| 29 | + |
| 30 | +--- Table sorted by the command name. |
| 31 | +---@alias SortedCommandNames table<CommandName, MatchingCommands[]> |
| 32 | + |
| 33 | +--- Sorted command batched. First sorted by the hue bridge. |
| 34 | +---@alias SortedCommandBatch table<HueBridgeDevice, SortedCommandNames> |
| 35 | + |
| 36 | +--- A matching group that covers some portion of a command batch. |
| 37 | +---@alias MatchingGroups table<HueLightGroup, BatchedCommand> |
| 38 | + |
| 39 | + |
| 40 | +local batched_command_utils = {} |
| 41 | + |
| 42 | +local switch_on_handler = command_handlers.switch_on_handler |
| 43 | +local switch_off_handler = command_handlers.switch_off_handler |
| 44 | +local switch_level_handler = command_handlers.switch_level_handler |
| 45 | +local set_color_handler = command_handlers.set_color_handler |
| 46 | +local set_hue_handler = command_handlers.set_hue_handler |
| 47 | +local set_saturation_handler = command_handlers.set_saturation_handler |
| 48 | +local set_color_temp_handler = command_handlers.set_color_temp_handler |
| 49 | + |
| 50 | +local capability_handlers = { |
| 51 | + [capabilities.switch.ID] = { |
| 52 | + [capabilities.switch.commands.on.NAME] = switch_on_handler, |
| 53 | + [capabilities.switch.commands.off.NAME] = switch_off_handler, |
| 54 | + }, |
| 55 | + [capabilities.switchLevel.ID] = { |
| 56 | + [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_handler, |
| 57 | + }, |
| 58 | + [capabilities.colorControl.ID] = { |
| 59 | + [capabilities.colorControl.commands.setColor.NAME] = set_color_handler, |
| 60 | + [capabilities.colorControl.commands.setHue.NAME] = set_hue_handler, |
| 61 | + [capabilities.colorControl.commands.setSaturation.NAME] = set_saturation_handler, |
| 62 | + }, |
| 63 | + [capabilities.colorTemperature.ID] = { |
| 64 | + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temp_handler, |
| 65 | + }, |
| 66 | +} |
| 67 | + |
| 68 | +-- Mapping for fields on the device that must also match for the commands to be handled in a group. |
| 69 | +local command_name_to_aux_fields = { |
| 70 | + [capabilities.colorControl.commands.setColor.NAME] = { |
| 71 | + Fields.GAMUT |
| 72 | + }, |
| 73 | + [capabilities.colorControl.commands.setHue.NAME] = { |
| 74 | + Fields.GAMUT, |
| 75 | + Fields.COLOR_SATURATION, |
| 76 | + }, |
| 77 | + [capabilities.colorControl.commands.setSaturation.NAME] = { |
| 78 | + Fields.GAMUT, |
| 79 | + Fields.COLOR_HUE, |
| 80 | + }, |
| 81 | + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = { |
| 82 | + Fields.MIN_KELVIN, |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +---@param cmd BatchedCommand |
| 87 | +---@return function? |
| 88 | +function batched_command_utils.get_handler(cmd) |
| 89 | + local capability = cmd.capability_command.capability |
| 90 | + local command = cmd.capability_command.command |
| 91 | + return capability_handlers[capability] and capability_handlers[capability][command] |
| 92 | +end |
| 93 | + |
| 94 | +--- Sort the table by bridge, command, and matching args + any auxilary data needed for the command. |
| 95 | +--- |
| 96 | +--- The sorted batch is first sorted in to a table with the bridge device as a key and an inner |
| 97 | +--- table as the value. |
| 98 | +--- |
| 99 | +--- The inner table is sorted by command name as the key and an inner array as the value. |
| 100 | +--- |
| 101 | +--- The inner array contains tables with the device id as the key and the BatchedCommand as the |
| 102 | +--- value where all of the BatchedCommands have matching arguments and auxilary data. |
| 103 | +--- |
| 104 | +--- See SortedCommandBatch. |
| 105 | +--- |
| 106 | +---@param driver HueDriver |
| 107 | +---@param batch BatchedCommand[] |
| 108 | +---@return SortedCommandBatch |
| 109 | +---@return BatchedCommand[] misfits Commands that cannot be attempted to handle in a batch |
| 110 | +function batched_command_utils.sort_batch(driver, batch) |
| 111 | + local sorted_batch = KVCounter() |
| 112 | + local misfits = {} |
| 113 | + |
| 114 | + for _, to_inspect in ipairs(batch) do |
| 115 | + -- Check if we can handle this in a batch |
| 116 | + if not batched_command_utils.get_handler(to_inspect) then |
| 117 | + misfits.insert(to_inspect) |
| 118 | + goto continue |
| 119 | + end |
| 120 | + |
| 121 | + -- First key off bridge. |
| 122 | + local device = driver:get_device_info(to_inspect.device_uuid) |
| 123 | + if not device then |
| 124 | + misfits.insert(to_inspect) |
| 125 | + goto continue |
| 126 | + end |
| 127 | + local parent_id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) |
| 128 | + local bridge_device = utils.get_hue_bridge_for_device(driver, device, parent_id, true) |
| 129 | + if not bridge_device then |
| 130 | + misfits.insert(to_inspect) |
| 131 | + goto continue |
| 132 | + end |
| 133 | + sorted_batch[bridge_device] = sorted_batch[bridge_device] or KVCounter() |
| 134 | + local by_bridge = sorted_batch[bridge_device] |
| 135 | + |
| 136 | + -- Next, key off the command name. |
| 137 | + -- Commands are unique across the capabilities supported here so avoid nesting another table |
| 138 | + -- with capability name. |
| 139 | + local command_name = to_inspect.capability_command.command |
| 140 | + by_bridge[command_name] = by_bridge[command_name] or KVCounter() |
| 141 | + local by_command = by_bridge[command_name] |
| 142 | + |
| 143 | + -- Add extra data that must match for the commands to be handled in a group |
| 144 | + to_inspect.auxilary_command_data = {} |
| 145 | + for _, field in ipairs(command_name_to_aux_fields[command_name] or {}) do |
| 146 | + to_inspect.auxilary_command_data[field] = device:get_field(field) |
| 147 | + end |
| 148 | + |
| 149 | + -- Initialize the index to the last position. |
| 150 | + -- This will be updated if a matching command group is found. |
| 151 | + local index = #by_command + 1 |
| 152 | + -- Finally, group commands with matching bridge and command name by matching arguments |
| 153 | + -- and auxilary command data. |
| 154 | + for match_idx, matching_table in ipairs(by_command) do |
| 155 | + -- Grab first command, all the arguments in this table are the same so it doesn't matter. |
| 156 | + local _, to_match = next(matching_table, nil) |
| 157 | + if to_match == nil then |
| 158 | + -- This shouldn't be possible, but just put command in the last index if it does happen |
| 159 | + break |
| 160 | + end |
| 161 | + |
| 162 | + if utils.deep_table_eq(to_match.capability_command.args, to_inspect.capability_command.args) and |
| 163 | + utils.deep_table_eq(to_match.auxilary_command_data, to_inspect.auxilary_command_data) then |
| 164 | + -- These commands match |
| 165 | + index = match_idx |
| 166 | + break |
| 167 | + end |
| 168 | + end |
| 169 | + if not by_command[index] then |
| 170 | + table.insert(by_command, index, KVCounter()) |
| 171 | + end |
| 172 | + by_command[index][device.id] = to_inspect |
| 173 | + |
| 174 | + ::continue:: |
| 175 | + end |
| 176 | + return sorted_batch, misfits |
| 177 | +end |
| 178 | + |
| 179 | +--- Find groups that the matching commands can use. |
| 180 | +--- Larger groups are prefered and overlap is not allowed. |
| 181 | +--- Removes commands from the provided matching commands as they are handled by matching groups. |
| 182 | +---@param bridge_device HueBridgeDevice |
| 183 | +---@param commands MatchingCommands |
| 184 | +---@return MatchingGroups |
| 185 | +function batched_command_utils.find_matching_groups(bridge_device, commands) |
| 186 | + local groups = bridge_device:get_field(Fields.GROUPS) or {} |
| 187 | + local matching_groups = {} |
| 188 | + -- Groups are sortered from most to least children |
| 189 | + for _, group in ipairs(groups) do |
| 190 | + if #group.devices == 0 or #group.devices > #commands then |
| 191 | + -- Can't match if the group has no light children or if it has more light children |
| 192 | + -- than is in the command |
| 193 | + goto continue |
| 194 | + end |
| 195 | + for _, device in ipairs(group.devices) do |
| 196 | + if not commands[device.id] then |
| 197 | + -- The commands didn't contain one of the light children for this group |
| 198 | + goto continue |
| 199 | + end |
| 200 | + end |
| 201 | + -- If we get here then we have a match. Save one of the commands for the handler |
| 202 | + matching_groups[group] = commands[group.devices[1].id] |
| 203 | + for _, device in ipairs(group.devices) do |
| 204 | + -- clear out commands handled by the group |
| 205 | + commands[device.id] = nil |
| 206 | + end |
| 207 | + if #commands == 0 then |
| 208 | + -- Nothing else to handle |
| 209 | + break |
| 210 | + end |
| 211 | + -- TODO: Break after a single group match? |
| 212 | + ::continue:: |
| 213 | + end |
| 214 | + return matching_groups |
| 215 | +end |
| 216 | + |
| 217 | +return batched_command_utils |
| 218 | + |
| 219 | + |
0 commit comments