diff --git a/src/py/libcamera/gen-py-controls.py b/src/py/libcamera/gen-py-controls.py
index cf09c146084d..d43a7c1c7eab 100755
--- a/src/py/libcamera/gen-py-controls.py
+++ b/src/py/libcamera/gen-py-controls.py
@@ -83,7 +83,7 @@ def main(argv):
             vendors.append(vendor)
 
         for ctrl in data['controls']:
-            ctrl = Control(*ctrl.popitem(), vendor)
+            ctrl = Control(*ctrl.popitem(), vendor, args.mode)
             controls.append(extend_control(ctrl, args.mode))
 
     data = {
diff --git a/utils/codegen/controls.py b/utils/codegen/controls.py
index 03c77cc64abe..602f15b25fb6 100644
--- a/utils/codegen/controls.py
+++ b/utils/codegen/controls.py
@@ -28,7 +28,7 @@ class ControlEnum(object):
 
 
 class Control(object):
-    def __init__(self, name, data, vendor):
+    def __init__(self, name, data, vendor, mode):
         self.__name = name
         self.__data = data
         self.__enum_values = None
@@ -60,6 +60,16 @@ class Control(object):
 
             self.__size = num_elems
 
+        if mode == 'properties':
+            self.__direction = 'out'
+        else:
+            direction = self.__data.get('direction')
+            if direction is None:
+                raise RuntimeError(f'Control `{self.__name}` missing required field `{direction}`')
+            if direction not in ['in', 'out', 'inout']:
+                raise RuntimeError(f'Control `{self.__name}` direction `{direction}` is invalid; must be one of `in`, `out`, or `inout`')
+            self.__direction = direction
+
     @property
     def description(self):
         """The control description"""
@@ -111,6 +121,18 @@ class Control(object):
         else:
             return f"Span<const {typ}>"
 
+    @property
+    def direction(self):
+        in_flag = 'ControlId::Direction::In'
+        out_flag = 'ControlId::Direction::Out'
+
+        if self.__direction == 'inout':
+            return f'{in_flag} | {out_flag}'
+        if self.__direction == 'in':
+            return in_flag
+        if self.__direction == 'out':
+            return out_flag
+
     @property
     def element_type(self):
         return self.__data.get('type')
diff --git a/utils/codegen/gen-controls.py b/utils/codegen/gen-controls.py
index 3034e9a54760..59b716c1c48c 100755
--- a/utils/codegen/gen-controls.py
+++ b/utils/codegen/gen-controls.py
@@ -71,7 +71,7 @@ def main(argv):
         ctrls = controls.setdefault(vendor, [])
 
         for i, ctrl in enumerate(data['controls']):
-            ctrl = Control(*ctrl.popitem(), vendor)
+            ctrl = Control(*ctrl.popitem(), vendor, args.mode)
             ctrls.append(extend_control(ctrl, i, ranges))
 
     # Sort the vendors by range numerical value
diff --git a/utils/codegen/gen-gst-controls.py b/utils/codegen/gen-gst-controls.py
index 2601a67588a3..df0988266294 100755
--- a/utils/codegen/gen-gst-controls.py
+++ b/utils/codegen/gen-gst-controls.py
@@ -154,7 +154,7 @@ def main(argv):
         ctrls = controls.setdefault(vendor, [])
 
         for ctrl in data['controls']:
-            ctrl = Control(*ctrl.popitem(), vendor)
+            ctrl = Control(*ctrl.popitem(), vendor, mode='controls')
 
             if ctrl.name in exposed_controls:
                 ctrls.append(extend_control(ctrl))
