xref: /openbmc/phosphor-fan-presence/control/gen-fan-zone-defs.py (revision 0f2588f2ba0eb545a7a841183368683213a437ac)
1#!/usr/bin/env python3
2
3"""
4This script reads in fan definition and zone definition YAML
5files and generates a set of structures for use by the fan control code.
6"""
7
8import os
9import sys
10import yaml
11from argparse import ArgumentParser
12from mako.lookup import TemplateLookup
13
14
15def parse_cpp_type(typeName):
16    """
17    Take a list of dbus types from YAML and convert it to a recursive cpp
18    formed data structure. Each entry of the original list gets converted
19    into a tuple consisting of the type name and a list with the params
20    for this type,
21        e.g.
22            ['dict', ['string', 'dict', ['string', 'int64']]]
23        is converted to
24            [('dict', [('string', []), ('dict', [('string', []),
25             ('int64', [])]]]
26    """
27
28    if not typeName:
29        return None
30
31    # Type names are _almost_ valid YAML. Insert a , before each [
32    # and then wrap it in a [ ] and it becomes valid YAML (assuming
33    # the user gave us a valid typename).
34    typeArray = yaml.safe_load("[" + ",[".join(typeName.split("[")) + "]")
35    typeTuple = preprocess_yaml_type_array(typeArray).pop(0)
36    return get_cpp_type(typeTuple)
37
38
39def preprocess_yaml_type_array(typeArray):
40    """
41    Flattens an array type into a tuple list that can be used to get the
42    supported cpp type from each element.
43    """
44
45    result = []
46
47    for i in range(len(typeArray)):
48        # Ignore lists because we merge them with the previous element
49        if type(typeArray[i]) is list:
50            continue
51
52        # If there is a next element and it is a list, merge it with the
53        # current element.
54        if i < len(typeArray) - 1 and type(typeArray[i + 1]) is list:
55            result.append(
56                (typeArray[i], preprocess_yaml_type_array(typeArray[i + 1]))
57            )
58        else:
59            result.append((typeArray[i], []))
60
61    return result
62
63
64def get_cpp_type(typeTuple):
65    """
66    Take a list of dbus types and perform validity checking, such as:
67        [ variant [ dict [ int32, int32 ], double ] ]
68    This function then converts the type-list into a C++ type string.
69    """
70
71    propertyMap = {
72        "byte": {"cppName": "uint8_t", "params": 0},
73        "boolean": {"cppName": "bool", "params": 0},
74        "int16": {"cppName": "int16_t", "params": 0},
75        "uint16": {"cppName": "uint16_t", "params": 0},
76        "int32": {"cppName": "int32_t", "params": 0},
77        "uint32": {"cppName": "uint32_t", "params": 0},
78        "int64": {"cppName": "int64_t", "params": 0},
79        "uint64": {"cppName": "uint64_t", "params": 0},
80        "double": {"cppName": "double", "params": 0},
81        "string": {"cppName": "std::string", "params": 0},
82        "array": {"cppName": "std::vector", "params": 1},
83        "dict": {"cppName": "std::map", "params": 2},
84    }
85
86    if len(typeTuple) != 2:
87        raise RuntimeError("Invalid typeTuple %s" % typeTuple)
88
89    first = typeTuple[0]
90    entry = propertyMap[first]
91
92    result = entry["cppName"]
93
94    # Handle 0-entry parameter lists.
95    if entry["params"] == 0:
96        if len(typeTuple[1]) != 0:
97            raise RuntimeError("Invalid typeTuple %s" % typeTuple)
98        else:
99            return result
100
101    # Get the parameter list
102    rest = typeTuple[1]
103
104    # Confirm parameter count matches.
105    if (entry["params"] != -1) and (entry["params"] != len(rest)):
106        raise RuntimeError("Invalid entry count for %s : %s" % (first, rest))
107
108    # Parse each parameter entry, if appropriate, and create C++ template
109    # syntax.
110    result += "<"
111    if entry.get("noparse"):
112        # Do not parse the parameter list, just use the first element
113        # of each tuple and ignore possible parameters
114        result += ", ".join([e[0] for e in rest])
115    else:
116        result += ", ".join([get_cpp_type(e) for e in rest])
117    result += ">"
118
119    return result
120
121
122def convertToMap(listOfDict):
123    """
124    Converts a list of dictionary entries to a std::map initialization list.
125    """
126    listOfDict = listOfDict.replace("'", '"')
127    listOfDict = listOfDict.replace("[", "{")
128    listOfDict = listOfDict.replace("]", "}")
129    listOfDict = listOfDict.replace(":", ",")
130    return listOfDict
131
132
133def genEvent(event):
134    """
135    Generates the source code of an event and returns it as a string
136    """
137    e = "SetSpeedEvent{\n"
138    e += '"' + event["name"] + '",\n'
139    e += "Group{\n"
140    for group in event["groups"]:
141        for member in group["members"]:
142            e += '{"' + member["object"] + '",\n'
143            e += '"' + member["interface"] + '",\n'
144            e += '"' + member["property"] + '"},\n'
145    e += "},\n"
146
147    e += "ActionData{\n"
148    for d in event["action"]:
149        e += "{Group{\n"
150        for g in d["groups"]:
151            for m in g["members"]:
152                e += '{"' + m["object"] + '",\n'
153                e += '"' + m["interface"] + '",\n'
154                e += '"' + m["property"] + '"},\n'
155        e += "},\n"
156        e += "std::vector<Action>{\n"
157        for a in d["actions"]:
158            if len(a["parameters"]) != 0:
159                e += "make_action(action::" + a["name"] + "(\n"
160            else:
161                e += "make_action(action::" + a["name"] + "\n"
162            for i, p in enumerate(a["parameters"]):
163                if (i + 1) != len(a["parameters"]):
164                    e += p + ",\n"
165                else:
166                    e += p + "\n"
167            if len(a["parameters"]) != 0:
168                e += ")),\n"
169            else:
170                e += "),\n"
171        e += "}},\n"
172    e += "},\n"
173
174    e += "std::vector<Trigger>{\n"
175    if ("timer" in event["triggers"]) and (
176        event["triggers"]["timer"] is not None
177    ):
178        e += "\tmake_trigger(trigger::timer(TimerConf{\n"
179        e += "\t" + event["triggers"]["timer"]["interval"] + ",\n"
180        e += "\t" + event["triggers"]["timer"]["type"] + "\n"
181        e += "\t})),\n"
182
183    if ("signals" in event["triggers"]) and (
184        event["triggers"]["signals"] is not None
185    ):
186        for s in event["triggers"]["signals"]:
187            e += "\tmake_trigger(trigger::signal(\n"
188            e += "match::" + s["match"] + "(\n"
189            for i, mp in enumerate(s["mparams"]["params"]):
190                if (i + 1) != len(s["mparams"]["params"]):
191                    e += "\t\t\t" + s["mparams"][mp] + ",\n"
192                else:
193                    e += "\t\t\t" + s["mparams"][mp] + "\n"
194            e += "\t\t),\n"
195            e += "\t\tmake_handler<SignalHandler>(\n"
196            if ("type" in s["sparams"]) and (s["sparams"]["type"] is not None):
197                e += s["signal"] + "<" + s["sparams"]["type"] + ">(\n"
198            else:
199                e += s["signal"] + "(\n"
200            for sp in s["sparams"]["params"]:
201                e += s["sparams"][sp] + ",\n"
202            if ("type" in s["hparams"]) and (s["hparams"]["type"] is not None):
203                e += (
204                    "handler::"
205                    + s["handler"]
206                    + "<"
207                    + s["hparams"]["type"]
208                    + ">(\n"
209                )
210            else:
211                e += "handler::" + s["handler"] + "(\n)"
212            for i, hp in enumerate(s["hparams"]["params"]):
213                if (i + 1) != len(s["hparams"]["params"]):
214                    e += s["hparams"][hp] + ",\n"
215                else:
216                    e += s["hparams"][hp] + "\n"
217            e += "))\n"
218            e += "\t\t)\n"
219            e += "\t)),\n"
220
221    if "init" in event["triggers"]:
222        for i in event["triggers"]["init"]:
223            e += "\tmake_trigger(trigger::init(\n"
224            if "method" in i:
225                e += "\t\tmake_handler<MethodHandler>(\n"
226                if ("type" in i["mparams"]) and (
227                    i["mparams"]["type"] is not None
228                ):
229                    e += i["method"] + "<" + i["mparams"]["type"] + ">(\n"
230                else:
231                    e += i["method"] + "(\n"
232                for ip in i["mparams"]["params"]:
233                    e += i["mparams"][ip] + ",\n"
234                if ("type" in i["hparams"]) and (
235                    i["hparams"]["type"] is not None
236                ):
237                    e += (
238                        "handler::"
239                        + i["handler"]
240                        + "<"
241                        + i["hparams"]["type"]
242                        + ">(\n"
243                    )
244                else:
245                    e += "handler::" + i["handler"] + "(\n)"
246                for i, hp in enumerate(i["hparams"]["params"]):
247                    if (i + 1) != len(i["hparams"]["params"]):
248                        e += i["hparams"][hp] + ",\n"
249                    else:
250                        e += i["hparams"][hp] + "\n"
251                e += "))\n"
252                e += "\t\t)\n"
253            e += "\t)),\n"
254
255    e += "},\n"
256
257    e += "}"
258
259    return e
260
261
262def getGroups(zNum, zCond, edata, events):
263    """
264    Extract and construct the groups for the given event.
265    """
266    groups = []
267    if ("groups" in edata) and (edata["groups"] is not None):
268        for eGroups in edata["groups"]:
269            if ("zone_conditions" in eGroups) and (
270                eGroups["zone_conditions"] is not None
271            ):
272                # Zone conditions are optional in the events yaml but skip
273                # if this event's condition is not in this zone's conditions
274                if all(
275                    "name" in z
276                    and z["name"] is not None
277                    and not any(c["name"] == z["name"] for c in zCond)
278                    for z in eGroups["zone_conditions"]
279                ):
280                    continue
281
282                # Zone numbers are optional in the events yaml but skip if this
283                # zone's zone number is not in the event's zone numbers
284                if all(
285                    "zones" in z
286                    and z["zones"] is not None
287                    and zNum not in z["zones"]
288                    for z in eGroups["zone_conditions"]
289                ):
290                    continue
291            eGroup = next(
292                g for g in events["groups"] if g["name"] == eGroups["name"]
293            )
294
295            group = {}
296            members = []
297            group["name"] = eGroup["name"]
298            for m in eGroup["members"]:
299                member = {}
300                member["path"] = eGroup["type"]
301                member["object"] = eGroup["type"] + m
302                member["interface"] = eGroups["interface"]
303                member["property"] = eGroups["property"]["name"]
304                member["type"] = eGroups["property"]["type"]
305                # Use defined service to note member on zone object
306                if ("service" in eGroup) and (eGroup["service"] is not None):
307                    member["service"] = eGroup["service"]
308                # Add expected group member's property value if given
309                if ("value" in eGroups["property"]) and (
310                    eGroups["property"]["value"] is not None
311                ):
312                    if (
313                        isinstance(eGroups["property"]["value"], str)
314                        or "string" in str(member["type"]).lower()
315                    ):
316                        member["value"] = (
317                            '"' + eGroups["property"]["value"] + '"'
318                        )
319                    else:
320                        member["value"] = eGroups["property"]["value"]
321                members.append(member)
322            group["members"] = members
323            groups.append(group)
324    return groups
325
326
327def getParameters(member, groups, section, events):
328    """
329    Extracts and constructs a section's parameters
330    """
331    params = {}
332    if ("parameters" in section) and (section["parameters"] is not None):
333        plist = []
334        for sp in section["parameters"]:
335            p = str(sp)
336            if p != "type":
337                plist.append(p)
338                if p != "group":
339                    params[p] = '"' + member[p] + '"'
340                else:
341                    params[p] = "Group\n{\n"
342                    for g in groups:
343                        for m in g["members"]:
344                            params[p] += (
345                                '{"'
346                                + str(m["object"])
347                                + '",\n'
348                                + '"'
349                                + str(m["interface"])
350                                + '",\n'
351                                + '"'
352                                + str(m["property"])
353                                + '"},\n'
354                            )
355                    params[p] += "}"
356            else:
357                params[p] = member[p]
358        params["params"] = plist
359    else:
360        params["params"] = []
361    return params
362
363
364def getInit(eGrps, eTrig, events):
365    """
366    Extracts and constructs an init trigger for the event's groups
367    which are required to be of the same type.
368    """
369    method = {}
370    methods = []
371    if len(eGrps) > 0:
372        # Use the first group member for retrieving the type
373        member = eGrps[0]["members"][0]
374        if ("method" in eTrig) and (eTrig["method"] is not None):
375            # Add method parameters
376            eMethod = next(
377                m for m in events["methods"] if m["name"] == eTrig["method"]
378            )
379            method["method"] = eMethod["name"]
380            method["mparams"] = getParameters(member, eGrps, eMethod, events)
381
382            # Add handler parameters
383            eHandler = next(
384                h for h in events["handlers"] if h["name"] == eTrig["handler"]
385            )
386            method["handler"] = eHandler["name"]
387            method["hparams"] = getParameters(member, eGrps, eHandler, events)
388
389    methods.append(method)
390
391    return methods
392
393
394def getSignal(eGrps, eTrig, events):
395    """
396    Extracts and constructs for each group member a signal
397    subscription of each match listed in the trigger.
398    """
399    signals = []
400    for group in eGrps:
401        for member in group["members"]:
402            signal = {}
403            # Add signal parameters
404            eSignal = next(
405                s for s in events["signals"] if s["name"] == eTrig["signal"]
406            )
407            signal["signal"] = eSignal["name"]
408            signal["sparams"] = getParameters(member, eGrps, eSignal, events)
409
410            # If service not given, subscribe to signal match
411            if "service" not in member:
412                # Add signal match parameters
413                eMatch = next(
414                    m
415                    for m in events["matches"]
416                    if m["name"] == eSignal["match"]
417                )
418                signal["match"] = eMatch["name"]
419                signal["mparams"] = getParameters(
420                    member, eGrps, eMatch, events
421                )
422
423            # Add handler parameters
424            eHandler = next(
425                h for h in events["handlers"] if h["name"] == eTrig["handler"]
426            )
427            signal["handler"] = eHandler["name"]
428            signal["hparams"] = getParameters(member, eGrps, eHandler, events)
429
430            signals.append(signal)
431
432    return signals
433
434
435def getTimer(eTrig):
436    """
437    Extracts and constructs the required parameters for an
438    event timer.
439    """
440    timer = {}
441    timer["interval"] = (
442        "static_cast<std::chrono::microseconds>"
443        + "("
444        + str(eTrig["interval"])
445        + ")"
446    )
447    timer["type"] = "TimerType::" + str(eTrig["type"])
448    return timer
449
450
451def getActions(zNum, zCond, edata, actions, events):
452    """
453    Extracts and constructs the make_action function call for
454    all the actions within the given event.
455    """
456    action = []
457    for eActions in actions["actions"]:
458        actions = {}
459        eAction = next(
460            a for a in events["actions"] if a["name"] == eActions["name"]
461        )
462        actions["name"] = eAction["name"]
463        actions["groups"] = getGroups(zNum, zCond, eActions, events)
464        params = []
465        if ("parameters" in eAction) and (eAction["parameters"] is not None):
466            for p in eAction["parameters"]:
467                param = "static_cast<"
468                if type(eActions[p]) is not dict:
469                    if p == "actions":
470                        param = "std::vector<Action>{"
471                        pActs = getActions(
472                            zNum, zCond, edata, eActions, events
473                        )
474                        for a in pActs:
475                            if len(a["parameters"]) != 0:
476                                param += (
477                                    "make_action(action::" + a["name"] + "(\n"
478                                )
479                                for i, ap in enumerate(a["parameters"]):
480                                    if (i + 1) != len(a["parameters"]):
481                                        param += ap + ","
482                                    else:
483                                        param += ap + ")"
484                            else:
485                                param += "make_action(action::" + a["name"]
486                            param += "),"
487                        param += "}"
488                    elif p == "defevents" or p == "altevents" or p == "events":
489                        param = "std::vector<SetSpeedEvent>{\n"
490                        for i, e in enumerate(eActions[p]):
491                            aEvent = getEvent(zNum, zCond, e, events)
492                            if not aEvent:
493                                continue
494                            if (i + 1) != len(eActions[p]):
495                                param += genEvent(aEvent) + ",\n"
496                            else:
497                                param += genEvent(aEvent) + "\n"
498                        param += "\t}"
499                    elif p == "property":
500                        if (
501                            isinstance(eActions[p], str)
502                            or "string" in str(eActions[p]["type"]).lower()
503                        ):
504                            param += (
505                                str(eActions[p]["type"]).lower()
506                                + '>("'
507                                + str(eActions[p])
508                                + '")'
509                            )
510                        else:
511                            param += (
512                                str(eActions[p]["type"]).lower()
513                                + ">("
514                                + str(eActions[p]["value"]).lower()
515                                + ")"
516                            )
517                    else:
518                        # Default type to 'size_t' when not given
519                        param += "size_t>(" + str(eActions[p]).lower() + ")"
520                else:
521                    if p == "timer":
522                        t = getTimer(eActions[p])
523                        param = (
524                            "TimerConf{"
525                            + t["interval"]
526                            + ","
527                            + t["type"]
528                            + "}"
529                        )
530                    else:
531                        param += str(eActions[p]["type"]).lower() + ">("
532                        if p != "map":
533                            if (
534                                isinstance(eActions[p]["value"], str)
535                                or "string" in str(eActions[p]["type"]).lower()
536                            ):
537                                param += '"' + str(eActions[p]["value"]) + '")'
538                            else:
539                                param += (
540                                    str(eActions[p]["value"]).lower() + ")"
541                                )
542                        else:
543                            param += (
544                                str(eActions[p]["type"]).lower()
545                                + convertToMap(str(eActions[p]["value"]))
546                                + ")"
547                            )
548                params.append(param)
549        actions["parameters"] = params
550        action.append(actions)
551    return action
552
553
554def getEvent(zone_num, zone_conditions, e, events_data):
555    """
556    Parses the sections of an event and populates the properties
557    that construct an event within the generated source.
558    """
559    event = {}
560
561    # Add set speed event name
562    event["name"] = e["name"]
563
564    # Add set speed event groups
565    event["groups"] = getGroups(zone_num, zone_conditions, e, events_data)
566
567    # Add optional set speed actions and function parameters
568    event["action"] = []
569    if ("actions" in e) and (e["actions"] is not None):
570        # List of dicts containing the list of groups and list of actions
571        sseActions = []
572        eActions = getActions(zone_num, zone_conditions, e, e, events_data)
573        for eAction in eActions:
574            # Skip events that have no groups defined for the event or actions
575            if not event["groups"] and not eAction["groups"]:
576                continue
577            # Find group in sseActions
578            grpExists = False
579            for sseDict in sseActions:
580                if eAction["groups"] == sseDict["groups"]:
581                    # Extend 'actions' list
582                    del eAction["groups"]
583                    sseDict["actions"].append(eAction)
584                    grpExists = True
585                    break
586            if not grpExists:
587                grps = eAction["groups"]
588                del eAction["groups"]
589                actList = []
590                actList.append(eAction)
591                sseActions.append({"groups": grps, "actions": actList})
592        event["action"] = sseActions
593
594    # Add event triggers
595    event["triggers"] = {}
596    for trig in e["triggers"]:
597        triggers = []
598        if trig["name"] == "timer":
599            event["triggers"]["timer"] = getTimer(trig)
600        elif trig["name"] == "signal":
601            if "signals" not in event["triggers"]:
602                event["triggers"]["signals"] = []
603            triggers = getSignal(event["groups"], trig, events_data)
604            event["triggers"]["signals"].extend(triggers)
605        elif trig["name"] == "init":
606            triggers = getInit(event["groups"], trig, events_data)
607            event["triggers"]["init"] = triggers
608
609    return event
610
611
612def addPrecondition(zNum, zCond, event, events_data):
613    """
614    Parses the precondition section of an event and populates the necessary
615    structures to generate a precondition for a set speed event.
616    """
617    precond = {}
618
619    # Add set speed event precondition name
620    precond["pcname"] = event["name"]
621
622    # Add set speed event precondition group
623    precond["pcgrps"] = getGroups(
624        zNum, zCond, event["precondition"], events_data
625    )
626
627    # Add set speed event precondition actions
628    pc = []
629    pcs = {}
630    pcs["name"] = event["precondition"]["name"]
631    epc = next(
632        p
633        for p in events_data["preconditions"]
634        if p["name"] == event["precondition"]["name"]
635    )
636    params = []
637    for p in epc["parameters"] or []:
638        param = {}
639        if p == "groups":
640            param["type"] = "std::vector<PrecondGroup>"
641            param["open"] = "{"
642            param["close"] = "}"
643            values = []
644            for group in precond["pcgrps"]:
645                for pcgrp in group["members"]:
646                    value = {}
647                    value["value"] = (
648                        'PrecondGroup{"'
649                        + str(pcgrp["object"])
650                        + '","'
651                        + str(pcgrp["interface"])
652                        + '","'
653                        + str(pcgrp["property"])
654                        + '",'
655                        + "static_cast<"
656                        + str(pcgrp["type"]).lower()
657                        + ">"
658                    )
659                    if (
660                        isinstance(pcgrp["value"], str)
661                        or "string" in str(pcgrp["type"]).lower()
662                    ):
663                        value["value"] += "(" + str(pcgrp["value"]) + ")}"
664                    else:
665                        value["value"] += (
666                            "(" + str(pcgrp["value"]).lower() + ")}"
667                        )
668                    values.append(value)
669            param["values"] = values
670        params.append(param)
671    pcs["params"] = params
672    pc.append(pcs)
673    precond["pcact"] = pc
674
675    pcevents = []
676    for pce in event["precondition"]["events"]:
677        pcevent = getEvent(zNum, zCond, pce, events_data)
678        if not pcevent:
679            continue
680        pcevents.append(pcevent)
681    precond["pcevts"] = pcevents
682
683    # Add precondition event triggers
684    precond["triggers"] = {}
685    for trig in event["precondition"]["triggers"]:
686        triggers = []
687        if trig["name"] == "timer":
688            precond["triggers"]["pctime"] = getTimer(trig)
689        elif trig["name"] == "signal":
690            if "pcsigs" not in precond["triggers"]:
691                precond["triggers"]["pcsigs"] = []
692            triggers = getSignal(precond["pcgrps"], trig, events_data)
693            precond["triggers"]["pcsigs"].extend(triggers)
694        elif trig["name"] == "init":
695            triggers = getInit(precond["pcgrps"], trig, events_data)
696            precond["triggers"]["init"] = triggers
697
698    return precond
699
700
701def getEventsInZone(zone_num, zone_conditions, events_data):
702    """
703    Constructs the event entries defined for each zone using the events yaml
704    provided.
705    """
706    events = []
707
708    if "events" in events_data:
709        for e in events_data["events"]:
710            event = {}
711
712            # Add precondition if given
713            if ("precondition" in e) and (e["precondition"] is not None):
714                event["pc"] = addPrecondition(
715                    zone_num, zone_conditions, e, events_data
716                )
717            else:
718                event = getEvent(zone_num, zone_conditions, e, events_data)
719                # Remove empty events and events that have
720                # no groups defined for the event or any of the actions
721                if not event or (
722                    not event["groups"]
723                    and all(not a["groups"] for a in event["action"])
724                ):
725                    continue
726            events.append(event)
727
728    return events
729
730
731def getFansInZone(zone_num, profiles, fan_data):
732    """
733    Parses the fan definition YAML files to find the fans
734    that match both the zone passed in and one of the
735    cooling profiles.
736    """
737
738    fans = []
739
740    for f in fan_data["fans"]:
741        if zone_num != f["cooling_zone"]:
742            continue
743
744        # 'cooling_profile' is optional (use 'all' instead)
745        if f.get("cooling_profile") is None:
746            profile = "all"
747        else:
748            profile = f["cooling_profile"]
749
750        if profile not in profiles:
751            continue
752
753        fan = {}
754        fan["name"] = f["inventory"]
755        fan["sensors"] = f["sensors"]
756        fan["target_interface"] = f.get(
757            "target_interface", "xyz.openbmc_project.Control.FanSpeed"
758        )
759        fan["target_path"] = f.get(
760            "target_path", "/xyz/openbmc_project/sensors/fan_tach/"
761        )
762        fans.append(fan)
763
764    return fans
765
766
767def getIfacesInZone(zone_ifaces):
768    """
769    Parse given interfaces for a zone for associating a zone with an interface
770    and set any properties listed to defined values upon fan control starting
771    on the zone.
772    """
773
774    ifaces = []
775    for i in zone_ifaces:
776        iface = {}
777        # Interface name not needed yet for fan zones but
778        # may be necessary as more interfaces are extended by the zones
779        iface["name"] = i["name"]
780
781        if ("properties" in i) and (i["properties"] is not None):
782            props = []
783            for p in i["properties"]:
784                prop = {}
785                prop["name"] = p["name"]
786                prop["func"] = str(p["name"]).lower()
787                prop["type"] = parse_cpp_type(p["type"])
788                if "persist" in p:
789                    persist = p["persist"]
790                    if persist is not None:
791                        if isinstance(persist, bool):
792                            prop["persist"] = "true" if persist else "false"
793                else:
794                    prop["persist"] = "false"
795                vals = []
796                for v in p["values"]:
797                    val = v["value"]
798                    if val is not None:
799                        if isinstance(val, bool):
800                            # Convert True/False to 'true'/'false'
801                            val = "true" if val else "false"
802                        elif isinstance(val, str):
803                            # Wrap strings with double-quotes
804                            val = '"' + val + '"'
805                        vals.append(val)
806                prop["values"] = vals
807                props.append(prop)
808            iface["props"] = props
809        ifaces.append(iface)
810
811    return ifaces
812
813
814def getConditionInZoneConditions(zone_condition, zone_conditions_data):
815    """
816    Parses the zone conditions definition YAML files to find the condition
817    that match both the zone condition passed in.
818    """
819
820    condition = {}
821
822    for c in zone_conditions_data["conditions"]:
823        if zone_condition != c["name"]:
824            continue
825        condition["type"] = c["type"]
826        properties = []
827        for p in c["properties"]:
828            property = {}
829            property["property"] = p["property"]
830            property["interface"] = p["interface"]
831            property["path"] = p["path"]
832            property["type"] = p["type"].lower()
833            property["value"] = str(p["value"]).lower()
834            properties.append(property)
835        condition["properties"] = properties
836
837        return condition
838
839
840def buildZoneData(zone_data, fan_data, events_data, zone_conditions_data):
841    """
842    Combines the zone definition YAML and fan
843    definition YAML to create a data structure defining
844    the fan cooling zones.
845    """
846
847    zone_groups = []
848
849    # Allow zone_conditions to not be in yaml (since its optional)
850    if not isinstance(zone_data, list) and zone_data != {}:
851        zone_data = [zone_data]
852    for group in zone_data:
853        conditions = []
854        # zone conditions are optional
855        if "zone_conditions" in group and group["zone_conditions"] is not None:
856            for c in group["zone_conditions"]:
857                if not zone_conditions_data:
858                    sys.exit(
859                        "No zone_conditions YAML file but "
860                        + "zone_conditions used in zone YAML"
861                    )
862
863                condition = getConditionInZoneConditions(
864                    c["name"], zone_conditions_data
865                )
866
867                if not condition:
868                    sys.exit("Missing zone condition " + c["name"])
869
870                conditions.append(condition)
871
872        zone_group = {}
873        zone_group["conditions"] = conditions
874
875        zones = []
876        for z in group["zones"]:
877            zone = {}
878
879            # 'zone' is required
880            if ("zone" not in z) or (z["zone"] is None):
881                sys.exit("Missing fan zone number in " + z)
882
883            zone["num"] = z["zone"]
884
885            zone["full_speed"] = z["full_speed"]
886
887            zone["default_floor"] = z["default_floor"]
888
889            # 'increase_delay' is optional (use 0 by default)
890            key = "increase_delay"
891            zone[key] = z.setdefault(key, 0)
892
893            # 'decrease_interval' is optional (use 0 by default)
894            key = "decrease_interval"
895            zone[key] = z.setdefault(key, 0)
896
897            # 'cooling_profiles' is optional (use 'all' instead)
898            if ("cooling_profiles" not in z) or (
899                z["cooling_profiles"] is None
900            ):
901                profiles = ["all"]
902            else:
903                profiles = z["cooling_profiles"]
904
905            # 'interfaces' is optional (no default)
906            ifaces = []
907            if ("interfaces" in z) and (z["interfaces"] is not None):
908                ifaces = getIfacesInZone(z["interfaces"])
909
910            fans = getFansInZone(z["zone"], profiles, fan_data)
911            events = getEventsInZone(
912                z["zone"], group.get("zone_conditions", {}), events_data
913            )
914
915            if len(fans) == 0:
916                sys.exit("Didn't find any fans in zone " + str(zone["num"]))
917
918            if ifaces:
919                zone["ifaces"] = ifaces
920            zone["fans"] = fans
921            zone["events"] = events
922            zones.append(zone)
923
924        zone_group["zones"] = zones
925        zone_groups.append(zone_group)
926
927    return zone_groups
928
929
930if __name__ == "__main__":
931    parser = ArgumentParser(description="Phosphor fan zone definition parser")
932
933    parser.add_argument(
934        "-z",
935        "--zone_yaml",
936        dest="zone_yaml",
937        default="example/zones.yaml",
938        help="fan zone definitional yaml",
939    )
940    parser.add_argument(
941        "-f",
942        "--fan_yaml",
943        dest="fan_yaml",
944        default="example/fans.yaml",
945        help="fan definitional yaml",
946    )
947    parser.add_argument(
948        "-e",
949        "--events_yaml",
950        dest="events_yaml",
951        help="events to set speeds yaml",
952    )
953    parser.add_argument(
954        "-c",
955        "--zone_conditions_yaml",
956        dest="zone_conditions_yaml",
957        help="conditions to determine zone yaml",
958    )
959    parser.add_argument(
960        "-o",
961        "--output_dir",
962        dest="output_dir",
963        default=".",
964        help="output directory",
965    )
966    args = parser.parse_args()
967
968    if not args.zone_yaml or not args.fan_yaml:
969        parser.print_usage()
970        sys.exit(1)
971
972    with open(args.zone_yaml, "r") as zone_input:
973        zone_data = yaml.safe_load(zone_input) or {}
974
975    with open(args.fan_yaml, "r") as fan_input:
976        fan_data = yaml.safe_load(fan_input) or {}
977
978    events_data = {}
979    if args.events_yaml:
980        with open(args.events_yaml, "r") as events_input:
981            events_data = yaml.safe_load(events_input) or {}
982
983    zone_conditions_data = {}
984    if args.zone_conditions_yaml:
985        with open(args.zone_conditions_yaml, "r") as zone_conditions_input:
986            zone_conditions_data = yaml.safe_load(zone_conditions_input) or {}
987
988    zone_config = buildZoneData(
989        zone_data.get("zone_configuration", {}),
990        fan_data,
991        events_data,
992        zone_conditions_data,
993    )
994
995    manager_config = zone_data.get("manager_configuration", {})
996
997    if manager_config.get("power_on_delay") is None:
998        manager_config["power_on_delay"] = 0
999
1000    tmpls_dir = os.path.join(
1001        os.path.dirname(os.path.realpath(__file__)), "templates"
1002    )
1003    output_file = os.path.join(args.output_dir, "fan_zone_defs.cpp")
1004    if sys.version_info < (3, 0):
1005        lkup = TemplateLookup(
1006            directories=tmpls_dir.split(), disable_unicode=True
1007        )
1008    else:
1009        lkup = TemplateLookup(directories=tmpls_dir.split())
1010    tmpl = lkup.get_template("fan_zone_defs.mako.cpp")
1011    with open(output_file, "w") as output:
1012        output.write(tmpl.render(zones=zone_config, mgr_data=manager_config))
1013