1#!/usr/bin/env python
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.template import Template
13
14tmpl = '''/* This is a generated file. */
15#include <sdbusplus/bus.hpp>
16#include "manager.hpp"
17#include "functor.hpp"
18#include "actions.hpp"
19#include "handlers.hpp"
20
21using namespace phosphor::fan::control;
22using namespace sdbusplus::bus::match::rules;
23
24const unsigned int Manager::_powerOnDelay{${mgr_data['power_on_delay']}};
25
26const std::vector<ZoneGroup> Manager::_zoneLayouts
27{
28%for zone_group in zones:
29    ZoneGroup{
30        std::vector<Condition>{
31        %for condition in zone_group['conditions']:
32            Condition{
33                "${condition['type']}",
34                std::vector<ConditionProperty>{
35                %for property in condition['properties']:
36                    ConditionProperty{
37                        "${property['property']}",
38                        "${property['interface']}",
39                        "${property['path']}",
40                        static_cast<${property['type']}>(${property['value']}),
41                    },
42                    %endfor
43                },
44            },
45            %endfor
46        },
47        std::vector<ZoneDefinition>{
48        %for zone in zone_group['zones']:
49            ZoneDefinition{
50                ${zone['num']},
51                ${zone['full_speed']},
52                ${zone['default_floor']},
53                ${zone['increase_delay']},
54                ${zone['decrease_interval']},
55                std::vector<FanDefinition>{
56                %for fan in zone['fans']:
57                    FanDefinition{
58                        "${fan['name']}",
59                        std::vector<std::string>{
60                        %for sensor in fan['sensors']:
61                            "${sensor}",
62                        %endfor
63                        }
64                    },
65                %endfor
66                },
67                std::vector<SetSpeedEvent>{
68                %for event in zone['events']:
69                    SetSpeedEvent{
70                        Group{
71                        %for member in event['group']:
72                        {
73                            "${member['name']}",
74                            {"${member['interface']}",
75                             "${member['property']}"}
76                        },
77                        %endfor
78                        },
79                        make_action(action::${event['action']['name']}(
80                        %for i, p in enumerate(event['action']['parameters']):
81                        %if (i+1) != len(event['action']['parameters']):
82                            static_cast<${p['type']}>(${p['value']}),
83                        %else:
84                            static_cast<${p['type']}>(${p['value']})
85                        %endif
86                        %endfor
87                        )),
88                        std::vector<PropertyChange>{
89                        %for s in event['signal']:
90                            PropertyChange{
91                                interface("org.freedesktop.DBus.Properties") +
92                                member("PropertiesChanged") +
93                                type::signal() +
94                                path("${s['path']}") +
95                                arg0namespace("${s['interface']}"),
96                                make_handler(propertySignal<${s['type']}>(
97                                    "${s['interface']}",
98                                    "${s['property']}",
99                                    handler::setProperty<${s['type']}>(
100                                        "${s['member']}",
101                                        "${s['interface']}",
102                                        "${s['property']}"
103                                    )
104                                ))
105                            },
106                        %endfor
107                        }
108                    },
109                %endfor
110                }
111            },
112        %endfor
113        }
114    },
115%endfor
116};
117'''
118
119
120def convertToMap(listOfDict):
121    """
122    Converts a list of dictionary entries to a std::map initialization list.
123    """
124    listOfDict = listOfDict.replace('[', '{')
125    listOfDict = listOfDict.replace(']', '}')
126    listOfDict = listOfDict.replace(':', ',')
127    return listOfDict
128
129
130def getEventsInZone(zone_num, zone_conditions, events_data):
131    """
132    Constructs the event entries defined for each zone using the events yaml
133    provided.
134    """
135    events = []
136
137    if 'events' in events_data:
138        for e in events_data['events']:
139
140            # Zone numbers are optional in the events yaml but skip if this
141            # zone's zone number is not in the event's zone numbers
142            if all('zones' in z and z['zones'] is not None and
143                   zone_num not in z['zones'] for z in e['zone_conditions']):
144                continue
145
146            # Zone conditions are optional in the events yaml but skip if this
147            # event's condition is not in this zone's conditions
148            if all('name' in z and z['name'] is not None and
149                   not any(c['name'] == z['name'] for c in zone_conditions)
150                   for z in e['zone_conditions']):
151                continue
152
153            event = {}
154            # Add set speed event group
155            group = []
156            groups = next(g for g in events_data['groups']
157                          if g['name'] == e['group'])
158            for member in groups['members']:
159                members = {}
160                members['type'] = groups['type']
161                members['name'] = ("/xyz/openbmc_project/" +
162                                   groups['type'] +
163                                   member)
164                members['interface'] = e['interface']
165                members['property'] = e['property']['name']
166                group.append(members)
167            event['group'] = group
168
169            # Add set speed action and function parameters
170            action = {}
171            actions = next(a for a in events_data['actions']
172                           if a['name'] == e['action']['name'])
173            action['name'] = actions['name']
174            params = []
175            for p in actions['parameters']:
176                param = {}
177                if type(e['action'][p]) is not dict:
178                    if p == 'property':
179                        param['value'] = str(e['action'][p]).lower()
180                        param['type'] = str(e['property']['type']).lower()
181                    else:
182                        # Default type to 'size_t' when not given
183                        param['value'] = str(e['action'][p]).lower()
184                        param['type'] = 'size_t'
185                    params.append(param)
186                else:
187                    param['type'] = str(e['action'][p]['type']).lower()
188                    if p != 'map':
189                        param['value'] = str(e['action'][p]['value']).lower()
190                    else:
191                        emap = convertToMap(str(e['action'][p]['value']))
192                        param['value'] = param['type'] + emap
193                    params.append(param)
194            action['parameters'] = params
195            event['action'] = action
196
197            # Add property change signal handler
198            signal = []
199            for path in group:
200                signals = {}
201                signals['path'] = path['name']
202                signals['interface'] = e['interface']
203                signals['property'] = e['property']['name']
204                signals['type'] = e['property']['type']
205                signals['member'] = path['name']
206                signal.append(signals)
207            event['signal'] = signal
208
209            events.append(event)
210
211    return events
212
213
214def getFansInZone(zone_num, profiles, fan_data):
215    """
216    Parses the fan definition YAML files to find the fans
217    that match both the zone passed in and one of the
218    cooling profiles.
219    """
220
221    fans = []
222
223    for f in fan_data['fans']:
224
225        if zone_num != f['cooling_zone']:
226            continue
227
228        # 'cooling_profile' is optional (use 'all' instead)
229        if f.get('cooling_profile') is None:
230            profile = "all"
231        else:
232            profile = f['cooling_profile']
233
234        if profile not in profiles:
235            continue
236
237        fan = {}
238        fan['name'] = f['inventory']
239        fan['sensors'] = f['sensors']
240        fans.append(fan)
241
242    return fans
243
244
245def getConditionInZoneConditions(zone_condition, zone_conditions_data):
246    """
247    Parses the zone conditions definition YAML files to find the condition
248    that match both the zone condition passed in.
249    """
250
251    condition = {}
252
253    for c in zone_conditions_data['conditions']:
254
255        if zone_condition != c['name']:
256            continue
257        condition['type'] = c['type']
258        properties = []
259        for p in c['properties']:
260            property = {}
261            property['property'] = p['property']
262            property['interface'] = p['interface']
263            property['path'] = p['path']
264            property['type'] = p['type'].lower()
265            property['value'] = str(p['value']).lower()
266            properties.append(property)
267        condition['properties'] = properties
268
269        return condition
270
271
272def buildZoneData(zone_data, fan_data, events_data, zone_conditions_data):
273    """
274    Combines the zone definition YAML and fan
275    definition YAML to create a data structure defining
276    the fan cooling zones.
277    """
278
279    zone_groups = []
280
281    for group in zone_data:
282        conditions = []
283        # zone conditions are optional
284        if 'zone_conditions' in group and group['zone_conditions'] is not None:
285            for c in group['zone_conditions']:
286
287                if not zone_conditions_data:
288                    sys.exit("No zone_conditions YAML file but " +
289                             "zone_conditions used in zone YAML")
290
291                condition = getConditionInZoneConditions(c['name'],
292                                                         zone_conditions_data)
293
294                if not condition:
295                    sys.exit("Missing zone condition " + c['name'])
296
297                conditions.append(condition)
298
299        zone_group = {}
300        zone_group['conditions'] = conditions
301
302        zones = []
303        for z in group['zones']:
304            zone = {}
305
306            # 'zone' is required
307            if ('zone' not in z) or (z['zone'] is None):
308                sys.exit("Missing fan zone number in " + zone_yaml)
309
310            zone['num'] = z['zone']
311
312            zone['full_speed'] = z['full_speed']
313
314            zone['default_floor'] = z['default_floor']
315
316            # 'increase_delay' is optional (use 0 by default)
317            key = 'increase_delay'
318            zone[key] = z.setdefault(key, 0)
319
320            # 'decrease_interval' is optional (use 0 by default)
321            key = 'decrease_interval'
322            zone[key] = z.setdefault(key, 0)
323
324            # 'cooling_profiles' is optional (use 'all' instead)
325            if ('cooling_profiles' not in z) or \
326                    (z['cooling_profiles'] is None):
327                profiles = ["all"]
328            else:
329                profiles = z['cooling_profiles']
330
331            fans = getFansInZone(z['zone'], profiles, fan_data)
332            events = getEventsInZone(z['zone'], group['zone_conditions'],
333                                     events_data)
334
335            if len(fans) == 0:
336                sys.exit("Didn't find any fans in zone " + str(zone['num']))
337
338            zone['fans'] = fans
339            zone['events'] = events
340            zones.append(zone)
341
342        zone_group['zones'] = zones
343        zone_groups.append(zone_group)
344
345    return zone_groups
346
347
348if __name__ == '__main__':
349    parser = ArgumentParser(
350        description="Phosphor fan zone definition parser")
351
352    parser.add_argument('-z', '--zone_yaml', dest='zone_yaml',
353                        default="example/zones.yaml",
354                        help='fan zone definitional yaml')
355    parser.add_argument('-f', '--fan_yaml', dest='fan_yaml',
356                        default="example/fans.yaml",
357                        help='fan definitional yaml')
358    parser.add_argument('-e', '--events_yaml', dest='events_yaml',
359                        help='events to set speeds yaml')
360    parser.add_argument('-c', '--zone_conditions_yaml',
361                        dest='zone_conditions_yaml',
362                        help='conditions to determine zone yaml')
363    parser.add_argument('-o', '--output_dir', dest='output_dir',
364                        default=".",
365                        help='output directory')
366    args = parser.parse_args()
367
368    if not args.zone_yaml or not args.fan_yaml:
369        parser.print_usage()
370        sys.exit(-1)
371
372    with open(args.zone_yaml, 'r') as zone_input:
373        zone_data = yaml.safe_load(zone_input) or {}
374
375    with open(args.fan_yaml, 'r') as fan_input:
376        fan_data = yaml.safe_load(fan_input) or {}
377
378    events_data = {}
379    if args.events_yaml:
380        with open(args.events_yaml, 'r') as events_input:
381            events_data = yaml.safe_load(events_input) or {}
382
383    zone_conditions_data = {}
384    if args.zone_conditions_yaml:
385        with open(args.zone_conditions_yaml, 'r') as zone_conditions_input:
386            zone_conditions_data = yaml.safe_load(zone_conditions_input) or {}
387
388    zone_config = buildZoneData(zone_data.get('zone_configuration', {}),
389                                fan_data, events_data, zone_conditions_data)
390
391    manager_config = zone_data.get('manager_configuration', {})
392
393    if manager_config.get('power_on_delay') is None:
394        manager_config['power_on_delay'] = 0
395
396    output_file = os.path.join(args.output_dir, "fan_zone_defs.cpp")
397    with open(output_file, 'w') as output:
398        output.write(Template(tmpl).render(zones=zone_config,
399                     mgr_data=manager_config))
400