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